From ef39985c5f54e71f256f84374090d6de8a643768 Mon Sep 17 00:00:00 2001 From: Arjan Mossel Date: Thu, 5 Sep 2024 16:59:04 +0200 Subject: [PATCH 1/4] Use MapDataResults class to manage fetch, queryModel subscription Following WordcloudComponent and FrequentWordsResults --- frontend/src/app/models/map-data.ts | 69 +++++++++++++++++++ frontend/src/app/services/api.service.ts | 4 +- .../src/app/services/visualization.service.ts | 4 +- .../app/visualization/map/map.component.ts | 54 +++++++-------- 4 files changed, 98 insertions(+), 33 deletions(-) create mode 100644 frontend/src/app/models/map-data.ts diff --git a/frontend/src/app/models/map-data.ts b/frontend/src/app/models/map-data.ts new file mode 100644 index 000000000..332c529ab --- /dev/null +++ b/frontend/src/app/models/map-data.ts @@ -0,0 +1,69 @@ +import { Observable, forkJoin, of } from 'rxjs'; +import { Results } from './results'; +import { GeoDocument, GeoLocation } from './search-results'; +import { Params } from '@angular/router'; +import { VisualizationService } from '@services'; +import { Store } from '../store/types'; +import { QueryModel } from './query'; +import { CorpusField } from './corpus'; +import { findByName } from '@utils/utils'; + + +interface MapDataParameters { + field: CorpusField; +} + + +interface MapData { + mapCenter: GeoLocation; + geoDocuments: GeoDocument[]; +} + + +export class MapDataResults extends Results { + private mapCenter: GeoLocation | null = null; + + constructor( + store: Store, + query: QueryModel, + private visualizationService: VisualizationService + ) { + super(store, query, ['visualizedField']); + this.connectToStore(); + this.getResults(); + } + + fetch(): Observable { + const field = this.state$.value.field; + if (!field) { + return of({ geoDocuments: [], mapCenter: null }); + } + + const getGeoCentroid$ = this.mapCenter + ? of(this.mapCenter) + : this.visualizationService.getGeoCentroid(field.name, this.query.corpus); + + return forkJoin({ + geoDocuments: this.visualizationService.getGeoData( + field.name, + this.query, + this.query.corpus + ), + mapCenter: getGeoCentroid$ + }); + } + + protected stateToStore(state: MapDataParameters): Params { + return { visualizedField: state.field?.name || null }; + } + + protected storeToState(params: Params): MapDataParameters { + const fieldName = params['visualizedField']; + const field = findByName(this.query.corpus.fields, fieldName); + return { field }; + } + + protected storeOnComplete(): Params { + return {}; + } +} diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 28a2e33d0..b62a426fd 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -147,9 +147,9 @@ export class ApiService { return this.http.post(url, data); } - public geoData(data: WordcloudParameters): Promise { + public geoData(data: WordcloudParameters): Observable { const url = this.apiRoute(this.visApiURL, 'geo'); - return this.http.post(url, data).toPromise(); + return this.http.post(url, data); } public geoCentroid(data: {corpus: string, field: string}): Promise { diff --git a/frontend/src/app/services/visualization.service.ts b/frontend/src/app/services/visualization.service.ts index 51f4b042c..5ef5daeee 100644 --- a/frontend/src/app/services/visualization.service.ts +++ b/frontend/src/app/services/visualization.service.ts @@ -37,8 +37,8 @@ export class VisualizationService { }); } - public async getGeoData(fieldName: string, queryModel: QueryModel, corpus: Corpus): - Promise { + public getGeoData(fieldName: string, queryModel: QueryModel, corpus: Corpus): + Observable { const query = queryModel.toAPIQuery(); return this.apiService.geoData({ ...query, diff --git a/frontend/src/app/visualization/map/map.component.ts b/frontend/src/app/visualization/map/map.component.ts index 03e7b2e08..7d31efc46 100644 --- a/frontend/src/app/visualization/map/map.component.ts +++ b/frontend/src/app/visualization/map/map.component.ts @@ -4,7 +4,8 @@ import embed, { VisualizationSpec } from 'vega-embed'; import { Corpus, CorpusField, GeoDocument, GeoLocation, QueryModel } from '@models'; import { VisualizationService } from '@services'; -import { showLoading } from '@utils/utils'; +import { MapDataResults } from '@models/map-data'; +import { RouterStoreService } from 'app/store/router-store.service'; @Component({ @@ -22,12 +23,15 @@ export class MapComponent implements OnChanges { @Output() mapError = new EventEmitter(); - mapCenter: GeoLocation; + mapCenter: GeoLocation | null; results: GeoDocument[]; isLoading$ = new BehaviorSubject(false); + private mapDataResults: MapDataResults; + constructor( + private routerStoreService: RouterStoreService, private visualizationService: VisualizationService ) { } @@ -42,42 +46,34 @@ export class MapComponent implements OnChanges { ngOnChanges(changes: SimpleChanges) { if (this.readyToLoad) { - this.loadMapCenter(); - if ( changes.corpus || changes.visualizedField || changes.queryModel ) { - this.queryModel.update.subscribe(this.loadData.bind(this)); - this.loadData(); + if (changes.corpus || changes.visualizedField || changes.queryModel) { + this.mapDataResults?.complete(); + + this.mapDataResults = new MapDataResults( + this.routerStoreService, + this.queryModel, + this.visualizationService + ); + + this.mapDataResults.result$.subscribe(data => { + this.results = data.geoDocuments; + this.mapCenter = data.mapCenter; + this.renderChart(); + }); + + + this.mapDataResults.error$.subscribe(error => this.emitError(error)); } } } - loadMapCenter() { - this.visualizationService.getGeoCentroid(this.visualizedField.name, this.corpus) - .then(centroid => { - this.mapCenter = centroid; - }) - .catch(this.emitError.bind(this)); + ngOnDestroy(): void { + this.mapDataResults?.complete(); } - loadData() { - showLoading( - this.isLoading$, - this.visualizationService - .getGeoData( - this.visualizedField.name, - this.queryModel, - this.corpus, - ) - .then(geoData => { - this.results = geoData; - this.renderChart(); - }) - .catch(this.emitError.bind(this)) - ); - } - getVegaSpec(): VisualizationSpec { // Returns a Vega map specification // Uses pan/zoom signals from https://vega.github.io/vega/examples/zoomable-world-map/ From 42f939a52f933eb81748063b683bbe86a67bedba Mon Sep 17 00:00:00 2001 From: Arjan Mossel Date: Thu, 12 Sep 2024 10:14:37 +0200 Subject: [PATCH 2/4] Use share operator for mapCenter --- frontend/src/app/models/map-data.ts | 10 +++------- frontend/src/app/services/api.service.ts | 4 ++-- frontend/src/app/services/visualization.service.ts | 4 ++-- frontend/src/app/visualization/map/map.component.ts | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/models/map-data.ts b/frontend/src/app/models/map-data.ts index 332c529ab..de15b7434 100644 --- a/frontend/src/app/models/map-data.ts +++ b/frontend/src/app/models/map-data.ts @@ -1,4 +1,4 @@ -import { Observable, forkJoin, of } from 'rxjs'; +import { Observable, forkJoin, of, share } from 'rxjs'; import { Results } from './results'; import { GeoDocument, GeoLocation } from './search-results'; import { Params } from '@angular/router'; @@ -21,8 +21,6 @@ interface MapData { export class MapDataResults extends Results { - private mapCenter: GeoLocation | null = null; - constructor( store: Store, query: QueryModel, @@ -39,9 +37,7 @@ export class MapDataResults extends Results { return of({ geoDocuments: [], mapCenter: null }); } - const getGeoCentroid$ = this.mapCenter - ? of(this.mapCenter) - : this.visualizationService.getGeoCentroid(field.name, this.query.corpus); + const mapCenter$ = this.visualizationService.getGeoCentroid(field.name, this.query.corpus).pipe(share()); return forkJoin({ geoDocuments: this.visualizationService.getGeoData( @@ -49,7 +45,7 @@ export class MapDataResults extends Results { this.query, this.query.corpus ), - mapCenter: getGeoCentroid$ + mapCenter: mapCenter$ }); } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index b62a426fd..c3d47d0d1 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -152,9 +152,9 @@ export class ApiService { return this.http.post(url, data); } - public geoCentroid(data: {corpus: string, field: string}): Promise { + public geoCentroid(data: {corpus: string, field: string}): Observable { const url = this.apiRoute(this.visApiURL, 'geo_centroid'); - return this.http.post(url, data).toPromise(); + return this.http.post(url, data); } public ngramTasks(data: NGramRequestParameters): Promise { diff --git a/frontend/src/app/services/visualization.service.ts b/frontend/src/app/services/visualization.service.ts index 5ef5daeee..a4bad778c 100644 --- a/frontend/src/app/services/visualization.service.ts +++ b/frontend/src/app/services/visualization.service.ts @@ -47,8 +47,8 @@ export class VisualizationService { }); } - public async getGeoCentroid(fieldName: string, corpus: Corpus): - Promise { + public getGeoCentroid(fieldName: string, corpus: Corpus): + Observable { return this.apiService.geoCentroid({ corpus: corpus.name, field: fieldName, diff --git a/frontend/src/app/visualization/map/map.component.ts b/frontend/src/app/visualization/map/map.component.ts index 7d31efc46..f934afbd8 100644 --- a/frontend/src/app/visualization/map/map.component.ts +++ b/frontend/src/app/visualization/map/map.component.ts @@ -23,7 +23,7 @@ export class MapComponent implements OnChanges { @Output() mapError = new EventEmitter(); - mapCenter: GeoLocation | null; + mapCenter: GeoLocation; results: GeoDocument[]; isLoading$ = new BehaviorSubject(false); From af012a43ffd5156c28630ff25603f001bc7cb0b2 Mon Sep 17 00:00:00 2001 From: Arjan Mossel Date: Fri, 4 Oct 2024 12:00:28 +0200 Subject: [PATCH 3/4] Revert "Use share operator for mapCenter" This reverts commit 42f939a52f933eb81748063b683bbe86a67bedba. --- frontend/src/app/models/map-data.ts | 10 +++++++--- frontend/src/app/services/api.service.ts | 4 ++-- frontend/src/app/services/visualization.service.ts | 4 ++-- frontend/src/app/visualization/map/map.component.ts | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/models/map-data.ts b/frontend/src/app/models/map-data.ts index de15b7434..332c529ab 100644 --- a/frontend/src/app/models/map-data.ts +++ b/frontend/src/app/models/map-data.ts @@ -1,4 +1,4 @@ -import { Observable, forkJoin, of, share } from 'rxjs'; +import { Observable, forkJoin, of } from 'rxjs'; import { Results } from './results'; import { GeoDocument, GeoLocation } from './search-results'; import { Params } from '@angular/router'; @@ -21,6 +21,8 @@ interface MapData { export class MapDataResults extends Results { + private mapCenter: GeoLocation | null = null; + constructor( store: Store, query: QueryModel, @@ -37,7 +39,9 @@ export class MapDataResults extends Results { return of({ geoDocuments: [], mapCenter: null }); } - const mapCenter$ = this.visualizationService.getGeoCentroid(field.name, this.query.corpus).pipe(share()); + const getGeoCentroid$ = this.mapCenter + ? of(this.mapCenter) + : this.visualizationService.getGeoCentroid(field.name, this.query.corpus); return forkJoin({ geoDocuments: this.visualizationService.getGeoData( @@ -45,7 +49,7 @@ export class MapDataResults extends Results { this.query, this.query.corpus ), - mapCenter: mapCenter$ + mapCenter: getGeoCentroid$ }); } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index c3d47d0d1..b62a426fd 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -152,9 +152,9 @@ export class ApiService { return this.http.post(url, data); } - public geoCentroid(data: {corpus: string, field: string}): Observable { + public geoCentroid(data: {corpus: string, field: string}): Promise { const url = this.apiRoute(this.visApiURL, 'geo_centroid'); - return this.http.post(url, data); + return this.http.post(url, data).toPromise(); } public ngramTasks(data: NGramRequestParameters): Promise { diff --git a/frontend/src/app/services/visualization.service.ts b/frontend/src/app/services/visualization.service.ts index a4bad778c..5ef5daeee 100644 --- a/frontend/src/app/services/visualization.service.ts +++ b/frontend/src/app/services/visualization.service.ts @@ -47,8 +47,8 @@ export class VisualizationService { }); } - public getGeoCentroid(fieldName: string, corpus: Corpus): - Observable { + public async getGeoCentroid(fieldName: string, corpus: Corpus): + Promise { return this.apiService.geoCentroid({ corpus: corpus.name, field: fieldName, diff --git a/frontend/src/app/visualization/map/map.component.ts b/frontend/src/app/visualization/map/map.component.ts index f934afbd8..7d31efc46 100644 --- a/frontend/src/app/visualization/map/map.component.ts +++ b/frontend/src/app/visualization/map/map.component.ts @@ -23,7 +23,7 @@ export class MapComponent implements OnChanges { @Output() mapError = new EventEmitter(); - mapCenter: GeoLocation; + mapCenter: GeoLocation | null; results: GeoDocument[]; isLoading$ = new BehaviorSubject(false); From 68d961ee3e31abae19e88a79b20413e2f271450a Mon Sep 17 00:00:00 2001 From: Arjan Mossel Date: Fri, 4 Oct 2024 13:20:24 +0200 Subject: [PATCH 4/4] Use mapCenter observable in MapDataResults --- frontend/src/app/models/map-data.ts | 34 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/frontend/src/app/models/map-data.ts b/frontend/src/app/models/map-data.ts index 332c529ab..bd70b1bdc 100644 --- a/frontend/src/app/models/map-data.ts +++ b/frontend/src/app/models/map-data.ts @@ -1,4 +1,4 @@ -import { Observable, forkJoin, of } from 'rxjs'; +import { map, of, Observable, switchMap, withLatestFrom } from 'rxjs'; import { Results } from './results'; import { GeoDocument, GeoLocation } from './search-results'; import { Params } from '@angular/router'; @@ -21,7 +21,7 @@ interface MapData { export class MapDataResults extends Results { - private mapCenter: GeoLocation | null = null; + private mapCenter$: Observable; constructor( store: Store, @@ -30,6 +30,13 @@ export class MapDataResults extends Results { ) { super(store, query, ['visualizedField']); this.connectToStore(); + this.mapCenter$ = this.state$.pipe( + map(state => state.field), + switchMap(field => field + ? this.visualizationService.getGeoCentroid(field.name, this.query.corpus) + : of(null) + ) + ); this.getResults(); } @@ -39,18 +46,19 @@ export class MapDataResults extends Results { return of({ geoDocuments: [], mapCenter: null }); } - const getGeoCentroid$ = this.mapCenter - ? of(this.mapCenter) - : this.visualizationService.getGeoCentroid(field.name, this.query.corpus); + const geoDocuments$ = this.visualizationService.getGeoData( + field.name, + this.query, + this.query.corpus + ); - return forkJoin({ - geoDocuments: this.visualizationService.getGeoData( - field.name, - this.query, - this.query.corpus - ), - mapCenter: getGeoCentroid$ - }); + return geoDocuments$.pipe( + withLatestFrom(this.mapCenter$), + map(([geoDocuments, mapCenter]) => ({ + geoDocuments, + mapCenter + })) + ); } protected stateToStore(state: MapDataParameters): Params {