From a9910181e9f090eedc3a20dadca09fe663f445d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr> Date: Sat, 10 Jul 2021 13:17:37 +0200 Subject: [PATCH] Add instance shared module --- .../components/cone-search/dec.component.html | 92 ++++++++ .../components/cone-search/dec.component.ts | 221 ++++++++++++++++++ .../shared/components/cone-search/index.ts | 13 ++ .../cone-search/input-group.component.scss | 12 + .../components/cone-search/ra.component.html | 92 ++++++++ .../components/cone-search/ra.component.ts | 218 +++++++++++++++++ .../cone-search/radius.component.html | 27 +++ .../cone-search/radius.component.scss | 14 ++ .../cone-search/radius.component.ts | 78 +++++++ .../cone-search/resolver.component.html | 15 ++ .../cone-search/resolver.component.ts | 45 ++++ .../datatable/datatable.component.html | 128 ++++++++++ .../datatable/datatable.component.scss | 41 ++++ .../datatable/datatable.component.ts | 162 +++++++++++++ .../shared/components/datatable/index.ts | 5 + .../app/instance/shared/components/index.ts | 7 + client/src/app/instance/shared/pipes/index.ts | 5 + .../shared/pipes/pretty-operator.pipe.ts | 27 +++ .../src/app/instance/shared/shared.module.ts | 25 ++ .../app/instance/shared/validators/index.ts | 2 + .../validators/nan-validator.directive.ts | 30 +++ .../validators/range-validator.directive.ts | 32 +++ .../instance/store/models/criterion.model.ts | 2 +- 23 files changed, 1292 insertions(+), 1 deletion(-) create mode 100644 client/src/app/instance/shared/components/cone-search/dec.component.html create mode 100644 client/src/app/instance/shared/components/cone-search/dec.component.ts create mode 100644 client/src/app/instance/shared/components/cone-search/index.ts create mode 100644 client/src/app/instance/shared/components/cone-search/input-group.component.scss create mode 100644 client/src/app/instance/shared/components/cone-search/ra.component.html create mode 100644 client/src/app/instance/shared/components/cone-search/ra.component.ts create mode 100644 client/src/app/instance/shared/components/cone-search/radius.component.html create mode 100644 client/src/app/instance/shared/components/cone-search/radius.component.scss create mode 100644 client/src/app/instance/shared/components/cone-search/radius.component.ts create mode 100644 client/src/app/instance/shared/components/cone-search/resolver.component.html create mode 100644 client/src/app/instance/shared/components/cone-search/resolver.component.ts create mode 100644 client/src/app/instance/shared/components/datatable/datatable.component.html create mode 100644 client/src/app/instance/shared/components/datatable/datatable.component.scss create mode 100644 client/src/app/instance/shared/components/datatable/datatable.component.ts create mode 100644 client/src/app/instance/shared/components/datatable/index.ts create mode 100644 client/src/app/instance/shared/components/index.ts create mode 100644 client/src/app/instance/shared/pipes/index.ts create mode 100644 client/src/app/instance/shared/pipes/pretty-operator.pipe.ts create mode 100644 client/src/app/instance/shared/shared.module.ts create mode 100644 client/src/app/instance/shared/validators/index.ts create mode 100644 client/src/app/instance/shared/validators/nan-validator.directive.ts create mode 100644 client/src/app/instance/shared/validators/range-validator.directive.ts diff --git a/client/src/app/instance/shared/components/cone-search/dec.component.html b/client/src/app/instance/shared/components/cone-search/dec.component.html new file mode 100644 index 00000000..93539d69 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/dec.component.html @@ -0,0 +1,92 @@ +<div class="row px-3"> + <label>DEC</label> + <div class="input-group"> + <input type="text" class="form-control" [formControl]="decDegree" (input)="decChange()" autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">°</span> + </div> + </div> +</div> + +<div class="row mt-2 px-3"> + <div class="col px-0 pr-xl-1"> + <div class="input-group"> + <input type="text" + class="form-control" + [formControl]="decH" + (input)="decChange()" + (focusin)="changeFocus('dech', true)" + (focusout)="changeFocus('dech', false)" + (change)="setToDefaultValue()" + autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">°</span> + </div> + </div> + </div> + <div class="w-100 d-block d-xl-none"></div> + <div class="col mt-1 mt-xl-auto px-0 pr-xl-1"> + <div class="input-group"> + <input type="text" + class="form-control" + [formControl]="decM" + (input)="decChange()" + (focusin)="changeFocus('decm', true)" + (focusout)="changeFocus('decm', false)" + (change)="setToDefaultValue()" + autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">'</span> + </div> + </div> + </div> + <div class="w-100 d-block d-xl-none"></div> + <div class="col mt-1 mt-xl-auto px-0"> + <div class="input-group"> + <input type="text" + class="form-control" + [formControl]="decS" + (input)="decChange()" + (focusin)="changeFocus('decs', true)" + (focusout)="changeFocus('decs', false)" + (change)="setToDefaultValue()" + autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">''</span> + </div> + </div> + </div> +</div> + +<div *ngIf="decDegree.invalid" class="row px-3 text-danger"> + <div *ngIf="decDegree.errors.nan"> + {{ decDegree.errors.nan.value }} + </div> + <div *ngIf="decDegree.errors.range" [hidden]="decDegree.errors.nan"> + {{ decDegree.errors.range.value }} + </div> +</div> +<div *ngIf="decH.invalid" class="row px-3 text-danger"> + <div *ngIf="decH.errors.nan"> + {{ decH.errors.nan.value }} + </div> + <div *ngIf="decH.errors.range" [hidden]="decH.errors.nan"> + {{ decH.errors.range.value }} + </div> +</div> +<div *ngIf="decM.invalid" class="row px-3 text-danger"> + <div *ngIf="decM.errors.nan"> + {{ decM.errors.nan.value }} + </div> + <div *ngIf="decM.errors.range" [hidden]="decM.errors.nan"> + {{ decM.errors.range.value }} + </div> +</div> +<div *ngIf="decS.invalid" class="row px-3 text-danger"> + <div *ngIf="decS.errors.nan"> + {{ decS.errors.nan.value }} + </div> + <div *ngIf="decS.errors.range" [hidden]="decS.errors.nan"> + {{ decS.errors.range.value }} + </div> +</div> diff --git a/client/src/app/instance/shared/components/cone-search/dec.component.ts b/client/src/app/instance/shared/components/cone-search/dec.component.ts new file mode 100644 index 00000000..35660315 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/dec.component.ts @@ -0,0 +1,221 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; + +import { nanValidator, rangeValidator } from '../../validators'; +import { ConeSearch, Resolver } from 'src/app/instance/store/models'; + +@Component({ + selector: 'app-dec', + templateUrl: 'dec.component.html', + styleUrls: ['input-group.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc DEC component. + */ +export class DecComponent { + /** + * Disables DEC fields. + * + * @param {boolean} disabled - If the field has to be disabled. + */ + @Input() + set disabled(disabled: boolean) { + this.isDisabled = disabled; + this.initFields(); + } + /** + * Sets RA, DEC and radius from cone search. + * + * @param {ConeSearch} coneSearch - The cone search. + */ + @Input() + set coneSearch(coneSearch: ConeSearch) { + this.ra = coneSearch.ra; + this.radius = coneSearch.radius; + if (coneSearch.dec) { + this.decDegree.setValue(coneSearch.dec); + if(this.decDegree.valid && !this.decHFocused && !this.decMFocused && !this.decSFocused) { + this.decDegree2HMS(coneSearch.dec); + } + } else { + this.decDegree.reset(); + this.decH.reset(); + this.decM.reset(); + this.decS.reset(); + } + this.initFields(); + } + /** + * Sets RA from resolver. + * + * @param {Resolver} resolver - The resolver. + */ + @Input() + set resolver(resolver: Resolver) { + this.resolvedDec = null; + if (resolver) { + this.resolvedDec = resolver.dec; + this.decDegree.setValue(resolver.dec); + this.decDegree2HMS(resolver.dec); + } + } + /** + * Sets isDegree. + * + * @param {string} unit - The unit. + */ + @Input() + set unit(unit: string) { + unit === 'degree' ? this.isDegree = true : this.isDegree = false; + this.initFields(); + } + @Output() updateConeSearch: EventEmitter<ConeSearch> = new EventEmitter(); + @Output() deleteResolver: EventEmitter<null> = new EventEmitter(); + + ra: number; + radius: number; + isDisabled = false; + isDegree = true; + resolvedDec: number; + decHFocused: boolean = false; + decMFocused: boolean = false; + decSFocused: boolean = false; + + decDegree = new FormControl('', [Validators.required, nanValidator, rangeValidator(-90, 90, 'DEC')]); + decH = new FormControl('', [nanValidator, rangeValidator(-90, 90, 'Degree')]); + decM = new FormControl('', [nanValidator, rangeValidator(0, 60, 'Minutes')]); + decS = new FormControl('', [nanValidator, rangeValidator(0, 60, 'Seconds')]); + + /** + * Sets DEC fields. + */ + initFields(): void { + if (this.isDisabled) { + this.decDegree.disable(); + this.decH.disable(); + this.decM.disable(); + this.decS.disable(); + } else if (this.isDegree) { + this.decDegree.enable(); + this.decH.disable(); + this.decM.disable(); + this.decS.disable(); + } else { + this.decDegree.disable(); + this.decH.enable(); + this.decM.enable(); + this.decS.enable(); + } + } + + /** + * Converts DEC hour minute second from degree and sets DEC HMS fields. + * + * @param {number} value - The degree value. + */ + decDegree2HMS(value: number): void { + const hh = Math.trunc(value); + let tmp = (Math.abs(value - hh)) * 60; + const mm = Math.trunc(tmp); + tmp = (tmp - mm) * 60; + const ss = tmp.toFixed(2); + this.decH.setValue(hh); + this.decM.setValue(mm); + this.decS.setValue(ss); + } + + /** + * Sets DEC degree from hour minute second and sets DEC degree field. + */ + decHMS2Degree(): void { + const hh = +this.decH.value; + const mm = +this.decM.value; + const ss = +this.decS.value; + const tmp = ((ss / 60) + mm) / 60; + let deg = tmp + Math.abs(hh); + if (hh < 0) { + deg = -deg; + } + this.decDegree.setValue(+deg.toFixed(8)); + } + + /** + * Changes fields focus. + * + * @param {string} field - The field. + * @param {boolean} isFocused - Is the field is focused. + */ + changeFocus(field: string, isFocused: boolean) { + switch (field) { + case 'dech': + this.decHFocused = isFocused; + break; + case 'decm': + this.decMFocused = isFocused; + break + case 'decs': + this.decSFocused = isFocused; + break; + } + } + + /** + * Manages DEC value change. + */ + decChange(): void { + if (this.isDegree) { + if (this.decDegree.valid) { + this.decDegree2HMS(this.decDegree.value); + } else { + this.decH.reset(); + this.decM.reset(); + this.decS.reset(); + } + this.updateConeSearch.emit({ ra: this.ra, dec: this.decDegree.value, radius: this.radius } as ConeSearch); + } else { + if (this.decH.valid && this.decM.valid && this.decS.valid) { + this.setToDefaultValue(); + this.decHMS2Degree(); + this.updateConeSearch.emit({ ra: this.ra, dec: this.decDegree.value, radius: this.radius } as ConeSearch); + } else { + this.decDegree.reset(); + } + } + this.resetResolver(); + } + + /** + * Sets DEC hour minute second fields to default value if not valid. + */ + setToDefaultValue(): void { + if (this.decH.value === '' || this.decH.value === null) { + this.decH.setValue(0); + } + if (this.decM.value === '' || this.decM.value === null) { + this.decM.setValue(0); + } + if (this.decS.value === '' || this.decS.value === null) { + this.decS.setValue(0); + } + } + + /** + * Emits reset resolver event. + */ + resetResolver(): void { + if (this.resolvedDec && this.resolvedDec !== this.decDegree.value) { + this.deleteResolver.emit(); + } + } +} diff --git a/client/src/app/instance/shared/components/cone-search/index.ts b/client/src/app/instance/shared/components/cone-search/index.ts new file mode 100644 index 00000000..5f42c030 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/index.ts @@ -0,0 +1,13 @@ +import { ConeSearchComponent } from './cone-search.component'; +import { ResolverComponent } from './resolver.component'; +import { RaComponent } from './ra.component'; +import { DecComponent } from './dec.component'; +import { RadiusComponent } from './radius.component'; + +export const coneSearchComponents = [ + ConeSearchComponent, + ResolverComponent, + RaComponent, + DecComponent, + RadiusComponent +]; diff --git a/client/src/app/instance/shared/components/cone-search/input-group.component.scss b/client/src/app/instance/shared/components/cone-search/input-group.component.scss new file mode 100644 index 00000000..ff4e60f4 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/input-group.component.scss @@ -0,0 +1,12 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +.input-group-text { + width: 36px; +} diff --git a/client/src/app/instance/shared/components/cone-search/ra.component.html b/client/src/app/instance/shared/components/cone-search/ra.component.html new file mode 100644 index 00000000..e1ff92f6 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/ra.component.html @@ -0,0 +1,92 @@ +<div class="row px-3"> + <label>RA</label> + <div class="input-group"> + <input type="text" class="form-control" [formControl]="raDegree" (input)="raChange()" autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">°</span> + </div> + </div> +</div> + +<div class="row mt-2 px-3"> + <div class="col px-0 pr-xl-1"> + <div class="input-group"> + <input type="text" + class="form-control" + [formControl]="raH" + (input)="raChange()" + (focusin)="changeFocus('rah', true)" + (focusout)="changeFocus('rah', false)" + (change)="setToDefaultValue()" + autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">H</span> + </div> + </div> + </div> + <div class="w-100 d-block d-xl-none"></div> + <div class="col mt-1 mt-xl-auto px-0 pr-xl-1"> + <div class="input-group"> + <input type="text" + class="form-control" + [formControl]="raM" + (input)="raChange()" + (focusin)="changeFocus('ram', true)" + (focusout)="changeFocus('ram', false)" + (change)="setToDefaultValue()" + autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">'</span> + </div> + </div> + </div> + <div class="w-100 d-block d-xl-none"></div> + <div class="col mt-1 mt-xl-auto px-0"> + <div class="input-group"> + <input type="text" + class="form-control" + [formControl]="raS" + (input)="raChange()" + (focusin)="changeFocus('ras', true)" + (focusout)="changeFocus('ras', false)" + (change)="setToDefaultValue()" + autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">''</span> + </div> + </div> + </div> +</div> + +<div *ngIf="raDegree.invalid" class="row px-3 text-danger"> + <div *ngIf="raDegree.errors.nan"> + {{ raDegree.errors.nan.value }} + </div> + <div *ngIf="raDegree.errors.range" [hidden]="raDegree.errors.nan"> + {{ raDegree.errors.range.value }} + </div> +</div> +<div *ngIf="raH.invalid" class="row px-3 text-danger"> + <div *ngIf="raH.errors.nan"> + {{ raH.errors.nan.value }} + </div> + <div *ngIf="raH.errors.range" [hidden]="raH.errors.nan"> + {{ raH.errors.range.value }} + </div> +</div> +<div *ngIf="raM.invalid" class="row px-3 text-danger"> + <div *ngIf="raM.errors.nan"> + {{ raM.errors.nan.value }} + </div> + <div *ngIf="raM.errors.range" [hidden]="raM.errors.nan"> + {{ raM.errors.range.value }} + </div> +</div> +<div *ngIf="raS.invalid" class="row px-3 text-danger"> + <div *ngIf="raS.errors.nan"> + {{ raS.errors.nan.value }} + </div> + <div *ngIf="raS.errors.range" [hidden]="raS.errors.nan"> + {{ raS.errors.range.value }} + </div> +</div> diff --git a/client/src/app/instance/shared/components/cone-search/ra.component.ts b/client/src/app/instance/shared/components/cone-search/ra.component.ts new file mode 100644 index 00000000..30050f20 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/ra.component.ts @@ -0,0 +1,218 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; + +import { nanValidator, rangeValidator } from '../../validators'; +import { ConeSearch, Resolver } from 'src/app/instance/store/models'; + +@Component({ + selector: 'app-ra', + templateUrl: 'ra.component.html', + styleUrls: ['input-group.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc RA component. + */ +export class RaComponent { + /** + * Disables RA fields. + * + * @param {boolean} disabled - If the field has to be disabled. + */ + @Input() + set disabled(disabled: boolean) { + this.isDisabled = disabled; + this.initFields(); + } + /** + * Sets RA, DEC and radius from cone search. + * + * @param {ConeSearch} coneSearch - The cone search. + */ + @Input() + set coneSearch(coneSearch: ConeSearch) { + this.dec = coneSearch.dec; + this.radius = coneSearch.radius; + if (coneSearch.ra) { + this.raDegree.setValue(coneSearch.ra); + if (this.raDegree.valid && !this.raHFocused && !this.raMFocused && !this.raSFocused) { + this.raDegree2HMS(coneSearch.ra); + } + } else { + this.raDegree.reset(); + this.raH.reset(); + this.raM.reset(); + this.raS.reset(); + } + this.initFields(); + } + /** + * Sets RA from resolver. + * + * @param {Resolver} resolver - The resolver. + */ + @Input() + set resolver(resolver: Resolver) { + this.resolvedRa = null; + if (resolver) { + this.resolvedRa = resolver.ra; + this.raDegree.setValue(resolver.ra); + this.raDegree2HMS(resolver.ra); + } + } + /** + * Sets isDegree. + * + * @param {string} unit - The unit. + */ + @Input() + set unit(unit: string) { + unit === 'degree' ? this.isDegree = true : this.isDegree = false; + this.initFields(); + } + @Output() updateConeSearch: EventEmitter<ConeSearch> = new EventEmitter(); + @Output() deleteResolver: EventEmitter<null> = new EventEmitter(); + + dec: number = null; + radius: number = null; + isDisabled = false; + isDegree = true; + resolvedRa: number; + raHFocused: boolean = false; + raMFocused: boolean = false; + raSFocused: boolean = false; + + raDegree = new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 360, 'RA')]); + raH = new FormControl('', [nanValidator, rangeValidator(0, 24, 'Hours')]); + raM = new FormControl('', [nanValidator, rangeValidator(0, 60, 'Minutes')]); + raS = new FormControl('', [nanValidator, rangeValidator(0, 60, 'Seconds')]); + + /** + * Sets RA fields. + */ + initFields(): void { + if (this.isDisabled) { + this.raDegree.disable(); + this.raH.disable(); + this.raM.disable(); + this.raS.disable(); + } else if (this.isDegree) { + this.raDegree.enable(); + this.raH.disable(); + this.raM.disable(); + this.raS.disable(); + } else { + this.raDegree.disable(); + this.raH.enable(); + this.raM.enable(); + this.raS.enable(); + } + } + + /** + * Converts RA hour minute second from degree and sets RA HMS fields. + * + * @param {number} value - The degree value. + */ + raDegree2HMS(value: number): void { + let tmp = value / 15; + const hh = Math.trunc(tmp); + tmp = (tmp - hh) * 60; + const mm = Math.trunc(tmp); + tmp = (tmp - mm) * 60; + const ss = +tmp.toFixed(2); + this.raH.setValue(hh); + this.raM.setValue(mm); + this.raS.setValue(ss); + } + + /** + * Sets RA degree from hour minute second and sets RA degree field. + */ + raHMS2Degree(): void { + const hh = +this.raH.value; + const mm = +this.raM.value; + const ss = +this.raS.value; + const deg = +(((((ss / 60) + mm) / 60) + hh) * 15).toFixed(8); + this.raDegree.setValue(deg); + } + + /** + * Changes fields focus. + * + * @param {string} field - The field. + * @param {boolean} isFocused - Is the field is focused. + */ + changeFocus(field: string, isFocused: boolean): void { + switch (field) { + case 'rah': + this.raHFocused = isFocused; + break; + case 'ram': + this.raMFocused = isFocused; + break + case 'ras': + this.raSFocused = isFocused; + break; + } + } + + /** + * Manages RA value change. + */ + raChange(): void { + if (this.isDegree) { + if (this.raDegree.valid) { + this.raDegree2HMS(this.raDegree.value); + } else { + this.raH.reset(); + this.raM.reset(); + this.raS.reset(); + } + this.updateConeSearch.emit({ ra: this.raDegree.value, dec: this.dec, radius: this.radius } as ConeSearch); + } else { + if (this.raH.valid && this.raM.valid && this.raS.valid) { + this.setToDefaultValue(); + this.raHMS2Degree(); + this.updateConeSearch.emit({ ra: this.raDegree.value, dec: this.dec, radius: this.radius } as ConeSearch); + } else { + this.raDegree.reset(); + } + } + this.resetResolver(); + } + + /** + * Sets RA hour minute second fields to default value if not valid. + */ + setToDefaultValue(): void { + if (this.raH.value === '' || this.raH.value === null) { + this.raH.setValue(0); + } + if (this.raM.value === '' || this.raM.value === null) { + this.raM.setValue(0); + } + if (this.raS.value === '' || this.raS.value === null) { + this.raS.setValue(0); + } + } + + /** + * Emits reset resolver event. + */ + resetResolver(): void { + if (this.resolvedRa && this.resolvedRa !== this.raDegree.value) { + this.deleteResolver.emit(); + } + } +} diff --git a/client/src/app/instance/shared/components/cone-search/radius.component.html b/client/src/app/instance/shared/components/cone-search/radius.component.html new file mode 100644 index 00000000..406cdfc3 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/radius.component.html @@ -0,0 +1,27 @@ +<div class="row"> + <div class="col form-group mb-0"> + <label>Radius</label> + <input #rr + type="range" + min="0" + max="150" + [formControl]="radiusRange" + (input)="radiusChange(rr.value)" + class="form-control-range mt-2" + autocomplete="off"> + </div> + <div class="w-100 d-block d-lg-none"></div> + <div class="col col-lg-auto form-group mb-0"> + <div class="input-group mt-4"> + <input #rf id="radius-field" type="number" class="form-control" [formControl]="radiusField" (input)="radiusChange(rf.value)" autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">arcsecond</span> + </div> + </div> + </div> + <div *ngIf="radiusField.invalid" class="col-12 text-danger"> + <div *ngIf="radiusField.errors.range"> + {{ radiusField.errors.range.value }} + </div> + </div> +</div> diff --git a/client/src/app/instance/shared/components/cone-search/radius.component.scss b/client/src/app/instance/shared/components/cone-search/radius.component.scss new file mode 100644 index 00000000..fcb10249 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/radius.component.scss @@ -0,0 +1,14 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +@media (min-width: 992px) { + #radius-field { + width: 100px; + } +} diff --git a/client/src/app/instance/shared/components/cone-search/radius.component.ts b/client/src/app/instance/shared/components/cone-search/radius.component.ts new file mode 100644 index 00000000..5add3abf --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/radius.component.ts @@ -0,0 +1,78 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { rangeValidator } from '../../validators'; +import { ConeSearch } from 'src/app/instance/store/models'; + +@Component({ + selector: 'app-radius', + templateUrl: 'radius.component.html', + styleUrls: ['radius.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Radius component. + */ +export class RadiusComponent { + /** + * Sets RA, DEC and radius from cone search. + * + * @param {ConeSearch} coneSearch - The cone search. + */ + @Input() + set coneSearch(coneSearch: ConeSearch) { + this.ra = coneSearch.ra; + this.dec = coneSearch.dec; + if (coneSearch.radius) { + this.radiusField.setValue(coneSearch.radius); + this.radiusRange.setValue(coneSearch.radius); + } else { + this.radiusRange.setValue(0); + this.radiusField.setValue(0); + } + } + /** + * Disables radius fields. + * + * @param {boolean} disabled - If the field has to be disabled. + */ + @Input() + set disabled(disabled: boolean) { + if (disabled) { + this.radiusField.disable(); + this.radiusRange.disable(); + } else { + this.radiusField.enable(); + this.radiusRange.enable(); + } + } + @Output() updateConeSearch: EventEmitter<ConeSearch> = new EventEmitter(); + + ra: number; + dec: number; + radiusRange = new FormControl(''); + radiusField = new FormControl('', [rangeValidator(0, 150, 'Radius')]); + + /** + * Sets radius value form inputs and emits cone search event. + * + * @param {string} value - The value of radius. + * + * @fires EventEmitter<ConeSearch> + */ + radiusChange(value: string): void { + this.radiusField.setValue(+value); + this.radiusRange.setValue(+value); + this.updateConeSearch.emit({ ra: this.ra, dec: this.dec, radius: +value } as ConeSearch); + } +} diff --git a/client/src/app/instance/shared/components/cone-search/resolver.component.html b/client/src/app/instance/shared/components/cone-search/resolver.component.html new file mode 100644 index 00000000..86714689 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/resolver.component.html @@ -0,0 +1,15 @@ +<div class="row"> + <div class="col pr-0"> + <label for="resolver">Resolve RA and DEC with Sesame Name Resolver</label> + <input #n id="resolver" type="text" class="form-control" [formControl]="field" autocomplete="off"> + </div> + <div class="col-auto pt-5 pt-lg-4"> + <button *ngIf="!resolverWip" id="btn-search" class="btn btn-outline-secondary mt-2" [disabled]="field.disabled" (click)="resolveName.emit(n.value)"> + <span class="fas fa-search"></span> + </button> + <button *ngIf="resolverWip" id="btn-wip" class="btn btn-outline-secondary mt-2" disabled> + <span class="fas fa-circle-notch fa-spin"></span> + <span class="sr-only">Loading...</span> + </button> + </div> +</div> diff --git a/client/src/app/instance/shared/components/cone-search/resolver.component.ts b/client/src/app/instance/shared/components/cone-search/resolver.component.ts new file mode 100644 index 00000000..1c017555 --- /dev/null +++ b/client/src/app/instance/shared/components/cone-search/resolver.component.ts @@ -0,0 +1,45 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { Resolver } from 'src/app/instance/store/models'; + +@Component({ + selector: 'app-resolver', + templateUrl: 'resolver.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Resolver component. + */ +export class ResolverComponent { + @Input() + set disabled(disabled: boolean) { + if (disabled) { + this.field.disable(); + } else { + this.field.enable(); + } + } + @Input() resolverWip: boolean; + @Input() + set resolver(resolver: Resolver) { + if (resolver) { + this.field.setValue(resolver.name); + } else { + this.field.reset(); + } + } + @Output() resolveName: EventEmitter<string> = new EventEmitter(); + + field = new FormControl(''); +} diff --git a/client/src/app/instance/shared/components/datatable/datatable.component.html b/client/src/app/instance/shared/components/datatable/datatable.component.html new file mode 100644 index 00000000..a16b9561 --- /dev/null +++ b/client/src/app/instance/shared/components/datatable/datatable.component.html @@ -0,0 +1,128 @@ +<div *ngIf="!requiredParams()" class="text-center"> + <span class="fas fa-circle-notch fa-spin fa-3x"></span> + <span class="sr-only">Loading...</span> +</div> +<div *ngIf="requiredParams()"> + <div *ngIf="dataset.config.datatable.selectable_row" class="mb-2"> + <button [disabled]="noSelectedData() || processWip" (click)="executeProcess.emit('csv')" + class="btn btn-sm btn-outline-primary"> + To CSV + </button> + <span *ngIf="processWip" class="float-right mr-2"> + <span class="fas fa-circle-notch fa-spin fa-2x"></span> + </span> + <a *ngIf="processDone" href="http://0.0.0.0:8085/{{ processId }}.csv" + class="btn btn-sm btn-outline-secondary float-right"> + Download your CSV + </a> + </div> + <div class="table-responsive"> + <table class="table table-bordered table-hover"> + <thead> + <tr> + <th *ngIf="dataset.config.datatable.selectable_row">#</th> + <th *ngFor="let attribute of getOutputList()" scope="col" class="clickable" (click)="sort(attribute.id)"> + {{ attribute.label }} + <span *ngIf="attribute.id === sortedCol" class="pl-2"> + <span [ngClass]="{'active': sortedOrder === 'a', 'inactive': sortedOrder === 'd'}"> + <span class="fas fa-fw fa-sort-amount-down-alt"></span> + </span> + <span [ngClass]="{'active': sortedOrder === 'd', 'inactive': sortedOrder === 'a'}"> + <span class="fas fa-fw fa-sort-amount-up"></span> + </span> + </span> + <span *ngIf="attribute.id !== sortedCol" class="pl-2"> + <span class="unsorted"> + <span class="fas fa-fw fa-arrows-alt-v"></span> + </span> + <span class="on-hover"> + <span class="fas fa-fw fa-sort-amount-down-alt"></span> + </span> + </span> + </th> + </tr> + </thead> + <tbody> + <tr *ngFor="let datum of data"> + <td *ngIf="dataset.config.datatable.selectable_row" class="data-selected" + (click)="toggleSelection(datum)"> + <button class="btn btn-block text-left p-0 m-0"> + <span *ngIf="!isSelected(datum)"> + <span class="far fa-square fa-lg text-secondary"></span> + </span> + <span *ngIf="isSelected(datum)"> + <span class="fas fa-check-square fa-lg theme-color"></span> + </span> + </button> + </td> + <td *ngFor="let attribute of getOutputList()" class="align-middle"> + <div [ngSwitch]="attribute.renderer"> + <div *ngSwitchCase="'detail'"> + <app-detail + [value]="datum[attribute.label]" + [datasetName]="dataset.name" + [config]="attribute.renderer_config"> + </app-detail> + </div> + <div *ngSwitchCase="'link'"> + <app-link + [value]="datum[attribute.label]" + [datasetName]="dataset.name" + [config]="attribute.renderer_config"> + </app-link> + </div> + <div *ngSwitchCase="'download'"> + <app-download + [value]="datum[attribute.label]" + [datasetName]="dataset.name" + [config]="attribute.renderer_config"> + </app-download> + </div> + <div *ngSwitchCase="'image'"> + <app-image + [value]="datum[attribute.label]" + [datasetName]="dataset.name" + [config]="attribute.renderer_config"> + </app-image> + </div> + <div *ngSwitchCase="'json'"> + <app-json + [value]="datum[attribute.label]" + [attributeLabel]="attribute.label" + [config]="attribute.renderer_config"> + </app-json> + </div> + <div *ngSwitchDefault> + {{ datum[attribute.label] }} + </div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + <div class="row mt-3"> + <div class="col"> + Showing + <select class="custom-select" (change)="changeNbItems($event.target.value)"> + <option value="10" selected="true">10</option> + <option value="20">20</option> + <option value="50">50</option> + <option value="100">100</option> + </select> + of {{ dataLength }} items + </div> + <div class="col-auto"> + <pagination + [(ngModel)]="page" + [totalItems]="dataLength" + [boundaryLinks]="true" + [rotate]="true" + [maxSize]="5" + [itemsPerPage]="nbItems" + previousText="‹" nextText="›" firstText="«" lastText="»" + (pageChanged)="changePage($event.page)"> + </pagination> + </div> + </div> +</div> diff --git a/client/src/app/instance/shared/components/datatable/datatable.component.scss b/client/src/app/instance/shared/components/datatable/datatable.component.scss new file mode 100644 index 00000000..25eb4367 --- /dev/null +++ b/client/src/app/instance/shared/components/datatable/datatable.component.scss @@ -0,0 +1,41 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + + .data-selected { + cursor: pointer; +} + +.data-selected button:focus { + box-shadow: none; +} + +ul { + margin-bottom: 0; +} + +.custom-select { + width: fit-content; +} + +.clickable:hover { + cursor: pointer; + background-color: #F7F7F7; +} + +.unsorted { + color: #c5c5c5; +} + +.clickable:hover .unsorted, .on-hover, .inactive, .clickable:hover .active { + display: none; +} + +.clickable:hover .on-hover, .clickable:hover .inactive { + display: inline; +} diff --git a/client/src/app/instance/shared/components/datatable/datatable.component.ts b/client/src/app/instance/shared/components/datatable/datatable.component.ts new file mode 100644 index 00000000..33150c61 --- /dev/null +++ b/client/src/app/instance/shared/components/datatable/datatable.component.ts @@ -0,0 +1,162 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { Attribute, Dataset } from 'src/app/metamodel/models'; +import { Pagination, PaginationOrder } from 'src/app/instance/store/models'; + + +@Component({ + selector: 'app-datatable', + templateUrl: 'datatable.component.html', + styleUrls: ['datatable.component.scss'], +}) +/** + * @class + * @classdesc Datatable component. + * + * @implements OnInit + */ +export class DatatableComponent implements OnInit { + @Input() dataset: Dataset; + @Input() attributeList: Attribute[]; + @Input() outputList: number[]; + @Input() data: any[]; + @Input() dataLength: number; + @Input() selectedData: any[] = []; + @Input() processWip: boolean = false; + @Input() processDone: boolean = false; + @Input() processId: string = null; + @Output() getData: EventEmitter<Pagination> = new EventEmitter(); + @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); + @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); + @Output() executeProcess: EventEmitter<string> = new EventEmitter(); + nbItems = 10; + page = 1; + sortedCol: number = null; + sortedOrder: PaginationOrder = PaginationOrder.a; + + ngOnInit() { + this.sortedCol = this.attributeList.find(a => a.order_by).id; + } + + /** + * Checks if required parameters to display datatable are passed to the component. + * + * @return boolean + */ + requiredParams(): boolean { + if (this.attributeList.length === 0 || this.outputList.length === 0 || !this.dataLength) { + return false; + } + return true; + } + + /** + * Checks if there is no data selected. + * + * @return boolean + */ + noSelectedData(): boolean { + return this.selectedData.length < 1; + } + + /** + * Returns output list from attribute list. + * + * @return Attribute[] + */ + getOutputList(): Attribute[] { + return this.attributeList + .filter(a => this.outputList.includes(a.id)) + .sort((a, b) => a.output_display - b.output_display); + } + + /** + * Emits events to select or unselect data. + * + * @param {any} datum - The data to select or unselect. + * + * @fires EventEmitter<number | string> + */ + toggleSelection(datum: any): void { + const attribute = this.attributeList.find(a => a.search_flag === 'ID'); + const index = this.selectedData.indexOf(datum[attribute.label]); + if (index > -1) { + this.deleteSelectedData.emit(datum[attribute.label]); + } else { + this.addSelectedData.emit(datum[attribute.label]); + } + } + + /** + * Checks if data is selected. + * + * @param {any} datum - The data. + * + * @return boolean + */ + isSelected(datum: any): boolean { + const attribute = this.attributeList.find(a => a.search_flag === 'ID'); + + if (this.selectedData.indexOf(datum[attribute.label]) > -1) { + return true; + } + return false; + } + + /** + * Emits event to change datatable page. + * + * @param {number} nb - The page number to access. + * + * @fires EventEmitter<Pagination> + */ + changePage(nb: number): void { + this.page = nb; + const pagination: Pagination = { + dname: this.dataset.name, + page: this.page, + nbItems: this.nbItems, + sortedCol: this.sortedCol, + order: this.sortedOrder + }; + this.getData.emit(pagination); + } + + /** + * Emits event to change datatable displayed items. + * + * @param {number} nb - The number of items to display. + * + * @fires EventEmitter<Pagination> + */ + changeNbItems(nb: number): void { + this.nbItems = nb; + this.changePage(1); + } + + /** + * Emits event to change the sorted order and the sorted column of the datatable. + * + * @param {number} id - The id of the column to sort. + * + * @fires EventEmitter<Pagination> + */ + sort(id: number): void { + if (id === this.sortedCol) { + this.sortedOrder = this.sortedOrder === PaginationOrder.a ? PaginationOrder.d : PaginationOrder.a; + } else { + this.sortedCol = id; + this.sortedOrder = PaginationOrder.a; + } + this.changePage(1); + } +} diff --git a/client/src/app/instance/shared/components/datatable/index.ts b/client/src/app/instance/shared/components/datatable/index.ts new file mode 100644 index 00000000..c0ec2582 --- /dev/null +++ b/client/src/app/instance/shared/components/datatable/index.ts @@ -0,0 +1,5 @@ +import { DatatableComponent } from "./datatable.component"; + +export const datatableComponents = [ + DatatableComponent +]; diff --git a/client/src/app/instance/shared/components/index.ts b/client/src/app/instance/shared/components/index.ts new file mode 100644 index 00000000..1bd2ebbd --- /dev/null +++ b/client/src/app/instance/shared/components/index.ts @@ -0,0 +1,7 @@ +import { coneSearchComponents } from './cone-search'; +import { datatableComponents } from './datatable'; + +export const sharedComponents = [ + coneSearchComponents, + datatableComponents +]; diff --git a/client/src/app/instance/shared/pipes/index.ts b/client/src/app/instance/shared/pipes/index.ts new file mode 100644 index 00000000..08f7ea71 --- /dev/null +++ b/client/src/app/instance/shared/pipes/index.ts @@ -0,0 +1,5 @@ +import { PrettyOperatorPipe } from './pretty-operator.pipe'; + +export const sharedPipes = [ + PrettyOperatorPipe +]; diff --git a/client/src/app/instance/shared/pipes/pretty-operator.pipe.ts b/client/src/app/instance/shared/pipes/pretty-operator.pipe.ts new file mode 100644 index 00000000..05cd736f --- /dev/null +++ b/client/src/app/instance/shared/pipes/pretty-operator.pipe.ts @@ -0,0 +1,27 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +import { getPrettyOperator } from 'src/app/instance/store/models'; + +/** + * @class + * @classdesc Translate Anis string operator to a pretty form label operator. + * + * @example + * // formats eq to = + * {{ eq | prettyOperator }} + */ +@Pipe({ name: 'prettyOperator' }) +export class PrettyOperatorPipe implements PipeTransform { + transform(operator: string): string { + return getPrettyOperator(operator); + } +} diff --git a/client/src/app/instance/shared/shared.module.ts b/client/src/app/instance/shared/shared.module.ts new file mode 100644 index 00000000..78bdbd61 --- /dev/null +++ b/client/src/app/instance/shared/shared.module.ts @@ -0,0 +1,25 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { NgModule } from '@angular/core'; + +import { sharedComponents } from './components'; +import { sharedPipes } from './pipes'; + +@NgModule({ + declarations: [ + sharedComponents, + sharedPipes + ], + exports: [ + sharedComponents, + sharedPipes + ] +}) +export class SharedModule { } diff --git a/client/src/app/instance/shared/validators/index.ts b/client/src/app/instance/shared/validators/index.ts new file mode 100644 index 00000000..6cb062e6 --- /dev/null +++ b/client/src/app/instance/shared/validators/index.ts @@ -0,0 +1,2 @@ +export * from './range-validator.directive'; +export * from './nan-validator.directive'; diff --git a/client/src/app/instance/shared/validators/nan-validator.directive.ts b/client/src/app/instance/shared/validators/nan-validator.directive.ts new file mode 100644 index 00000000..eb5d9d72 --- /dev/null +++ b/client/src/app/instance/shared/validators/nan-validator.directive.ts @@ -0,0 +1,30 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { FormControl } from '@angular/forms'; + +/** + * Validates NaN. + * + * @param {FormControl} control - The form control where to check NaN. + * + * @return {[key: string]: any} | null + */ +export function nanValidator(control: FormControl): {[key: string]: any} | null { + const value = parseFloat(control.value); + const regexp: RegExp = /^\-?\d+([.,]+\d*)?$/; + const isFloat = regexp.test(control.value); + if (control.value === '' || control.value === null) { + return null; + } + if (isNaN(value) || !isFloat) { + return { 'nan': { value: control.value + ' is not a number' } }; + } + return null; +} diff --git a/client/src/app/instance/shared/validators/range-validator.directive.ts b/client/src/app/instance/shared/validators/range-validator.directive.ts new file mode 100644 index 00000000..fa4ef419 --- /dev/null +++ b/client/src/app/instance/shared/validators/range-validator.directive.ts @@ -0,0 +1,32 @@ +/** + * This file is part of Anis Client. + * + * @copyright Laboratoire d'Astrophysique de Marseille / CNRS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { ValidatorFn, AbstractControl } from '@angular/forms'; + +/** + * Validates range. + * + * @param {number} min - The minimum for the range. + * @param {number} max - The maximum for the range. + * @param {string} [formLabel] - The label of the form displayed in error message. + * + * @return ValidatorFn + */ +export function rangeValidator(min: number, max: number, formLabel?: string): ValidatorFn { + return (control: AbstractControl): {[key: string]: any} | null => { + const value = parseFloat(control.value); + if (value < min || value > max) { + if (formLabel) { + return { 'range': { value: formLabel + ' must be between ' + min + ' and ' + max } }; + } + return { 'range': { value: 'Must be between ' + min + ' and ' + max } }; + } + return null; + } +} diff --git a/client/src/app/instance/store/models/criterion.model.ts b/client/src/app/instance/store/models/criterion.model.ts index 1b2f328a..81d6161c 100644 --- a/client/src/app/instance/store/models/criterion.model.ts +++ b/client/src/app/instance/store/models/criterion.model.ts @@ -173,7 +173,7 @@ export const getPrettyCriterion = (criterion: Criterion): string => { * // returns = * getPrettyOperator('eq') */ -const getPrettyOperator = (operator: string): string => { +export const getPrettyOperator = (operator: string): string => { switch (operator) { case 'eq': return '='; -- GitLab