From 3652b8f42f987d0536a04028691f032aafe5c34c Mon Sep 17 00:00:00 2001 From: Ronit Jadhav Date: Mon, 12 Feb 2024 12:08:37 +0100 Subject: [PATCH 1/2] Added Geocoding service for Multiple Providers --- .../feature/map/src/lib/feature-map.module.ts | 10 +++ libs/feature/map/src/lib/geocoding.service.ts | 54 ++++++++++++++ .../lib/geocoding/geocoding.component.spec.ts | 73 +++++++++++++++++-- .../src/lib/geocoding/geocoding.component.ts | 38 +++++++--- package-lock.json | 8 +- package.json | 2 +- 6 files changed, 162 insertions(+), 23 deletions(-) create mode 100644 libs/feature/map/src/lib/geocoding.service.ts diff --git a/libs/feature/map/src/lib/feature-map.module.ts b/libs/feature/map/src/lib/feature-map.module.ts index aad9e1954..3d415baec 100644 --- a/libs/feature/map/src/lib/feature-map.module.ts +++ b/libs/feature/map/src/lib/feature-map.module.ts @@ -24,6 +24,7 @@ import { AddLayerFromWmsComponent } from './add-layer-from-wms/add-layer-from-wm import { AddLayerFromFileComponent } from './add-layer-from-file/add-layer-from-file.component' import { AddLayerFromWfsComponent } from './add-layer-from-wfs/add-layer-from-wfs.component' import { GeocodingComponent } from './geocoding/geocoding.component' +import { GEOCODING_PROVIDER } from './geocoding.service' @NgModule({ declarations: [ @@ -65,6 +66,15 @@ import { GeocodingComponent } from './geocoding/geocoding.component' useValue: defaultMapOptions, }, MapFacade, + { + provide: GEOCODING_PROVIDER, + useValue: { + id: 'geonames', + options: { + maxRows: 5, + }, + }, + }, ], }) export class FeatureMapModule {} diff --git a/libs/feature/map/src/lib/geocoding.service.ts b/libs/feature/map/src/lib/geocoding.service.ts new file mode 100644 index 000000000..35e3f5f74 --- /dev/null +++ b/libs/feature/map/src/lib/geocoding.service.ts @@ -0,0 +1,54 @@ +import { Injectable, Inject, InjectionToken } from '@angular/core' +import { + queryGeoadmin, + GeoadminOptions, + GeocodingResult, + queryGeonames, + GeonamesOptions, + DataGouvFrOptions, + queryDataGouvFr, +} from '@geospatial-sdk/geocoding' +import { from, Observable, throwError } from 'rxjs' +import { catchError } from 'rxjs/operators' + +export const GEOCODING_PROVIDER = new InjectionToken('geocoding-provider') + +export interface GeocodingProvider { + id: 'geoadmin' | 'geonames' | 'data-gouv-fr' + options: GeoadminOptions | GeonamesOptions | DataGouvFrOptions +} + +@Injectable({ + providedIn: 'root', +}) +export class GeocodingService { + constructor( + @Inject(GEOCODING_PROVIDER) private provider: GeocodingProvider + ) {} + + query(text: string): Observable { + let queryObservable: Observable + switch (this.provider.id) { + case 'geoadmin': + queryObservable = from( + queryGeoadmin(text, this.provider.options as GeoadminOptions) + ) + break + case 'geonames': + queryObservable = from( + queryGeonames(text, this.provider.options as GeonamesOptions) + ) + break + case 'data-gouv-fr': + queryObservable = from( + queryDataGouvFr(text, this.provider.options as DataGouvFrOptions) + ) + break + default: + return throwError( + () => new Error(`Unsupported geocoding provider: ${this.provider.id}`) + ) + } + return queryObservable.pipe(catchError((error) => throwError(error))) + } +} diff --git a/libs/feature/map/src/lib/geocoding/geocoding.component.spec.ts b/libs/feature/map/src/lib/geocoding/geocoding.component.spec.ts index de77aa93e..4ba52cb46 100644 --- a/libs/feature/map/src/lib/geocoding/geocoding.component.spec.ts +++ b/libs/feature/map/src/lib/geocoding/geocoding.component.spec.ts @@ -12,6 +12,8 @@ import { FEATURE_COLLECTION_POINT_FIXTURE_4326 } from '@geonetwork-ui/common/fix import Feature from 'ol/Feature' import { Geometry } from 'ol/geom' import { TranslateModule } from '@ngx-translate/core' +import { GeocodingService } from '../geocoding.service' +import { of } from 'rxjs' const vectorLayer = new VectorLayer({ source: new VectorSource({ @@ -40,6 +42,10 @@ const mapManagerMock = { map: mapMock, } +const geocodingServiceMock = { + query: jest.fn().mockReturnValue(of([])), +} + describe('GeocodingComponent', () => { let component: GeocodingComponent let fixture: ComponentFixture @@ -48,7 +54,10 @@ describe('GeocodingComponent', () => { await TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [GeocodingComponent], - providers: [{ provide: MapManagerService, useValue: mapManagerMock }], + providers: [ + { provide: MapManagerService, useValue: mapManagerMock }, + { provide: GeocodingService, useValue: geocodingServiceMock }, + ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents() @@ -85,18 +94,68 @@ describe('GeocodingComponent', () => { }) describe('zoomToLocation', () => { - it('should zoom to the location of the result', () => { + let viewMock: any + let zoomToPointSpy: jest.SpyInstance + let zoomToPolygonSpy: jest.SpyInstance + + beforeEach(() => { + viewMock = { + setCenter: jest.fn(), + setZoom: jest.fn(), + fit: jest.fn(), + } + mapMock.getView = jest.fn().mockReturnValue(viewMock) + zoomToPointSpy = jest.spyOn(component, 'zoomToPoint') + zoomToPolygonSpy = jest.spyOn(component, 'zoomToPolygon') + }) + + it('should zoom to the location of the result if geometry type is Point', () => { const result = { geom: { - coordinates: [[0, 0]], + type: 'Point', + coordinates: [0, 0], }, } - const viewMock = { - fit: jest.fn(), + component.zoomToLocation(result) + expect(zoomToPointSpy).toHaveBeenCalledWith( + result.geom.coordinates, + viewMock + ) + }) + + it('should zoom to the location of the result if geometry type is Polygon', () => { + const result = { + geom: { + type: 'Polygon', + coordinates: [ + [ + [0, 0], + [1, 1], + [2, 2], + [0, 0], + ], + ], + }, + } + component.zoomToLocation(result) + expect(zoomToPolygonSpy).toHaveBeenCalledWith( + result.geom.coordinates, + viewMock + ) + }) + + it('should log an error if geometry type is unsupported', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation() + const result = { + geom: { + type: 'Unsupported', + coordinates: [0, 0], + }, } - mapMock.getView = jest.fn().mockReturnValue(viewMock) component.zoomToLocation(result) - expect(viewMock.fit).toHaveBeenCalled() + expect(consoleSpy).toHaveBeenCalledWith( + `Unsupported geometry type: ${result.geom.type}` + ) }) }) describe('onEnterPress', () => { diff --git a/libs/feature/map/src/lib/geocoding/geocoding.component.ts b/libs/feature/map/src/lib/geocoding/geocoding.component.ts index ec3901345..20794964b 100644 --- a/libs/feature/map/src/lib/geocoding/geocoding.component.ts +++ b/libs/feature/map/src/lib/geocoding/geocoding.component.ts @@ -1,10 +1,10 @@ import { Component, OnDestroy } from '@angular/core' -import { queryGeoadmin, GeoadminOptions } from '@geospatial-sdk/geocoding' import { catchError, from, Subject, takeUntil } from 'rxjs' import { debounceTime, switchMap } from 'rxjs/operators' import { MapManagerService } from '../manager/map-manager.service' import { fromLonLat } from 'ol/proj' import { Polygon } from 'ol/geom' +import { GeocodingService } from '../geocoding.service' @Component({ selector: 'gn-ui-geocoding', @@ -16,18 +16,20 @@ export class GeocodingComponent implements OnDestroy { results: any[] = [] searchTextChanged = new Subject() destroy$ = new Subject() + errorMessage: string | null = null - constructor(private mapManager: MapManagerService) { + constructor( + private mapManager: MapManagerService, + private geocodingService: GeocodingService + ) { this.searchTextChanged .pipe( debounceTime(300), switchMap((searchText) => { - const options: GeoadminOptions = { - origins: ['zipcode', 'gg25', 'address'], - limit: 6, - } - return from(queryGeoadmin(searchText, options)).pipe( + return from(this.geocodingService.query(searchText)).pipe( catchError((error) => { + this.errorMessage = + 'An error occurred while searching. Please try again.' console.error(error) return [] }) @@ -57,18 +59,32 @@ export class GeocodingComponent implements OnDestroy { clearSearch() { this.searchText = '' this.results = [] + this.errorMessage = null } - zoomToLocation(result) { + zoomToLocation(result: any) { const map = this.mapManager.map const view = map.getView() const geometry = result.geom - const polygonCoords = geometry.coordinates - const transformedCoords = polygonCoords[0].map((coord) => fromLonLat(coord)) + if (geometry.type === 'Point') { + this.zoomToPoint(geometry.coordinates, view) + } else if (geometry.type === 'Polygon') { + this.zoomToPolygon(geometry.coordinates, view) + } else { + console.error(`Unsupported geometry type: ${geometry.type}`) + } + } - const polygon = new Polygon([transformedCoords]) + zoomToPoint(pointCoords: [number, number], view: any) { + const transformedCoords = fromLonLat(pointCoords) + view.setCenter(transformedCoords) + view.setZoom(12) + } + zoomToPolygon(polygonCoords: [[number, number][]], view: any) { + const transformedCoords = polygonCoords[0].map((coord) => fromLonLat(coord)) + const polygon = new Polygon([transformedCoords]) view.fit(polygon, { duration: 100, maxZoom: 12, diff --git a/package-lock.json b/package-lock.json index e22d2fa78..52c27a6a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@bartholomej/ngx-translate-extract": "^8.0.2", "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", "@camptocamp/ogc-client": "^0.4.0", - "@geospatial-sdk/geocoding": "^0.0.5-alpha.1", + "@geospatial-sdk/geocoding": "^0.0.5-alpha.2", "@ltd/j-toml": "~1.35.2", "@messageformat/core": "^3.0.1", "@nestjs/common": "10.1.3", @@ -4312,9 +4312,9 @@ "dev": true }, "node_modules/@geospatial-sdk/geocoding": { - "version": "0.0.5-alpha.1", - "resolved": "https://registry.npmjs.org/@geospatial-sdk/geocoding/-/geocoding-0.0.5-alpha.1.tgz", - "integrity": "sha512-LM1aKG1hl2hnJFLouyjUpCwwT2ToQXeUlExHUvGi/cq1vy2z4AeygLL9etPkEnCPz70B6713gN4mKsmDWdaDiQ==" + "version": "0.0.5-alpha.2", + "resolved": "https://registry.npmjs.org/@geospatial-sdk/geocoding/-/geocoding-0.0.5-alpha.2.tgz", + "integrity": "sha512-q9szQpj+/a0A1Dp9+na8wdkhouMhegLVTD5bB1DkHCJW5eG8CUA/cPzfg1REONNQgXMNUHZHp8mGjQEmTu/zHQ==" }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", diff --git a/package.json b/package.json index b0116a5b0..cee2ebecd 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@bartholomej/ngx-translate-extract": "^8.0.2", "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", "@camptocamp/ogc-client": "^0.4.0", - "@geospatial-sdk/geocoding": "^0.0.5-alpha.1", + "@geospatial-sdk/geocoding": "^0.0.5-alpha.2", "@ltd/j-toml": "~1.35.2", "@messageformat/core": "^3.0.1", "@nestjs/common": "10.1.3", From 1c8006b8e2c506f57749d104d6f3d1c90116d1e6 Mon Sep 17 00:00:00 2001 From: Ronit Jadhav Date: Wed, 14 Feb 2024 13:47:44 +0100 Subject: [PATCH 2/2] Refactor GeocodingService to support multiple geocoding providers --- .../feature/map/src/lib/feature-map.module.ts | 9 ++----- libs/feature/map/src/lib/geocoding.service.ts | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/libs/feature/map/src/lib/feature-map.module.ts b/libs/feature/map/src/lib/feature-map.module.ts index 3d415baec..8365e02f8 100644 --- a/libs/feature/map/src/lib/feature-map.module.ts +++ b/libs/feature/map/src/lib/feature-map.module.ts @@ -24,7 +24,7 @@ import { AddLayerFromWmsComponent } from './add-layer-from-wms/add-layer-from-wm import { AddLayerFromFileComponent } from './add-layer-from-file/add-layer-from-file.component' import { AddLayerFromWfsComponent } from './add-layer-from-wfs/add-layer-from-wfs.component' import { GeocodingComponent } from './geocoding/geocoding.component' -import { GEOCODING_PROVIDER } from './geocoding.service' +import { GEOCODING_PROVIDER, GeocodingProvider } from './geocoding.service' @NgModule({ declarations: [ @@ -68,12 +68,7 @@ import { GEOCODING_PROVIDER } from './geocoding.service' MapFacade, { provide: GEOCODING_PROVIDER, - useValue: { - id: 'geonames', - options: { - maxRows: 5, - }, - }, + useValue: ['geonames', { maxRows: 5 }] as GeocodingProvider, }, ], }) diff --git a/libs/feature/map/src/lib/geocoding.service.ts b/libs/feature/map/src/lib/geocoding.service.ts index 35e3f5f74..14ba3dcbf 100644 --- a/libs/feature/map/src/lib/geocoding.service.ts +++ b/libs/feature/map/src/lib/geocoding.service.ts @@ -11,12 +11,17 @@ import { import { from, Observable, throwError } from 'rxjs' import { catchError } from 'rxjs/operators' -export const GEOCODING_PROVIDER = new InjectionToken('geocoding-provider') +type GeoadminGeocodingProvider = ['geoadmin', GeoadminOptions] +type GeonamesGeocodingProvider = ['geonames', GeonamesOptions] +type DataGouvFrGeocodingProvider = ['data-gouv-fr', DataGouvFrOptions] +export type GeocodingProvider = + | GeoadminGeocodingProvider + | GeonamesGeocodingProvider + | DataGouvFrGeocodingProvider -export interface GeocodingProvider { - id: 'geoadmin' | 'geonames' | 'data-gouv-fr' - options: GeoadminOptions | GeonamesOptions | DataGouvFrOptions -} +export const GEOCODING_PROVIDER = new InjectionToken( + 'geocoding-provider' +) @Injectable({ providedIn: 'root', @@ -28,25 +33,25 @@ export class GeocodingService { query(text: string): Observable { let queryObservable: Observable - switch (this.provider.id) { + switch (this.provider[0]) { case 'geoadmin': queryObservable = from( - queryGeoadmin(text, this.provider.options as GeoadminOptions) + queryGeoadmin(text, this.provider[1] as GeoadminOptions) ) break case 'geonames': queryObservable = from( - queryGeonames(text, this.provider.options as GeonamesOptions) + queryGeonames(text, this.provider[1] as GeonamesOptions) ) break case 'data-gouv-fr': queryObservable = from( - queryDataGouvFr(text, this.provider.options as DataGouvFrOptions) + queryDataGouvFr(text, this.provider[1] as DataGouvFrOptions) ) break default: return throwError( - () => new Error(`Unsupported geocoding provider: ${this.provider.id}`) + () => new Error(`Unsupported geocoding provider: ${this.provider[0]}`) ) } return queryObservable.pipe(catchError((error) => throwError(error)))