diff --git a/client/src/app/instance/instance.reducer.ts b/client/src/app/instance/instance.reducer.ts index 0c1981cf76c6306499f8c3167c22227f8dedc97f..f4454161d4f8074368ddc3d583a050798ea7abf4 100644 --- a/client/src/app/instance/instance.reducer.ts +++ b/client/src/app/instance/instance.reducer.ts @@ -13,17 +13,20 @@ import { RouterReducerState } from 'src/app/custom-route-serializer'; import * as metamodel from './store/reducers/metamodel.reducer'; import * as search from './store/reducers/search.reducer'; import * as samp from './store/reducers/samp.reducer'; +import * as coneSearch from './store/reducers/cone-search.reducer'; export interface State { metamodel: metamodel.State, search: search.State, - samp: samp.State + samp: samp.State, + coneSearch: coneSearch.State } const reducers = { metamodel: metamodel.metamodelReducer, search: search.searchReducer, - samp: samp.sampReducer + samp: samp.sampReducer, + coneSearch: coneSearch.coneSearchReducer }; export const instanceReducer = combineReducers(reducers); diff --git a/client/src/app/instance/search/components/criteria/cone-search-tab.component.html b/client/src/app/instance/search/components/criteria/cone-search-tab.component.html new file mode 100644 index 0000000000000000000000000000000000000000..ea951c32bd3106f1c745ad5e18876c73d592bad5 --- /dev/null +++ b/client/src/app/instance/search/components/criteria/cone-search-tab.component.html @@ -0,0 +1,37 @@ +<accordion *ngIf="(datasetList | datasetByName:datasetSelected).config.cone_search.cone_search_enabled" [isAnimated]="true"> + <accordion-group #ag [panelClass]="'custom-accordion'" [isOpen]="(datasetList | datasetByName:datasetSelected).config.cone_search.cone_search_opened" class="my-2"> + <button class="btn btn-link btn-block clearfix" accordion-heading> + <span class="pull-left float-left"> + Cone search + + <span *ngIf="ag.isOpen"> + <span class="fas fa-chevron-up"></span> + </span> + <span *ngIf="!ag.isOpen"> + <span class="fas fa-chevron-down"></span> + </span> + </span> + </button> + <div class="row"> + <div class="col"> + <app-cone-search + [coneSearch]="coneSearch" + [resolver]="resolver" + [resolverIsLoading]="resolverIsLoading" + [resolverIsLoaded]="resolverIsLoaded" + (addConeSearch)="addConeSearch.emit($event)" + (deleteConeSearch)="deleteConeSearch.emit()" + (retrieveCoordinates)="retrieveCoordinates.emit($event)" #cs> + </app-cone-search> + </div> + <div class="col-2 text-center align-self-end"> + <button class="btn btn-outline-success" *ngIf="!coneSearch" [hidden]="cs.form.invalid" (click)="addConeSearch.emit(cs.getConeSearch())"> + <span class="fas fa-plus fa-fw"></span> + </button> + <button class="btn btn-outline-danger" *ngIf="coneSearch" (click)="deleteConeSearch.emit()"> + <span class="fa fa-times fa-fw"></span> + </button> + </div> + </div> + </accordion-group> +</accordion> diff --git a/client/src/app/instance/search/components/criteria/cone-search-tab.component.ts b/client/src/app/instance/search/components/criteria/cone-search-tab.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..85a3f259e3df262a93c190d37c7733d31b1743dc --- /dev/null +++ b/client/src/app/instance/search/components/criteria/cone-search-tab.component.ts @@ -0,0 +1,34 @@ +/** + * 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 { Dataset } from 'src/app/metamodel/models'; +import { ConeSearch, Resolver } from 'src/app/instance/store/models'; + +@Component({ + selector: 'app-cone-search-tab', + templateUrl: 'cone-search-tab.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +/** + * @class + * @classdesc Search cone search tab component. + */ +export class ConeSearchTabComponent { + @Input() datasetSelected: string; + @Input() datasetList: Dataset[]; + @Input() coneSearch: ConeSearch; + @Input() resolver: Resolver; + @Input() resolverIsLoading: boolean; + @Input() resolverIsLoaded: boolean; + @Output() addConeSearch: EventEmitter<ConeSearch> = new EventEmitter(); + @Output() deleteConeSearch: EventEmitter<{ }> = new EventEmitter(); + @Output() retrieveCoordinates: EventEmitter<string> = new EventEmitter(); +} diff --git a/client/src/app/instance/search/components/criteria/index.ts b/client/src/app/instance/search/components/criteria/index.ts index bdee82025471c6892a6af287afb3c151e019d2fd..88656ad3c2668f4eb770c6db2672fb8302115a68 100644 --- a/client/src/app/instance/search/components/criteria/index.ts +++ b/client/src/app/instance/search/components/criteria/index.ts @@ -1,8 +1,10 @@ +import { ConeSearchTabComponent } from './cone-search-tab.component'; import { CriteriaTabsComponent } from './criteria-tabs.component'; import { CriteriaByFamilyComponent } from './criteria-by-family.component'; import { searchTypeComponents } from './search-type'; export const criteriaComponents = [ + ConeSearchTabComponent, CriteriaTabsComponent, CriteriaByFamilyComponent, searchTypeComponents diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.html b/client/src/app/instance/search/components/result/datatable-tab.component.html index 670acae1dd47d1c6dc836cbd4f78e0cec4592d37..66316d7af426d1a63ecfb6c0e2078d6b9d6d5605 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.html +++ b/client/src/app/instance/search/components/result/datatable-tab.component.html @@ -1,5 +1,5 @@ -<accordion *ngIf="getDataset().config.datatable.datatable_enabled" [isAnimated]="true"> - <accordion-group #ag [isOpen]="getDataset().config.datatable.datatable_opened" [panelClass]="'custom-accordion'" class="my-2"> +<accordion *ngIf="(datasetList | datasetByName:datasetSelected).config.datatable.datatable_enabled" [isAnimated]="true"> + <accordion-group #ag [isOpen]="(datasetList | datasetByName:datasetSelected).config.datatable.datatable_opened" [panelClass]="'custom-accordion'" class="my-2"> <button class="btn btn-link btn-block clearfix" accordion-heading> <span class="pull-left float-left"> Display result details @@ -9,7 +9,7 @@ </span> </button> <app-datatable - [dataset]="getDataset()" + [dataset]="datasetList | datasetByName:datasetSelected" [attributeList]="attributeList" [outputList]="outputList" [dataLength]="dataLength" diff --git a/client/src/app/instance/search/components/result/datatable-tab.component.ts b/client/src/app/instance/search/components/result/datatable-tab.component.ts index 0b7b2258095c4ff700fc940781e0ed6a4dd5d6af..c2d7dd1dd0ce224bd65ad333c910a84809bc0c1d 100644 --- a/client/src/app/instance/search/components/result/datatable-tab.component.ts +++ b/client/src/app/instance/search/components/result/datatable-tab.component.ts @@ -34,13 +34,4 @@ export class DatatableTabComponent { @Output() retrieveData: EventEmitter<Pagination> = new EventEmitter(); @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); - - /** - * Returns selected dataset for the search. - * - * @return Dataset - */ - getDataset(): Dataset { - return this.datasetList.find(dataset => dataset.name === this.datasetSelected); - } } diff --git a/client/src/app/instance/search/components/result/reminder.component.html b/client/src/app/instance/search/components/result/reminder.component.html index d76e57d79282e0ffe5efefd8ab799a41ee6036e9..38372acef640c24c36328c786925527dd5ecfd8f 100644 --- a/client/src/app/instance/search/components/result/reminder.component.html +++ b/client/src/app/instance/search/components/result/reminder.component.html @@ -24,14 +24,14 @@ </div> <div *ngIf="nbCriteria() > 0" class="row"> - <!-- <div *ngIf="isConeSearchAdded" class="col-12 col-md-6 col-xl-4 pb-3"> + <div *ngIf="coneSearch" class="col-12 col-md-6 col-xl-4 pb-3"> <span class="title">Cone search</span> <ul class="list-unstyled pl-3"> <li>RA = {{ coneSearch.ra }}°</li> <li>DEC = {{ coneSearch.dec }}°</li> <li>radius = {{ coneSearch.radius }} arcsecond</li> </ul> - </div> --> + </div> <ng-container *ngFor="let family of criteriaFamilyList"> <ng-container *ngIf="criteriaByFamily(family.id).length > 0"> diff --git a/client/src/app/instance/search/components/result/reminder.component.ts b/client/src/app/instance/search/components/result/reminder.component.ts index 9961ece07a9ed7101ff1de5aa915187a94884628..8e3d88ea4ffc5584ba30b77ba2168c0a1e993cd0 100644 --- a/client/src/app/instance/search/components/result/reminder.component.ts +++ b/client/src/app/instance/search/components/result/reminder.component.ts @@ -28,9 +28,8 @@ export class ReminderComponent { @Input() criteriaFamilyList: CriteriaFamily[]; @Input() outputFamilyList: OutputFamily[]; @Input() outputCategoryList: OutputCategory[]; - // @Input() isConeSearchAdded: boolean; - // @Input() coneSearch: ConeSearch; @Input() criteriaList: Criterion[]; + @Input() coneSearch: ConeSearch; @Input() outputList: number[]; isSummaryActivated(): boolean { @@ -49,9 +48,9 @@ export class ReminderComponent { * @return number */ nbCriteria(): number { - // if (this.isConeSearchAdded) { - // return this.criteriaList.length + 1; - // } + if (this.coneSearch) { + return this.criteriaList.length + 1; + } return this.criteriaList.length; } diff --git a/client/src/app/instance/search/components/summary.component.html b/client/src/app/instance/search/components/summary.component.html index 0fc5455ed4c10d7ee751b4823bb686211b37d056..86c376fb03367425c7f6a4776396d78ee31c6422 100644 --- a/client/src/app/instance/search/components/summary.component.html +++ b/client/src/app/instance/search/components/summary.component.html @@ -15,14 +15,14 @@ <p *ngIf="noCriteria()" class="pl-5 font-weight-bold"> No selected criteria </p> - <!-- <span *ngIf="isConeSearchAdded" class="pl-5"> + <span *ngIf="coneSearch" class="pl-5"> Cone search: <ul class="ml-3 pl-5 list-unstyled"> <li>RA = {{ coneSearch.ra }}°</li> <li>DEC = {{ coneSearch.dec }}°</li> <li>radius = {{ coneSearch.radius }} arcsecond</li> </ul> - </span> --> + </span> <ul *ngIf="criteriaList.length > 0" class="pl-5 list-unstyled"> <li *ngFor="let criterion of criteriaList"> {{ getAttribute(criterion.id).form_label }} {{ printCriterion(criterion) }} diff --git a/client/src/app/instance/search/components/summary.component.ts b/client/src/app/instance/search/components/summary.component.ts index dfa03f44f4c86f23bb6fb8ab10ad3e5a8600b462..3e76c6275872536dca5f00257e2969ea611ad3b3 100644 --- a/client/src/app/instance/search/components/summary.component.ts +++ b/client/src/app/instance/search/components/summary.component.ts @@ -9,9 +9,8 @@ import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core'; -import { Criterion, SearchQueryParams, getPrettyCriterion } from '../../store/models'; +import { Criterion, ConeSearch, SearchQueryParams, getPrettyCriterion } from '../../store/models'; import { Attribute, Dataset, CriteriaFamily, OutputFamily, OutputCategory } from 'src/app/metamodel/models'; -// import { ConeSearch } from '../../shared/cone-search/store/model'; @Component({ selector: 'app-summary', @@ -27,8 +26,6 @@ export class SummaryComponent { @Input() currentStep: string; @Input() datasetSelected: string; @Input() datasetList: Dataset[]; - // @Input() isConeSearchAdded: boolean; - // @Input() coneSearch: ConeSearch; @Input() attributeList: Attribute[]; @Input() criteriaFamilyList: CriteriaFamily[]; @Input() outputFamilyList: OutputFamily[]; @@ -36,6 +33,7 @@ export class SummaryComponent { @Input() criteriaList: Criterion[]; @Input() outputList: number[]; @Input() queryParams: SearchQueryParams; + @Input() coneSearch: ConeSearch; accordionFamilyIsOpen = true; @@ -54,8 +52,7 @@ export class SummaryComponent { * @return boolean */ noCriteria(): boolean { - //if (this.isConeSearchAdded || this.criteriaList.length > 0) { - if (this.criteriaList.length > 0) { + if (this.coneSearch || this.criteriaList.length > 0) { return false } return true; diff --git a/client/src/app/instance/search/containers/abstract-search.component.ts b/client/src/app/instance/search/containers/abstract-search.component.ts index a6ebdbbebc3bb3175648b19ddadfb4adc037c880..6774ae781e2266fbb6a6eb2503c477954393eef0 100644 --- a/client/src/app/instance/search/containers/abstract-search.component.ts +++ b/client/src/app/instance/search/containers/abstract-search.component.ts @@ -3,7 +3,7 @@ import { Directive, OnInit, OnDestroy } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable, Subscription } from 'rxjs'; -import { Criterion, SearchQueryParams } from '../../store/models'; +import { ConeSearch, Criterion, SearchQueryParams } from '../../store/models'; import { Dataset, Attribute, CriteriaFamily, OutputFamily, OutputCategory } from 'src/app/metamodel/models'; import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; @@ -14,6 +14,7 @@ import * as outputFamilySelector from 'src/app/metamodel/selectors/output-family import * as outputCategorySelector from 'src/app/metamodel/selectors/output-category.selector'; import * as searchActions from '../../store/actions/search.actions'; import * as searchSelector from '../../store/selectors/search.selector'; +import * as coneSearchSelector from '../../store/selectors/cone-search.selector'; @Directive() export abstract class AbstractSearchComponent implements OnInit, OnDestroy { @@ -39,6 +40,7 @@ export abstract class AbstractSearchComponent implements OnInit, OnDestroy { public criteriaList: Observable<Criterion[]>; public outputList: Observable<number[]>; public queryParams: Observable<SearchQueryParams>; + public coneSearch: Observable<ConeSearch>; private attributeListIsLoadedSubscription: Subscription; @@ -65,6 +67,7 @@ export abstract class AbstractSearchComponent implements OnInit, OnDestroy { this.criteriaList = this.store.select(searchSelector.selectCriteriaList); this.outputList = this.store.select(searchSelector.selectOutputList); this.queryParams = this.store.select(searchSelector.selectQueryParams); + this.coneSearch = this.store.select(coneSearchSelector.selectConeSearch); } ngOnInit() { diff --git a/client/src/app/instance/search/containers/criteria.component.html b/client/src/app/instance/search/containers/criteria.component.html index 5fb87883773d70c4baf88ec337f5320fec6bd919..8ff2d1d903ea462d493393f6efa68700811f4265 100644 --- a/client/src/app/instance/search/containers/criteria.component.html +++ b/client/src/app/instance/search/containers/criteria.component.html @@ -7,13 +7,17 @@ && (criteriaFamilyListIsLoaded | async) && (attributeListIsLoaded | async)" class="row mt-4"> <div class="col-12 col-md-8 col-lg-9"> - <!-- <app-cone-search-tab + <app-cone-search-tab [datasetSelected]="datasetSelected | async" [datasetList]="datasetList | async" - [isConeSearchAdded]="isConeSearchAdded | async" - [isValidConeSearch]="isValidConeSearch | async" - (coneSearchAdded)="coneSearchAdded($event)"> - </app-cone-search-tab> --> + [coneSearch]="coneSearch | async" + [resolver]="resolver | async" + [resolverIsLoading]="resolverIsLoading | async" + [resolverIsLoaded]="resolverIsLoaded | async" + (addConeSearch)="addConeSearch($event)" + (deleteConeSearch)="deleteConeSearch()" + (retrieveCoordinates)="retrieveCoordinates($event)"> + </app-cone-search-tab> <app-criteria-tabs [attributeList]="attributeList | async" [criteriaFamilyList]="criteriaFamilyList | async" @@ -34,7 +38,8 @@ [outputCategoryList]="outputCategoryList | async" [criteriaList]="criteriaList | async" [outputList]="outputList | async" - [queryParams]="queryParams | async"> + [queryParams]="queryParams | async" + [coneSearch]="coneSearch | async"> </app-summary> </div> </div> @@ -47,7 +52,7 @@ </a> </div> <div class="col col-auto"> - <a routerLink="/search/output/{{ datasetSelected | async }}" [queryParams]="queryParams | async" + <a routerLink="/instance/{{ instanceSelected | async }}/search/output/{{ datasetSelected | async }}" [queryParams]="queryParams | async" class="btn btn-outline-primary"> Output <span class="fas fa-arrow-right"></span> </a> diff --git a/client/src/app/instance/search/containers/criteria.component.ts b/client/src/app/instance/search/containers/criteria.component.ts index a226c24ed0131bc8d5bfba3dc12a8f590121fd29..b03d239dc5f4ac0bc2e9a84b79b23d81a2ce6cd8 100644 --- a/client/src/app/instance/search/containers/criteria.component.ts +++ b/client/src/app/instance/search/containers/criteria.component.ts @@ -9,9 +9,14 @@ import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + import { AbstractSearchComponent } from './abstract-search.component'; -import { Criterion } from '../../store/models'; +import { ConeSearch, Criterion, Resolver } from '../../store/models'; import * as searchActions from '../../store/actions/search.actions'; +import * as coneSearchActions from '../../store/actions/cone-search.actions'; +import * as coneSearchSelector from '../../store/selectors/cone-search.selector'; @Component({ selector: 'app-criteria', @@ -22,6 +27,17 @@ import * as searchActions from '../../store/actions/search.actions'; * @classdesc Search criteria container. */ export class CriteriaComponent extends AbstractSearchComponent { + public resolver: Observable<Resolver>; + public resolverIsLoading: Observable<boolean>; + public resolverIsLoaded: Observable<boolean>; + + constructor(protected store: Store<{ }>) { + super(store); + this.resolver = this.store.select(coneSearchSelector.selectResolver); + this.resolverIsLoading = this.store.select(coneSearchSelector.selectResolverIsLoading); + this.resolverIsLoaded = this.store.select(coneSearchSelector.selectResolverIsLoaded); + } + ngOnInit() { Promise.resolve(null).then(() => this.store.dispatch(searchActions.changeStep({ step: 'criteria' }))); Promise.resolve(null).then(() => this.store.dispatch(searchActions.checkCriteria())); @@ -45,4 +61,16 @@ export class CriteriaComponent extends AbstractSearchComponent { deleteCriterion(idCriterion: number): void { this.store.dispatch(searchActions.deleteCriterion({ idCriterion })); } + + addConeSearch(coneSearch: ConeSearch): void { + this.store.dispatch(coneSearchActions.addConeSearch({ coneSearch })); + } + + deleteConeSearch(): void { + this.store.dispatch(coneSearchActions.deleteConeSearch()); + } + + retrieveCoordinates(name: string): void { + this.store.dispatch(coneSearchActions.retrieveCoordinates({ name })); + } } diff --git a/client/src/app/instance/search/containers/dataset.component.html b/client/src/app/instance/search/containers/dataset.component.html index b86d581556b64900a383598f8beae670d3dff20f..77107988152d7cafa0de1aee2c1d0172768d3afd 100644 --- a/client/src/app/instance/search/containers/dataset.component.html +++ b/client/src/app/instance/search/containers/dataset.component.html @@ -36,7 +36,8 @@ [outputCategoryList]="outputCategoryList | async" [criteriaList]="criteriaList | async" [outputList]="outputList | async" - [queryParams]="queryParams | async"> + [queryParams]="queryParams | async" + [coneSearch]="coneSearch | async"> </app-summary> </div> </ng-container> diff --git a/client/src/app/instance/search/containers/output.component.html b/client/src/app/instance/search/containers/output.component.html index 9b0cdec4da374ed048fea9eea3630a1628a1e605..12d7f60558376fa5c8b4a679cc8fa1a2b4eafce0 100644 --- a/client/src/app/instance/search/containers/output.component.html +++ b/client/src/app/instance/search/containers/output.component.html @@ -27,7 +27,8 @@ [outputCategoryList]="outputCategoryList | async" [criteriaList]="criteriaList | async" [outputList]="outputList | async" - [queryParams]="queryParams | async"> + [queryParams]="queryParams | async" + [coneSearch]="coneSearch | async"> </app-summary> </div> </div> diff --git a/client/src/app/instance/search/containers/result.component.html b/client/src/app/instance/search/containers/result.component.html index 9eacc2ffbff8b44a625d444390f47af89e481e08..ab045ae224c68d9ae47765843967539f45be1aa7 100644 --- a/client/src/app/instance/search/containers/result.component.html +++ b/client/src/app/instance/search/containers/result.component.html @@ -36,6 +36,7 @@ [outputFamilyList]="outputFamilyList | async" [outputCategoryList]="outputCategoryList | async" [criteriaList]="criteriaList | async" + [coneSearch]="coneSearch | async" [outputList]="outputList | async"> </app-reminder> <app-samp diff --git a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.html b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.html index c0cf4330cf393e0886bfdffecd414e6a7720b607..6e676e971e10e1ebf73318e490770eb147abdae1 100644 --- a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.html +++ b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.html @@ -1,47 +1,29 @@ <div class="row pb-4"> <div class="col"> <app-resolver - [resolverWip]="resolverWip | async" - [resolver]="resolver | async" - [disabled]="disabled" - (resolveName)="retrieveCoordinates($event)"> + [coneSearch]="coneSearch" + [resolver]="resolver" + [resolverIsLoading]="resolverIsLoading" + [resolverIsLoaded]="resolverIsLoaded" + (retrieveCoordinates)="retrieveCoordinates.emit($event)"> </app-resolver> </div> </div> <div class="row"> <div class="col pb-4"> - <app-ra - [coneSearch]="coneSearch | async" - [resolver]="resolver | async" - [unit]="unit" - [disabled]="disabled" - (updateConeSearch)="updateConeSearch($event)" - (deleteResolver)="deleteResolver()"> + <app-ra [form]="form" [unit]="unit" [resolver]="resolver"> </app-ra> </div> <div class="col-auto p-0 align-self-center"> - <button class="btn btn-outline-secondary" - [disabled]="disabled" - (click)="unit === 'degree' ? unit = 'hms' : unit = 'degree'" - title="Change unit"> - <span class="fas fa-sync-alt"></span> + <button class="btn btn-outline-secondary" [disabled]="coneSearch" (click)="unit === 'degree' ? unit = 'hms' : unit = 'degree'" title="Change unit"> + <span class="fas fa-sync-alt"></span> </button> </div> <div class="col"> - <app-dec - [coneSearch]="coneSearch | async" - [resolver]="resolver | async" - [unit]="unit" - [disabled]="disabled" - (updateConeSearch)="updateConeSearch($event)" - (deleteResolver)="deleteResolver()"> + <app-dec [form]="form" [unit]="unit" [resolver]="resolver"> </app-dec> </div> <div class="col-12"> - <app-radius - [coneSearch]="coneSearch | async" - [disabled]="disabled" - (updateConeSearch)="updateConeSearch($event)"> - </app-radius> + <app-radius [form]="form"></app-radius> </div> </div> diff --git a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts index d506c3180a782ca2f64141bb863c8e39b23ca25e..ca589c83866668ad1f6fd8953bb7567d637889df 100644 --- a/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts +++ b/client/src/app/instance/shared-search/components/cone-search/cone-search.component.ts @@ -7,9 +7,11 @@ * file that was distributed with this source code. */ -import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; import { ConeSearch, Resolver } from 'src/app/instance/store/models'; +import { nanValidator, rangeValidator } from '../../validators'; @Component({ selector: 'app-cone-search', @@ -19,14 +21,60 @@ import { ConeSearch, Resolver } from 'src/app/instance/store/models'; * @class * @classdesc Cone search container. */ -export class ConeSearchComponent { - @Input() disabled: boolean = false; - @Input() resolverWip: boolean; - @Input() resolver: Resolver; +export class ConeSearchComponent implements OnChanges { @Input() coneSearch: ConeSearch; + @Input() resolver: Resolver; + @Input() resolverIsLoading: boolean; + @Input() resolverIsLoaded: boolean; + @Output() addConeSearch: EventEmitter<ConeSearch> = new EventEmitter(); + @Output() deleteConeSearch: EventEmitter<{ }> = new EventEmitter(); @Output() retrieveCoordinates: EventEmitter<string> = new EventEmitter(); - @Output() updateConeSearch: EventEmitter<ConeSearch> = new EventEmitter(); - @Output() deleteResolver: EventEmitter<{}> = new EventEmitter(); - unit = 'degree'; + public form = new FormGroup({ + ra: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 360, 'RA')]), + ra_hms: new FormGroup({ + h: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 24, 'Hours')]), + m: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 60, 'Minutes')]), + s: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 60, 'Seconds')]) + }), + dec: new FormControl('', [Validators.required, nanValidator, rangeValidator(-90, 90, 'DEC')]), + dec_dms: new FormGroup({ + d: new FormControl('', [Validators.required, nanValidator, rangeValidator(-90, 90, 'Degree')]), + m: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 60, 'Minutes')]), + s: new FormControl('', [Validators.required, nanValidator, rangeValidator(0, 60, 'Seconds')]) + }), + radius: new FormControl(2, [Validators.required, rangeValidator(0, 150, 'Radius')]) + }); + + public unit = 'degree'; + + ngOnChanges(changes: SimpleChanges) { + if (changes.resolver && changes.resolver.currentValue) { + this.unit = 'degree'; + } + + if (changes.coneSearch && !changes.coneSearch.currentValue) { + if (this.unit = 'degree') { + this.form.controls.ra.enable(); + this.form.controls.dec.enable(); + this.form.controls.radius.enable(); + } else { + this.form.controls.ra_hms.enable(); + this.form.controls.dec_dms.enable(); + } + } + + if (changes.coneSearch && changes.coneSearch.currentValue) { + this.form.patchValue(this.coneSearch); + this.form.disable(); + } + } + + getConeSearch(): ConeSearch { + return { + ra: this.form.controls.ra.value, + dec: this.form.controls.dec.value, + radius: this.form.controls.radius.value + }; + } } diff --git a/client/src/app/instance/shared-search/components/cone-search/dec.component.html b/client/src/app/instance/shared-search/components/cone-search/dec.component.html index 93539d69f28e7d93341ffa4e254b8440f9acc3aa..c8fe7677d7fa8cafd270f39ecdfae51c2630b970 100644 --- a/client/src/app/instance/shared-search/components/cone-search/dec.component.html +++ b/client/src/app/instance/shared-search/components/cone-search/dec.component.html @@ -1,92 +1,75 @@ -<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"> +<form [formGroup]="form" novalidate> + <div class="row px-3"> + <label>DEC</label> <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"> + <input type="number" class="form-control" formControlName="dec" 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 formGroupName="dec_dms"> + <div class="row mt-2 px-3"> + <div class="col px-0 pr-xl-1"> + <div class="input-group"> + <input type="number" class="form-control" formControlName="d" autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">°</span> + </div> + </div> </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 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="number" class="form-control" formControlName="m" 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="number" class="form-control" formControlName="s" autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">''</span> + </div> + </div> </div> </div> </div> -</div> +</form> -<div *ngIf="decDegree.invalid" class="row px-3 text-danger"> - <div *ngIf="decDegree.errors.nan"> - {{ decDegree.errors.nan.value }} +<div *ngIf="form.controls.dec.invalid" class="row px-3 text-danger"> + <div *ngIf="form.controls.dec.errors.nan"> + {{ form.controls.dec.errors.nan.value }} </div> - <div *ngIf="decDegree.errors.range" [hidden]="decDegree.errors.nan"> - {{ decDegree.errors.range.value }} + <div *ngIf="form.controls.dec.errors.range" [hidden]="form.controls.dec.errors.nan"> + {{ form.controls.dec.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 *ngIf="getDecDmsForm().controls.d.invalid" class="row px-3 text-danger"> + <div *ngIf="getDecDmsForm().controls.d.errors.nan"> + {{ getDecDmsForm().controls.d.errors.nan.value }} </div> - <div *ngIf="decH.errors.range" [hidden]="decH.errors.nan"> - {{ decH.errors.range.value }} + <div *ngIf="getDecDmsForm().controls.d.errors.range" [hidden]="getDecDmsForm().controls.d.errors.nan"> + {{ getDecDmsForm().controls.d.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 *ngIf="getDecDmsForm().controls.m.invalid" class="row px-3 text-danger"> + <div *ngIf="getDecDmsForm().controls.m.errors.nan"> + {{ getDecDmsForm().controls.m.errors.nan.value }} </div> - <div *ngIf="decM.errors.range" [hidden]="decM.errors.nan"> - {{ decM.errors.range.value }} + <div *ngIf="getDecDmsForm().controls.m.errors.range" [hidden]="getDecDmsForm().controls.m.errors.nan"> + {{ getDecDmsForm().controls.m.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 *ngIf="getDecDmsForm().controls.s.invalid" class="row px-3 text-danger"> + <div *ngIf="getDecDmsForm().controls.s.errors.nan"> + {{ getDecDmsForm().controls.s.errors.nan.value }} </div> - <div *ngIf="decS.errors.range" [hidden]="decS.errors.nan"> - {{ decS.errors.range.value }} + <div *ngIf="getDecDmsForm().controls.s.errors.range" [hidden]="getDecDmsForm().controls.s.errors.nan"> + {{ getDecDmsForm().controls.s.errors.range.value }} </div> </div> diff --git a/client/src/app/instance/shared-search/components/cone-search/dec.component.ts b/client/src/app/instance/shared-search/components/cone-search/dec.component.ts index 3566031548cf7ad568b692487382f20033bccb0d..193bc66543fb5e9e5110c7d582591d5305160e29 100644 --- a/client/src/app/instance/shared-search/components/cone-search/dec.component.ts +++ b/client/src/app/instance/shared-search/components/cone-search/dec.component.ts @@ -7,11 +7,13 @@ * file that was distributed with this source code. */ -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; -import { FormControl, Validators } from '@angular/forms'; +import { Component, Input, ChangeDetectionStrategy, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; +import { FormGroup } from '@angular/forms'; -import { nanValidator, rangeValidator } from '../../validators'; -import { ConeSearch, Resolver } from 'src/app/instance/store/models'; +import { Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +import { Resolver } from 'src/app/instance/store/models'; @Component({ selector: 'app-dec', @@ -23,199 +25,71 @@ import { ConeSearch, Resolver } from 'src/app/instance/store/models'; * @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(); +export class DecComponent implements OnInit, OnDestroy, OnChanges { + @Input() form: FormGroup; + @Input() unit: string; + @Input() resolver: Resolver; + + public decControlSubscription: Subscription + public decDmsSubscription: Subscription; + + ngOnInit() { + this.form.controls.dec_dms.disable(); + this.decControlSubscription = this.form.controls.dec.valueChanges.pipe(debounceTime(250)) + .subscribe(deg => this.deg2DMS(deg)); } - /** - * 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); + + ngOnChanges(changes: SimpleChanges): void { + if (changes.unit && !changes.unit.firstChange) { + if (changes.unit.currentValue === 'degree') { + this.form.controls.dec_dms.disable(); + this.form.controls.dec.enable(); + this.decControlSubscription = this.form.controls.dec.valueChanges.pipe(debounceTime(250)) + .subscribe(deg => this.deg2DMS(deg)); + if (this.decDmsSubscription) this.decDmsSubscription.unsubscribe(); + } + if (changes.unit.currentValue === 'hms') { + this.form.controls.dec_dms.enable(); + this.form.controls.dec.disable(); + this.decDmsSubscription = this.form.controls.dec_dms.valueChanges.pipe(debounceTime(250)) + .subscribe(value => this.DMS2Deg(value)); + if (this.decControlSubscription) this.decControlSubscription.unsubscribe(); } - } 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); + + if (changes.resolver && changes.resolver.currentValue) { + this.form.controls.dec.setValue(changes.resolver.currentValue.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(); - } + getDecDmsForm() { + const decDmsForm = this.form.controls.dec_dms as FormGroup; + return decDmsForm; } - /** - * 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; + deg2DMS(deg: number): void { + const hh = Math.trunc(deg); + let tmp = (Math.abs(deg - 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); + const decDmsForm = this.getDecDmsForm(); + decDmsForm.controls.d.setValue(hh); + decDmsForm.controls.m.setValue(mm); + decDmsForm.controls.s.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) { + DMS2Deg(dms: {d: number, m: number, s: number }): void { + const tmp = ((dms.s / 60) + dms.m) / 60; + let deg = tmp + Math.abs(dms.d); + if (dms.d < 0) { deg = -deg; } - this.decDegree.setValue(+deg.toFixed(8)); + this.form.controls.dec.setValue(deg); } - /** - * 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(); - } + ngOnDestroy() { + if (this.decControlSubscription) this.decControlSubscription.unsubscribe(); + if (this.decDmsSubscription) this.decDmsSubscription.unsubscribe(); } } diff --git a/client/src/app/instance/shared-search/components/cone-search/ra.component.html b/client/src/app/instance/shared-search/components/cone-search/ra.component.html index e1ff92f6fa13d6a68cb3cbf742663ba56fe49b94..bb180bf81ba5dc29c24c2197b74691d51fbd7002 100644 --- a/client/src/app/instance/shared-search/components/cone-search/ra.component.html +++ b/client/src/app/instance/shared-search/components/cone-search/ra.component.html @@ -1,92 +1,75 @@ -<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"> +<form [formGroup]="form" novalidate> + <div class="row px-3"> + <label>RA</label> <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"> + <input type="number" class="form-control" formControlName="ra" autocomplete="off"> <div class="input-group-append"> - <span class="input-group-text">H</span> + <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]="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 formGroupName="ra_hms"> + <div class="row mt-2 px-3"> + <div class="col px-0 pr-xl-1"> + <div class="input-group"> + <input type="number" class="form-control" formControlName="h" autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">H</span> + </div> + </div> </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 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="number" class="form-control" formControlName="m" 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="number" class="form-control" formControlName="s" autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">''</span> + </div> + </div> </div> </div> </div> -</div> +</form> -<div *ngIf="raDegree.invalid" class="row px-3 text-danger"> - <div *ngIf="raDegree.errors.nan"> - {{ raDegree.errors.nan.value }} +<div *ngIf="form.controls.ra.invalid" class="row px-3 text-danger"> + <div *ngIf="form.controls.ra.errors.nan"> + {{ form.controls.ra.errors.nan.value }} </div> - <div *ngIf="raDegree.errors.range" [hidden]="raDegree.errors.nan"> - {{ raDegree.errors.range.value }} + <div *ngIf="form.controls.ra.errors.range" [hidden]="form.controls.ra.errors.nan"> + {{ form.controls.ra.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 *ngIf="getRaHmsForm().controls.h.invalid" class="row px-3 text-danger"> + <div *ngIf="getRaHmsForm().controls.h.errors.nan"> + {{ getRaHmsForm().controls.h.errors.nan.value }} </div> - <div *ngIf="raH.errors.range" [hidden]="raH.errors.nan"> - {{ raH.errors.range.value }} + <div *ngIf="getRaHmsForm().controls.h.errors.range" [hidden]="getRaHmsForm().controls.h.errors.nan"> + {{ getRaHmsForm().controls.h.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 *ngIf="getRaHmsForm().controls.m.invalid" class="row px-3 text-danger"> + <div *ngIf="getRaHmsForm().controls.m.errors.nan"> + {{ getRaHmsForm().controls.m.errors.nan.value }} </div> - <div *ngIf="raM.errors.range" [hidden]="raM.errors.nan"> - {{ raM.errors.range.value }} + <div *ngIf="getRaHmsForm().controls.m.errors.range" [hidden]="getRaHmsForm().controls.m.errors.nan"> + {{ getRaHmsForm().controls.m.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 *ngIf="getRaHmsForm().controls.s.invalid" class="row px-3 text-danger"> + <div *ngIf="getRaHmsForm().controls.s.errors.nan"> + {{ getRaHmsForm().controls.s.errors.nan.value }} </div> - <div *ngIf="raS.errors.range" [hidden]="raS.errors.nan"> - {{ raS.errors.range.value }} + <div *ngIf="getRaHmsForm().controls.s.errors.range" [hidden]="getRaHmsForm().controls.s.errors.nan"> + {{ getRaHmsForm().controls.s.errors.range.value }} </div> </div> diff --git a/client/src/app/instance/shared-search/components/cone-search/ra.component.ts b/client/src/app/instance/shared-search/components/cone-search/ra.component.ts index 30050f202540cf03c3d6659588e2c2c1595b5e90..aab36bf7ec811d5984d1d977b0b78847db203ab9 100644 --- a/client/src/app/instance/shared-search/components/cone-search/ra.component.ts +++ b/client/src/app/instance/shared-search/components/cone-search/ra.component.ts @@ -7,11 +7,13 @@ * file that was distributed with this source code. */ -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; -import { FormControl, Validators } from '@angular/forms'; +import { Component, Input, ChangeDetectionStrategy, OnDestroy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { FormGroup } from '@angular/forms'; -import { nanValidator, rangeValidator } from '../../validators'; -import { ConeSearch, Resolver } from 'src/app/instance/store/models'; +import { Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +import { Resolver } from 'src/app/instance/store/models'; @Component({ selector: 'app-ra', @@ -23,196 +25,68 @@ import { ConeSearch, Resolver } from 'src/app/instance/store/models'; * @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(); +export class RaComponent implements OnInit, OnDestroy, OnChanges { + @Input() form: FormGroup; + @Input() unit: string; + @Input() resolver: Resolver; + + public raControlSubscription: Subscription; + public raHmsFormSubscription: Subscription; + + ngOnInit() { + this.form.controls.ra_hms.disable(); + this.raControlSubscription = this.form.controls.ra.valueChanges.pipe(debounceTime(250)) + .subscribe(deg => this.deg2HMS(deg)); } - /** - * 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); + + ngOnChanges(changes: SimpleChanges): void { + if (changes.unit && !changes.unit.firstChange) { + if (changes.unit.currentValue === 'degree') { + this.form.controls.ra_hms.disable(); + this.form.controls.ra.enable(); + this.raControlSubscription = this.form.controls.ra.valueChanges.pipe(debounceTime(250)) + .subscribe(deg => this.deg2HMS(deg)); + if (this.raHmsFormSubscription) this.raHmsFormSubscription.unsubscribe(); + } + if (changes.unit.currentValue === 'hms') { + this.form.controls.ra_hms.enable(); + this.form.controls.ra.disable(); + this.raHmsFormSubscription = this.form.controls.ra_hms.valueChanges.pipe(debounceTime(250)) + .subscribe(value => this.HMS2Deg(value)); + if (this.raControlSubscription) this.raControlSubscription.unsubscribe(); } - } 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); + + if (changes.resolver && changes.resolver.currentValue) { + this.form.controls.ra.setValue(changes.resolver.currentValue.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(); - } + getRaHmsForm() { + const raHmsForm = this.form.controls.ra_hms as FormGroup; + return raHmsForm; } - /** - * 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; + deg2HMS(deg: number): void { + let tmp = deg / 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); + const raHmsForm = this.getRaHmsForm(); + raHmsForm.controls.h.setValue(hh); + raHmsForm.controls.m.setValue(mm); + raHmsForm.controls.s.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); + HMS2Deg(hms: {h: number, m: number, s: number }): void { + const deg = +(((((hms.s / 60) + hms.m) / 60) + hms.h) * 15).toFixed(8); + this.form.controls.ra.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(); - } + ngOnDestroy() { + if (this.raControlSubscription) this.raControlSubscription.unsubscribe(); + if (this.raHmsFormSubscription) this.raHmsFormSubscription.unsubscribe(); } } diff --git a/client/src/app/instance/shared-search/components/cone-search/radius.component.html b/client/src/app/instance/shared-search/components/cone-search/radius.component.html index 406cdfc30e2dcaeb8151aad6ac776a9d23cc3c60..87cdf7a216c9304bef8e199db31a26860a70adda 100644 --- a/client/src/app/instance/shared-search/components/cone-search/radius.component.html +++ b/client/src/app/instance/shared-search/components/cone-search/radius.component.html @@ -1,27 +1,22 @@ -<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> +<form [formGroup]="form" novalidate> + <div class="row"> + <div class="col form-group mb-0"> + <label>Radius</label> + <input type="range" min="0" max="150" class="form-control-range mt-2" [value]="form.value.radius" formControlName="radius" 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 type="number" class="form-control" [value]="form.value.radius" formControlName="radius" autocomplete="off"> + <div class="input-group-append"> + <span class="input-group-text">arcsecond</span> + </div> </div> </div> - </div> - <div *ngIf="radiusField.invalid" class="col-12 text-danger"> - <div *ngIf="radiusField.errors.range"> - {{ radiusField.errors.range.value }} + <div *ngIf="form.controls.radius.invalid" class="col-12 text-danger"> + <div *ngIf="form.controls.radius.errors.range"> + {{ form.controls.radius.errors.range.value }} + </div> </div> </div> -</div> +</form> \ No newline at end of file diff --git a/client/src/app/instance/shared-search/components/cone-search/radius.component.ts b/client/src/app/instance/shared-search/components/cone-search/radius.component.ts index 5add3abf531de6ea605e5547a42bc19832ec16f1..fde6ceb216655a19b21d8966877a01fd29a0d0b1 100644 --- a/client/src/app/instance/shared-search/components/cone-search/radius.component.ts +++ b/client/src/app/instance/shared-search/components/cone-search/radius.component.ts @@ -7,11 +7,8 @@ * 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'; +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { FormGroup } from '@angular/forms'; @Component({ selector: 'app-radius', @@ -24,55 +21,5 @@ import { ConeSearch } from 'src/app/instance/store/models'; * @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); - } + @Input() form: FormGroup; } diff --git a/client/src/app/instance/shared-search/components/cone-search/resolver.component.html b/client/src/app/instance/shared-search/components/cone-search/resolver.component.html index 86714689d9db49082707206b9002ed94cdf661e8..abf2a55cc7b6b98bc988778340658be4d0588d4b 100644 --- a/client/src/app/instance/shared-search/components/cone-search/resolver.component.html +++ b/client/src/app/instance/shared-search/components/cone-search/resolver.component.html @@ -1,15 +1,17 @@ -<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"> +<form [formGroup]="form" (ngSubmit)="submit()" novalidate> + <div class="row"> + <div class="col pr-0"> + <label for="resolver">Resolve RA and DEC with Sesame Name Resolver</label> + <input type="text" class="form-control" name="name" formControlName="name" autocomplete="off"> + </div> + <div class="col-auto pt-5 pt-lg-4"> + <button *ngIf="!resolverIsLoading" [disabled]="!form.valid || form.pristine" type="submit" class="btn btn-outline-secondary mt-2"> + <span class="fas fa-search"></span> + </button> + <button *ngIf="resolverIsLoading" 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> - <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> +</form> diff --git a/client/src/app/instance/shared-search/components/cone-search/resolver.component.ts b/client/src/app/instance/shared-search/components/cone-search/resolver.component.ts index 1c01755596db0bfad2c8509903f7f76d68e1f957..18c094444753d9795a7088eb0aca5e3a1a8fae1d 100644 --- a/client/src/app/instance/shared-search/components/cone-search/resolver.component.ts +++ b/client/src/app/instance/shared-search/components/cone-search/resolver.component.ts @@ -7,10 +7,10 @@ * file that was distributed with this source code. */ -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; -import { FormControl } from '@angular/forms'; +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { Resolver } from 'src/app/instance/store/models'; +import { ConeSearch, Resolver } from 'src/app/instance/store/models'; @Component({ selector: 'app-resolver', @@ -21,25 +21,34 @@ import { Resolver } from 'src/app/instance/store/models'; * @class * @classdesc Resolver component. */ -export class ResolverComponent { - @Input() - set disabled(disabled: boolean) { - if (disabled) { - this.field.disable(); - } else { - this.field.enable(); +export class ResolverComponent implements OnInit, OnChanges { + @Input() coneSearch: ConeSearch; + @Input() resolver: Resolver; + @Input() resolverIsLoading: boolean; + @Input() resolverIsLoaded: boolean; + @Output() retrieveCoordinates: EventEmitter<string> = new EventEmitter(); + + public form = new FormGroup({ + name: new FormControl('', [Validators.required]) + }); + + ngOnInit() { + if (this.coneSearch) { + this.form.disable(); } } - @Input() resolverWip: boolean; - @Input() - set resolver(resolver: Resolver) { - if (resolver) { - this.field.setValue(resolver.name); - } else { - this.field.reset(); + + ngOnChanges(changes: SimpleChanges): void { + if (changes.coneSearch && !changes.coneSearch.currentValue) { + this.form.enable(); + } + + if (changes.coneSearch && changes.coneSearch.currentValue) { + this.form.disable(); } } - @Output() resolveName: EventEmitter<string> = new EventEmitter(); - field = new FormControl(''); + submit() { + this.retrieveCoordinates.emit(this.form.controls.name.value); + } } diff --git a/client/src/app/instance/shared-search/components/index.ts b/client/src/app/instance/shared-search/components/index.ts index f3ee95d0784280eae6250d149041229fd9e5127f..1bd2ebbdd5b479facf234dd6ba38ab4ef88f589e 100644 --- a/client/src/app/instance/shared-search/components/index.ts +++ b/client/src/app/instance/shared-search/components/index.ts @@ -1,7 +1,7 @@ -// import { coneSearchComponents } from './cone-search'; +import { coneSearchComponents } from './cone-search'; import { datatableComponents } from './datatable'; export const sharedComponents = [ - // coneSearchComponents, + coneSearchComponents, datatableComponents ]; diff --git a/client/src/app/instance/store/actions/cone-search.actions.ts b/client/src/app/instance/store/actions/cone-search.actions.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..779b424153952b2ca4f4e0dba7157b5a97c55c0e 100644 --- a/client/src/app/instance/store/actions/cone-search.actions.ts +++ b/client/src/app/instance/store/actions/cone-search.actions.ts @@ -0,0 +1,18 @@ +/** + * 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 { createAction, props } from '@ngrx/store'; + +import { ConeSearch, Resolver } from '../models'; + +export const addConeSearch = createAction('[ConeSearch] Add Cone Search', props<{ coneSearch: ConeSearch }>()); +export const deleteConeSearch = createAction('[ConeSearch] Delete Cone Search'); +export const retrieveCoordinates = createAction('[ConeSearch] Retrieve Coordinates', props<{ name: string }>()); +export const retrieveCoordinatesSuccess = createAction('[ConeSearch] Retrieve Coordinates Success', props<{ resolver: Resolver }>()); +export const retrieveCoordinatesFail = createAction('[ConeSearch] Retrieve Coordinates Fail'); diff --git a/client/src/app/instance/store/effects/cone-search.effects.ts b/client/src/app/instance/store/effects/cone-search.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..84d2ab8fe05cc36bdba788ba155cd47ae0c7270d --- /dev/null +++ b/client/src/app/instance/store/effects/cone-search.effects.ts @@ -0,0 +1,58 @@ +/** + * 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 { Injectable } from '@angular/core'; + +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { of } from 'rxjs'; +import { map, tap, mergeMap, catchError } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; + +import * as coneSearchActions from '../actions/cone-search.actions'; +import { ConeSearchService } from '../services/cone-search.service'; + +@Injectable() +export class ConeSearchEffects { + retrieveCoordinates$ = createEffect(() => + this.actions$.pipe( + ofType(coneSearchActions.retrieveCoordinates), + mergeMap((action) => this.coneSearchService.retrieveCoordinates(action.name) + .pipe( + map(response => { + const parser = new DOMParser(); + const xml = parser.parseFromString(response,'text/xml'); + if (xml.getElementsByTagName('Resolver').length === 0) { + const name = xml.getElementsByTagName('name')[0].childNodes[0].nodeValue; + return coneSearchActions.retrieveCoordinatesFail(); + } + const name = xml.getElementsByTagName('name')[0].childNodes[0].nodeValue; + const ra = +xml.getElementsByTagName('jradeg')[0].childNodes[0].nodeValue; + const dec = +xml.getElementsByTagName('jdedeg')[0].childNodes[0].nodeValue; + const resolver = { name, ra, dec }; + return coneSearchActions.retrieveCoordinatesSuccess({ resolver }); + }), + catchError(() => of(coneSearchActions.retrieveCoordinatesFail())) + ) + ) + ) + ); + + retrieveCoordinatesFail$ = createEffect(() => + this.actions$.pipe( + ofType(coneSearchActions.retrieveCoordinatesFail), + tap(() => this.toastr.error('Failure to retrieve coordinates', 'The coordinates could not be retrieved')) + ), { dispatch: false} + ); + + constructor( + private actions$: Actions, + private coneSearchService: ConeSearchService, + private toastr: ToastrService + ) {} +} diff --git a/client/src/app/instance/store/effects/index.ts b/client/src/app/instance/store/effects/index.ts index 296d89b24794a19ce38357ac7eab009ed9234257..3d2ecd77ad3f9e6f15ba5a0e1938aac5679cb49d 100644 --- a/client/src/app/instance/store/effects/index.ts +++ b/client/src/app/instance/store/effects/index.ts @@ -1,9 +1,11 @@ import { MetamodelEffects } from './metamodel.effects'; -import { SampEffects } from "./samp.effects"; -import { SearchEffects } from "./search.effects"; +import { SampEffects } from './samp.effects'; +import { SearchEffects } from './search.effects'; +import { ConeSearchEffects } from './cone-search.effects'; export const instanceEffects = [ MetamodelEffects, SampEffects, - SearchEffects + SearchEffects, + ConeSearchEffects ]; diff --git a/client/src/app/instance/store/effects/search.effects.ts b/client/src/app/instance/store/effects/search.effects.ts index f3ffcd4d55f29538f39c453a2b763b179796226e..b92513c667482841cf169ebeef11fa595520adc9 100644 --- a/client/src/app/instance/store/effects/search.effects.ts +++ b/client/src/app/instance/store/effects/search.effects.ts @@ -15,7 +15,7 @@ import { of } from 'rxjs'; import { map, tap, mergeMap, catchError } from 'rxjs/operators'; import { ToastrService } from 'ngx-toastr'; -import { criterionToString, stringToCriterion } from '../models'; +import { ConeSearch, criterionToString, stringToCriterion } from '../models'; import { SearchService } from '../services/search.service'; import * as searchActions from '../actions/search.actions'; import * as attributeActions from 'src/app/metamodel/actions/attribute.actions'; @@ -25,6 +25,8 @@ import * as outputFamilyActions from 'src/app/metamodel/actions/output-family.ac import * as outputCategoryActions from 'src/app/metamodel/actions/output-category.actions'; import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; import * as searchSelector from '../selectors/search.selector'; +import * as coneSearchActions from '../actions/cone-search.actions'; +import * as coneSearchSelector from '../selectors/cone-search.selector'; @Injectable() export class SearchEffects { @@ -88,11 +90,19 @@ export class SearchEffects { this.actions$.pipe( ofType(searchActions.loadDefaultFormParameters), concatLatestFrom(() => [ + this.store.select(searchSelector.selectPristine), + this.store.select(searchSelector.selectCurrentDataset), this.store.select(attributeSelector.selectAllAttributes), this.store.select(searchSelector.selectCriteriaListByRoute), + this.store.select(coneSearchSelector.selectConeSearchByRoute), this.store.select(searchSelector.selectOutputListByRoute) ]), - mergeMap(([action, attributeList, criteriaList, outputList]) => { + mergeMap(([action, pristine, currentDataset, attributeList, criteriaList, coneSearch, outputList]) => { + if (!pristine || !currentDataset) { + // Default form parameters already loaded or no dataset selected + return of({ type: '[No Action] Load Default Form Parameters' }); + } + // Update criteria list let defaultCriteriaList = []; if (criteriaList) { @@ -109,6 +119,17 @@ export class SearchEffects { .map(attribute => stringToCriterion(attribute)); } + // Update cone search + let defaultConeSearch: ConeSearch = null; + if (coneSearch) { + const params = coneSearch.split(':'); + defaultConeSearch = { + ra: +params[0], + dec: +params[1], + radius: +params[2] + }; + } + // Update output list let defaultOutputList = []; if (outputList) { @@ -124,6 +145,7 @@ export class SearchEffects { // Returns actions and mark the form as dirty return [ searchActions.updateCriteriaList({ criteriaList: defaultCriteriaList }), + coneSearchActions.addConeSearch({ coneSearch: defaultConeSearch }), searchActions.updateOutputList({ outputList: defaultOutputList }), searchActions.markAsDirty() ]; @@ -136,13 +158,17 @@ export class SearchEffects { ofType(searchActions.retrieveDataLength), concatLatestFrom(() => [ this.store.select(datasetSelector.selectDatasetNameByRoute), - this.store.select(searchSelector.selectCriteriaList) + this.store.select(searchSelector.selectCriteriaList), + this.store.select(coneSearchSelector.selectConeSearch) ]), - mergeMap(([action, datasetName, criteriaList]) => { + mergeMap(([action, datasetName, criteriaList, coneSearch]) => { let query = datasetName + '?a=count'; if (criteriaList.length > 0) { query += '&c=' + criteriaList.map(criterion => criterionToString(criterion)).join(';'); } + if (coneSearch) { + query += '&cs=' + coneSearch.ra + ':' + coneSearch.dec + ':' + coneSearch.radius; + } return this.searchService.retrieveDataLength(query) .pipe( @@ -166,13 +192,18 @@ export class SearchEffects { concatLatestFrom(() => [ this.store.select(datasetSelector.selectDatasetNameByRoute), this.store.select(searchSelector.selectCriteriaList), + this.store.select(coneSearchSelector.selectConeSearch), this.store.select(searchSelector.selectOutputList) ]), - mergeMap(([action, datasetName, criteriaList, outputList]) => { + mergeMap(([action, datasetName, criteriaList, coneSearch, outputList]) => { let query = datasetName + '?a=' + outputList.join(';'); if (criteriaList.length > 0) { query += '&c=' + criteriaList.map(criterion => criterionToString(criterion)).join(';'); } + if (coneSearch) { + query += '&cs=' + coneSearch.ra + ':' + coneSearch.dec + ':' + coneSearch.radius; + } + query += '&p=' + action.pagination.nbItems + ':' + action.pagination.page; query += '&o=' + action.pagination.sortedCol + ':' + action.pagination.order; diff --git a/client/src/app/instance/store/reducers/cone-search.reducer.ts b/client/src/app/instance/store/reducers/cone-search.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a4ee35ce53cc7f5271515cf2811850994e04353 --- /dev/null +++ b/client/src/app/instance/store/reducers/cone-search.reducer.ts @@ -0,0 +1,59 @@ +/** + * 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 { createReducer, on } from '@ngrx/store'; + +import * as coneSearchActions from '../actions/cone-search.actions'; +import { ConeSearch, Resolver } from '../models'; + +export interface State { + coneSearch: ConeSearch; + resolver: Resolver; + resolverIsLoading: boolean; + resolverIsLoaded: boolean; +} + +export const initialState: State = { + coneSearch: null, + resolver: null, + resolverIsLoading: false, + resolverIsLoaded: false +} + +export const coneSearchReducer = createReducer( + initialState, + on(coneSearchActions.addConeSearch, (state, { coneSearch }) => ({ + ...state, + coneSearch + })), + on(coneSearchActions.deleteConeSearch, state => ({ + ...state, + coneSearch: null + })), + on(coneSearchActions.retrieveCoordinates, state => ({ + ...state, + resolverIsLoading: true, + resolverIsLoaded: false + })), + on(coneSearchActions.retrieveCoordinatesSuccess, (state, { resolver }) => ({ + ...state, + resolver, + resolverIsLoading: false, + resolverIsLoaded: true + })), + on(coneSearchActions.retrieveCoordinatesFail, state => ({ + ...state, + resolverIsLoading: false + })) +); + +export const selectConeSearch = (state: State) => state.coneSearch; +export const selectResolver = (state: State) => state.resolver; +export const selectResolverIsLoading = (state: State) => state.resolverIsLoading; +export const selectResolverIsLoaded = (state: State) => state.resolverIsLoaded; diff --git a/client/src/app/instance/store/selectors/cone-search.selector.ts b/client/src/app/instance/store/selectors/cone-search.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..695bba185eacc01db90b4dfface55f691ee3d3c7 --- /dev/null +++ b/client/src/app/instance/store/selectors/cone-search.selector.ts @@ -0,0 +1,43 @@ +/** + * 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 { createSelector } from '@ngrx/store'; + +import * as reducer from '../../instance.reducer'; +import * as fromConeSearch from '../reducers/cone-search.reducer'; + +export const selectConeSearchState = createSelector( + reducer.getInstanceState, + (state: reducer.State) => state.coneSearch +); + +export const selectConeSearch = createSelector( + selectConeSearchState, + fromConeSearch.selectConeSearch +); + +export const selectResolver = createSelector( + selectConeSearchState, + fromConeSearch.selectResolver +); + +export const selectResolverIsLoading = createSelector( + selectConeSearchState, + fromConeSearch.selectResolverIsLoading +); + +export const selectResolverIsLoaded = createSelector( + selectConeSearchState, + fromConeSearch.selectResolverIsLoaded +); + +export const selectConeSearchByRoute = createSelector( + reducer.selectRouterState, + router => router.state.queryParams.cs as string +); diff --git a/client/src/app/instance/store/selectors/search.selector.ts b/client/src/app/instance/store/selectors/search.selector.ts index ee70032ca896242b28fe17f59bd61982c7159f74..094c36733e1f0aed698df8503b56aaf91977b642 100644 --- a/client/src/app/instance/store/selectors/search.selector.ts +++ b/client/src/app/instance/store/selectors/search.selector.ts @@ -9,9 +9,10 @@ import { createSelector } from '@ngrx/store'; -import { Criterion, SearchQueryParams, criterionToString } from '../models'; +import { Criterion, SearchQueryParams, criterionToString, ConeSearch } from '../models'; import * as reducer from '../../instance.reducer'; import * as fromSearch from '../reducers/search.reducer'; +import * as coneSearchSelector from './cone-search.selector'; export const selectInstanceState = createSelector( reducer.getInstanceState, @@ -100,12 +101,14 @@ export const selectSelectedData = createSelector( export const selectQueryParams = createSelector( selectCriteriaList, + coneSearchSelector.selectConeSearch, selectOutputList, selectCriteriaStepChecked, selectOutputStepChecked, selectResultStepChecked, ( criteriaList: Criterion[], + coneSearch: ConeSearch, outputList: number[], criteriaStepChecked: boolean, outputStepChecked: boolean, @@ -118,6 +121,12 @@ export const selectQueryParams = createSelector( s: step, a: outputList.join(';') }; + if (coneSearch) { + queryParams = { + ...queryParams, + cs: coneSearch.ra + ':' + coneSearch.dec + ':' + coneSearch.radius + }; + } if (criteriaList.length > 0) { queryParams = { ...queryParams, diff --git a/client/src/app/instance/store/services/cone-search.service.ts b/client/src/app/instance/store/services/cone-search.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..37c3420e5f1af5b426de2cf6bc6c8885e152f8bb --- /dev/null +++ b/client/src/app/instance/store/services/cone-search.service.ts @@ -0,0 +1,33 @@ +/** + * 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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable() +/** + * @class + * @classdesc Cone search service. + */ +export class ConeSearchService { + constructor(private http: HttpClient) { } + + /** + * Retrieves coordinates of an object name. + * + * @param {string} name - The name of the object. + * + * @return Observable<any> + */ + retrieveCoordinates(name: string): Observable<any> { + const url = 'https://cdsweb.u-strasbg.fr/cgi-bin/nph-sesame/-ox/NSV?' + name; + return this.http.get(url, { responseType: 'text' }); + } +} diff --git a/client/src/app/instance/store/services/index.ts b/client/src/app/instance/store/services/index.ts index 136f9cb320ff0395ea365076e1f7753d31f9f680..4707c60a165f5e0b28a886c148854579537703d5 100644 --- a/client/src/app/instance/store/services/index.ts +++ b/client/src/app/instance/store/services/index.ts @@ -1,7 +1,9 @@ import { SearchService } from './search.service'; import { SampService } from './samp.service'; +import { ConeSearchService } from './cone-search.service'; export const instanceServices = [ SearchService, - SampService + SampService, + ConeSearchService ]; diff --git a/server/src/Search/Query/ConeSearch.php b/server/src/Search/Query/ConeSearch.php index f229f130e1a0d35ecb4a4179a9fc2160507bec29..4d4cee16257e82b9b412a6507e67c444f2aaa1a4 100644 --- a/server/src/Search/Query/ConeSearch.php +++ b/server/src/Search/Query/ConeSearch.php @@ -45,12 +45,12 @@ class ConeSearch extends AbstractQueryPart $radius = floatval($radius); $coneSearchConfig = $dataset->getConfig()['cone_search']; - if ($coneSearchConfig['enabled'] !== true) { + if ($coneSearchConfig['cone_search_enabled'] !== true) { throw SearchQueryException::coneSearchUnavailable(); } - $attributeRa = $this->getAttribute($dataset, $coneSearchConfig['column_ra']); - $attributeDec = $this->getAttribute($dataset, $coneSearchConfig['column_dec']); + $attributeRa = $this->getAttribute($dataset, $coneSearchConfig['cone_search_column_ra']); + $attributeDec = $this->getAttribute($dataset, $coneSearchConfig['cone_search_column_dec']); $columnRa = $dataset->getTableRef() . '.' . $attributeRa->getName(); $columnDec = $dataset->getTableRef() . '.' . $attributeDec->getName();