From 37d247f503310f48baf9faa79ef3e556ed97ae46 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Tue, 28 May 2024 12:13:29 +0200 Subject: [PATCH 01/10] feat: filter out non geo datasets --- .../record-metadata.component.ts | 42 +++++++++++++++---- .../dataviz/src/lib/service/data.service.ts | 1 + .../src/lib/map-view/map-view.component.ts | 32 +++++++++++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts index d04313355a..b287d819c4 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts @@ -2,14 +2,15 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { SourcesService } from '@geonetwork-ui/feature/catalog' import { SearchService } from '@geonetwork-ui/feature/search' import { ErrorType } from '@geonetwork-ui/ui/elements' -import { BehaviorSubject, combineLatest } from 'rxjs' -import { filter, map, mergeMap } from 'rxjs/operators' +import { BehaviorSubject, combineLatest, from, of } from 'rxjs' +import { filter, map, mergeMap, switchMap } from 'rxjs/operators' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { Keyword, Organization, } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { DataService } from '@geonetwork-ui/feature/dataviz' @Component({ selector: 'datahub-record-metadata', @@ -24,10 +25,36 @@ export class RecordMetadataComponent { this.metadataViewFacade.mapApiLinks$, this.metadataViewFacade.geoDataLinks$, ]).pipe( - map( - ([mapLinks, geoDataLinks]) => - mapLinks?.length > 0 || geoDataLinks?.length > 0 - ) + switchMap(([mapApiLinks, geoDataLinks]) => { + const allLinks = [...mapApiLinks, ...geoDataLinks] + const ogcLinks = allLinks.filter( + (link) => + link.type === 'service' && + link.accessServiceProtocol === 'ogcFeatures' + ) + const otherLinks = allLinks.filter((link) => !ogcLinks.includes(link)) + if (ogcLinks.length === 0) { + return of(otherLinks?.length > 0) + } + return from(ogcLinks).pipe( + mergeMap((link) => + this.dataService.readAsGeoJson(link).pipe( + map((data) => { + const firstFeatures = data.features.slice(0, 10) + return { + link, + hasGeometry: firstFeatures.some( + (feature) => feature.geometry != null + ), + } + }) + ) + ), + map((features) => { + return features.hasGeometry || otherLinks?.length > 0 + }) + ) + }) ) displayData$ = combineLatest([ @@ -104,7 +131,8 @@ export class RecordMetadataComponent { public metadataViewFacade: MdViewFacade, private searchService: SearchService, private sourceService: SourcesService, - private orgsService: OrganizationsServiceInterface + private orgsService: OrganizationsServiceInterface, + private dataService: DataService ) {} onTabIndexChange(index: number): void { diff --git a/libs/feature/dataviz/src/lib/service/data.service.ts b/libs/feature/dataviz/src/lib/service/data.service.ts index 993f479c63..c3abd9b5e4 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.ts @@ -161,6 +161,7 @@ export class DataService { ogcApiLink.url.href ) return Object.keys(collectionInfo.bulkDownloadLinks).map((downloadLink) => { + console.log(downloadLink) return { ...ogcApiLink, type: 'download', diff --git a/libs/feature/record/src/lib/map-view/map-view.component.ts b/libs/feature/record/src/lib/map-view/map-view.component.ts index 24870f16b6..c75beab1a0 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.ts +++ b/libs/feature/record/src/lib/map-view/map-view.component.ts @@ -32,10 +32,13 @@ import { import { catchError, distinctUntilChanged, + filter, finalize, map, + mergeMap, switchMap, tap, + toArray, } from 'rxjs/operators' import { MdViewFacade } from '../state/mdview.facade' import { DataService } from '@geonetwork-ui/feature/dataviz' @@ -57,7 +60,34 @@ export class MapViewComponent implements OnInit, OnDestroy { this.mdViewFacade.mapApiLinks$, this.mdViewFacade.geoDataLinks$, ]).pipe( - map(([mapApiLinks, geoDataLinks]) => [...mapApiLinks, ...geoDataLinks]) + switchMap(([mapApiLinks, geoDataLinks]) => { + const allLinks = [...mapApiLinks, ...geoDataLinks] + const ogcLinks = allLinks.filter( + (link) => + link.type === 'service' && + link.accessServiceProtocol === 'ogcFeatures' + ) + const otherLinks = allLinks.filter((link) => !ogcLinks.includes(link)) + return from(ogcLinks).pipe( + mergeMap((link) => + this.dataService.readAsGeoJson(link).pipe( + map((data) => { + const firstFeatures = data.features.slice(0, 10) + return { + link, + hasGeometry: firstFeatures.every((feature) => feature.geometry), + } + }) + ) + ), + filter(({ hasGeometry }) => hasGeometry), + map(({ link }) => { + return link + }), + toArray(), + map((ogcLinksWithGeometry) => [...otherLinks, ...ogcLinksWithGeometry]) + ) + }) ) dropdownChoices$ = this.compatibleMapLinks$.pipe( From 5f13e66d5a65e3d409774a99e67e0621f32e4f4c Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Tue, 28 May 2024 13:21:57 +0200 Subject: [PATCH 02/10] feat: move link filtering to view facade to avoid duplicates --- .../record-metadata.component.ts | 36 +++---------------- .../dataviz/src/lib/service/data.service.ts | 16 ++++++++- .../src/lib/map-view/map-view.component.ts | 34 +++--------------- .../record/src/lib/state/mdview.facade.ts | 35 ++++++++++++++++-- 4 files changed, 58 insertions(+), 63 deletions(-) diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts index b287d819c4..7f1fe404c3 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts @@ -23,38 +23,12 @@ export class RecordMetadataComponent { displayMap$ = combineLatest([ this.metadataViewFacade.mapApiLinks$, - this.metadataViewFacade.geoDataLinks$, + this.metadataViewFacade.geospatialLinks$, ]).pipe( - switchMap(([mapApiLinks, geoDataLinks]) => { - const allLinks = [...mapApiLinks, ...geoDataLinks] - const ogcLinks = allLinks.filter( - (link) => - link.type === 'service' && - link.accessServiceProtocol === 'ogcFeatures' - ) - const otherLinks = allLinks.filter((link) => !ogcLinks.includes(link)) - if (ogcLinks.length === 0) { - return of(otherLinks?.length > 0) - } - return from(ogcLinks).pipe( - mergeMap((link) => - this.dataService.readAsGeoJson(link).pipe( - map((data) => { - const firstFeatures = data.features.slice(0, 10) - return { - link, - hasGeometry: firstFeatures.some( - (feature) => feature.geometry != null - ), - } - }) - ) - ), - map((features) => { - return features.hasGeometry || otherLinks?.length > 0 - }) - ) - }) + map( + ([mapApiLinks, geospatialLinks]) => + mapApiLinks?.length > 0 || geospatialLinks?.length > 0 + ) ) displayData$ = combineLatest([ diff --git a/libs/feature/dataviz/src/lib/service/data.service.ts b/libs/feature/dataviz/src/lib/service/data.service.ts index c3abd9b5e4..e58f2caab1 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.ts @@ -3,6 +3,7 @@ import { marker } from '@biesbjerg/ngx-translate-extract-marker' import { OgcApiCollectionInfo, OgcApiEndpoint, + OgcApiRecord, WfsEndpoint, WfsVersion, } from '@camptocamp/ogc-client' @@ -161,7 +162,6 @@ export class DataService { ogcApiLink.url.href ) return Object.keys(collectionInfo.bulkDownloadLinks).map((downloadLink) => { - console.log(downloadLink) return { ...ogcApiLink, type: 'download', @@ -184,6 +184,20 @@ export class DataService { }) } + async getItemsFromOgcApi( + url: string, + itemsLimit?: number + ): Promise { + const endpoint = new OgcApiEndpoint(this.proxy.getProxiedUrl(url)) + return await endpoint.featureCollections + .then((collections) => { + return endpoint.getCollectionItems(collections[0], itemsLimit) + }) + .catch((error) => { + throw new Error(`ogc.unreachable.unknown`) + }) + } + getDownloadLinksFromEsriRest( esriRestLink: DatasetServiceDistribution ): DatasetDistribution[] { diff --git a/libs/feature/record/src/lib/map-view/map-view.component.ts b/libs/feature/record/src/lib/map-view/map-view.component.ts index c75beab1a0..c4da71a1d9 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.ts +++ b/libs/feature/record/src/lib/map-view/map-view.component.ts @@ -58,36 +58,12 @@ export class MapViewComponent implements OnInit, OnDestroy { compatibleMapLinks$ = combineLatest([ this.mdViewFacade.mapApiLinks$, - this.mdViewFacade.geoDataLinks$, + this.mdViewFacade.geospatialLinks$, ]).pipe( - switchMap(([mapApiLinks, geoDataLinks]) => { - const allLinks = [...mapApiLinks, ...geoDataLinks] - const ogcLinks = allLinks.filter( - (link) => - link.type === 'service' && - link.accessServiceProtocol === 'ogcFeatures' - ) - const otherLinks = allLinks.filter((link) => !ogcLinks.includes(link)) - return from(ogcLinks).pipe( - mergeMap((link) => - this.dataService.readAsGeoJson(link).pipe( - map((data) => { - const firstFeatures = data.features.slice(0, 10) - return { - link, - hasGeometry: firstFeatures.every((feature) => feature.geometry), - } - }) - ) - ), - filter(({ hasGeometry }) => hasGeometry), - map(({ link }) => { - return link - }), - toArray(), - map((ogcLinksWithGeometry) => [...otherLinks, ...ogcLinksWithGeometry]) - ) - }) + map(([mapApiLinks, geospatialLinks]) => [ + ...mapApiLinks, + ...geospatialLinks, + ]) ) dropdownChoices$ = this.compatibleMapLinks$.pipe( diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 37364de54d..27cec417b7 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core' import { select, Store } from '@ngrx/store' -import { filter, map } from 'rxjs/operators' +import { filter, map, switchMap } from 'rxjs/operators' import * as MdViewActions from './mdview.actions' import * as MdViewSelectors from './mdview.selectors' import { LinkClassifierService, LinkUsage } from '@geonetwork-ui/util/shared' @@ -10,6 +10,9 @@ import { UserFeedback, } from '@geonetwork-ui/common/domain/model/record' import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' +import { OgcApiRecord } from '@camptocamp/ogc-client' +import { from, of } from 'rxjs' +import { DataService } from '@geonetwork-ui/feature/dataviz' @Injectable() /** @@ -22,7 +25,8 @@ export class MdViewFacade { constructor( private store: Store, private linkClassifier: LinkClassifierService, - private avatarService: AvatarServiceInterface + private avatarService: AvatarServiceInterface, + private dataService: DataService ) {} isPresent$ = this.store.pipe( @@ -90,6 +94,33 @@ export class MdViewFacade { ) ) + geospatialLinks$ = this.allLinks$.pipe( + map((links) => { + return links.filter((link) => { + if ( + this.linkClassifier.hasUsage(link, LinkUsage.GEODATA) && + link.type === 'service' && + link.accessServiceProtocol === 'ogcFeatures' + ) { + return from( + this.dataService.getItemsFromOgcApi(link.url.href, 10) + ).pipe( + switchMap((collectionRecords: OgcApiRecord[]) => { + const hasGeometry = collectionRecords.some( + (record) => record.geometry + ) + return hasGeometry ? of(link) : of(null) + }) + ) + } else if (this.linkClassifier.hasUsage(link, LinkUsage.GEODATA)) { + return link + } else { + return null + } + }) + }) + ) + landingPageLinks$ = this.metadata$.pipe( map((record) => ('landingPage' in record ? [record.landingPage] : [])) ) From 83b17346ba9c10865505178893b8b104bce977c9 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Tue, 28 May 2024 16:55:30 +0200 Subject: [PATCH 03/10] feat: handle non geo ogc links for mapping --- .../record-metadata.component.ts | 8 +-- .../dataviz/src/lib/service/data.service.ts | 7 +-- .../src/lib/map-view/map-view.component.ts | 7 ++- .../record/src/lib/state/mdview.facade.ts | 51 ++++++++++++------- 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts index 7f1fe404c3..3e747cb649 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts @@ -25,10 +25,10 @@ export class RecordMetadataComponent { this.metadataViewFacade.mapApiLinks$, this.metadataViewFacade.geospatialLinks$, ]).pipe( - map( - ([mapApiLinks, geospatialLinks]) => - mapApiLinks?.length > 0 || geospatialLinks?.length > 0 - ) + map(([mapApiLinks, geospatialLinks]) => { + console.log(geospatialLinks) + return mapApiLinks?.length > 0 || geospatialLinks?.length > 0 + }) ) displayData$ = combineLatest([ diff --git a/libs/feature/dataviz/src/lib/service/data.service.ts b/libs/feature/dataviz/src/lib/service/data.service.ts index e58f2caab1..7e7db5f31d 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.ts @@ -184,14 +184,11 @@ export class DataService { }) } - async getItemsFromOgcApi( - url: string, - itemsLimit?: number - ): Promise { + async getItemsFromOgcApi(url: string): Promise { const endpoint = new OgcApiEndpoint(this.proxy.getProxiedUrl(url)) return await endpoint.featureCollections .then((collections) => { - return endpoint.getCollectionItems(collections[0], itemsLimit) + return endpoint.getCollectionItem(collections[0], '10') }) .catch((error) => { throw new Error(`ogc.unreachable.unknown`) diff --git a/libs/feature/record/src/lib/map-view/map-view.component.ts b/libs/feature/record/src/lib/map-view/map-view.component.ts index c4da71a1d9..60f6cc9f62 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.ts +++ b/libs/feature/record/src/lib/map-view/map-view.component.ts @@ -60,10 +60,9 @@ export class MapViewComponent implements OnInit, OnDestroy { this.mdViewFacade.mapApiLinks$, this.mdViewFacade.geospatialLinks$, ]).pipe( - map(([mapApiLinks, geospatialLinks]) => [ - ...mapApiLinks, - ...geospatialLinks, - ]) + map(([mapApiLinks, geospatialLinks]) => { + return [...mapApiLinks, ...geospatialLinks] + }) ) dropdownChoices$ = this.compatibleMapLinks$.pipe( diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 27cec417b7..5e963f7fed 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -1,6 +1,16 @@ import { Injectable } from '@angular/core' import { select, Store } from '@ngrx/store' -import { filter, map, switchMap } from 'rxjs/operators' +import { + defaultIfEmpty, + distinct, + filter, + map, + mergeMap, + scan, + switchMap, + tap, + toArray, +} from 'rxjs/operators' import * as MdViewActions from './mdview.actions' import * as MdViewSelectors from './mdview.selectors' import { LinkClassifierService, LinkUsage } from '@geonetwork-ui/util/shared' @@ -95,30 +105,35 @@ export class MdViewFacade { ) geospatialLinks$ = this.allLinks$.pipe( - map((links) => { - return links.filter((link) => { + mergeMap((links) => { + return from(links) + }), + mergeMap((link) => { + if (this.linkClassifier.hasUsage(link, LinkUsage.GEODATA)) { if ( - this.linkClassifier.hasUsage(link, LinkUsage.GEODATA) && link.type === 'service' && link.accessServiceProtocol === 'ogcFeatures' ) { - return from( - this.dataService.getItemsFromOgcApi(link.url.href, 10) - ).pipe( - switchMap((collectionRecords: OgcApiRecord[]) => { - const hasGeometry = collectionRecords.some( - (record) => record.geometry - ) - return hasGeometry ? of(link) : of(null) - }) + return from(this.dataService.getItemsFromOgcApi(link.url.href)).pipe( + map((collectionRecords: OgcApiRecord) => { + const hasGeometry = collectionRecords.geometry + return hasGeometry ? link : null + }), + defaultIfEmpty(null) ) - } else if (this.linkClassifier.hasUsage(link, LinkUsage.GEODATA)) { - return link } else { - return null + return of(link) } - }) - }) + } else { + return of(null) + } + }), + scan((acc, val) => { + if (val !== null && !acc.includes(val)) { + acc.push(val) + } + return acc + }, []) ) landingPageLinks$ = this.metadata$.pipe( From ac1bc4319f4733c97800282f8d6b3bc1d0aac305 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Wed, 29 May 2024 11:23:43 +0200 Subject: [PATCH 04/10] feat: add dump and change feature offset --- libs/feature/dataviz/src/lib/service/data.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/feature/dataviz/src/lib/service/data.service.ts b/libs/feature/dataviz/src/lib/service/data.service.ts index 7e7db5f31d..c428511bd6 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.ts @@ -188,7 +188,7 @@ export class DataService { const endpoint = new OgcApiEndpoint(this.proxy.getProxiedUrl(url)) return await endpoint.featureCollections .then((collections) => { - return endpoint.getCollectionItem(collections[0], '10') + return endpoint.getCollectionItem(collections[0], '1') }) .catch((error) => { throw new Error(`ogc.unreachable.unknown`) From 61b027ed9c7f7516eed28d2dbbbdd26938229aef Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Wed, 29 May 2024 12:14:21 +0200 Subject: [PATCH 05/10] fix: fix unit tests --- .../record-metadata.component.spec.ts | 4 ++- .../record-metadata.component.ts | 1 - .../lib/map-view/map-view.component.spec.ts | 30 +++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts index 5f6585bc4d..e0f41f3041 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts @@ -39,6 +39,7 @@ class MdViewFacadeMock { mapApiLinks$ = new BehaviorSubject([]) dataLinks$ = new BehaviorSubject([]) geoDataLinks$ = new BehaviorSubject([]) + geospatialLinks$ = new BehaviorSubject([]) downloadLinks$ = new BehaviorSubject([]) apiLinks$ = new BehaviorSubject([]) otherLinks$ = new BehaviorSubject([]) @@ -377,6 +378,7 @@ describe('RecordMetadataComponent', () => { }) describe('when a GEODATA link present', () => { beforeEach(() => { + facade.geospatialLinks$.next(['link']) facade.geoDataLinks$.next(['link']) fixture.detectChanges() mapTab = fixture.debugElement.queryAll(By.css('mat-tab'))[0] @@ -399,7 +401,7 @@ describe('RecordMetadataComponent', () => { beforeEach(() => { facade.mapApiLinks$.next(['link']) facade.dataLinks$.next(null) - facade.geoDataLinks$.next(null) + facade.geospatialLinks$.next(null) fixture.detectChanges() tableTab = fixture.debugElement.queryAll(By.css('mat-tab'))[1] chartTab = fixture.debugElement.queryAll(By.css('mat-tab'))[2] diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts index 3e747cb649..2352920fbf 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts @@ -26,7 +26,6 @@ export class RecordMetadataComponent { this.metadataViewFacade.geospatialLinks$, ]).pipe( map(([mapApiLinks, geospatialLinks]) => { - console.log(geospatialLinks) return mapApiLinks?.length > 0 || geospatialLinks?.length > 0 }) ) diff --git a/libs/feature/record/src/lib/map-view/map-view.component.spec.ts b/libs/feature/record/src/lib/map-view/map-view.component.spec.ts index 6753dff0bf..6ad3356fb6 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.spec.ts +++ b/libs/feature/record/src/lib/map-view/map-view.component.spec.ts @@ -67,7 +67,7 @@ jest.mock('@geonetwork-ui/util/app-config', () => ({ class MdViewFacadeMock { mapApiLinks$ = new Subject() - geoDataLinks$ = new Subject() + geospatialLinks$ = new Subject() metadata$ = of({ title: 'abcd' }) } @@ -272,7 +272,7 @@ describe('MapViewComponent', () => { describe('with no link compatible with MAP_API or GEODATA usage', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geoDataLinks$.next([]) + mdViewFacade.geospatialLinks$.next([]) tick() fixture.detectChanges() })) @@ -317,7 +317,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wms', }, ]) - mdViewFacade.geoDataLinks$.next([]) + mdViewFacade.geospatialLinks$.next([]) tick() fixture.detectChanges() })) @@ -365,7 +365,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wms', }, ]) - mdViewFacade.geoDataLinks$.next([ + mdViewFacade.geospatialLinks$.next([ { url: new URL('http://abcd.com/wfs'), name: 'featuretype', @@ -419,7 +419,7 @@ describe('MapViewComponent', () => { describe('with a link using WFS protocol', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geoDataLinks$.next([ + mdViewFacade.geospatialLinks$.next([ { url: new URL('http://abcd.com/wfs'), name: 'featuretype', @@ -453,7 +453,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wmts', }, ]) - mdViewFacade.geoDataLinks$.next([]) + mdViewFacade.geospatialLinks$.next([]) tick(200) fixture.detectChanges() })) @@ -474,7 +474,7 @@ describe('MapViewComponent', () => { describe('with a link using ESRI:REST protocol', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geoDataLinks$.next([ + mdViewFacade.geospatialLinks$.next([ { name: 'mes_hdf', url: new URL( @@ -503,7 +503,7 @@ describe('MapViewComponent', () => { describe('with a link using OGC API protocol', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geoDataLinks$.next([ + mdViewFacade.geospatialLinks$.next([ { name: 'ogc layer', url: new URL('http://abcd.com/data/ogcapi'), @@ -530,7 +530,7 @@ describe('MapViewComponent', () => { describe('with a link using WFS which returns an error', () => { beforeEach(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geoDataLinks$.next([ + mdViewFacade.geospatialLinks$.next([ { url: new URL('http://abcd.com/wfs/error'), name: 'featuretype', @@ -548,7 +548,7 @@ describe('MapViewComponent', () => { describe('during download', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geoDataLinks$.next([ + mdViewFacade.geospatialLinks$.next([ { url: new URL('http://abcd.com/data.geojson'), name: 'data.geojson', @@ -571,7 +571,7 @@ describe('MapViewComponent', () => { describe('after download', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geoDataLinks$.next([ + mdViewFacade.geospatialLinks$.next([ { url: new URL('http://abcd.com/data.geojson'), name: 'data.geojson', @@ -605,7 +605,7 @@ describe('MapViewComponent', () => { describe('when receiving several metadata records', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geoDataLinks$.next([ + mdViewFacade.geospatialLinks$.next([ { url: new URL('http://abcd.com/data.geojson'), name: 'data.geojson', @@ -620,7 +620,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wms', }, ]) - mdViewFacade.geoDataLinks$.next([]) + mdViewFacade.geospatialLinks$.next([]) tick() fixture.detectChanges() })) @@ -671,7 +671,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wms', }, ]) - mdViewFacade.geoDataLinks$.next([]) + mdViewFacade.geospatialLinks$.next([]) dropdownComponent.selectValue.emit(1) tick() fixture.detectChanges() @@ -856,7 +856,7 @@ describe('MapViewComponent', () => { describe('changing the map context', () => { beforeEach(() => { jest.spyOn(component, 'resetSelection') - mdViewFacade.geoDataLinks$.next([]) + mdViewFacade.geospatialLinks$.next([]) mdViewFacade.mapApiLinks$.next([]) }) it('resets selection', () => { From 3a46fb6d0ff226520a7152109d99d85d57b7cc90 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 30 May 2024 15:36:56 +0200 Subject: [PATCH 06/10] feat: change observable name --- .../record-metadata.component.spec.ts | 6 ++-- .../record-metadata.component.ts | 14 ++++----- .../lib/map-view/map-view.component.spec.ts | 30 +++++++++---------- .../src/lib/map-view/map-view.component.ts | 13 ++++---- .../record/src/lib/state/mdview.facade.ts | 14 ++------- 5 files changed, 31 insertions(+), 46 deletions(-) diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts index e0f41f3041..b0fd122885 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.spec.ts @@ -39,7 +39,7 @@ class MdViewFacadeMock { mapApiLinks$ = new BehaviorSubject([]) dataLinks$ = new BehaviorSubject([]) geoDataLinks$ = new BehaviorSubject([]) - geospatialLinks$ = new BehaviorSubject([]) + geoDataLinksWithGeometry$ = new BehaviorSubject([]) downloadLinks$ = new BehaviorSubject([]) apiLinks$ = new BehaviorSubject([]) otherLinks$ = new BehaviorSubject([]) @@ -378,7 +378,7 @@ describe('RecordMetadataComponent', () => { }) describe('when a GEODATA link present', () => { beforeEach(() => { - facade.geospatialLinks$.next(['link']) + facade.geoDataLinksWithGeometry$.next(['link']) facade.geoDataLinks$.next(['link']) fixture.detectChanges() mapTab = fixture.debugElement.queryAll(By.css('mat-tab'))[0] @@ -401,7 +401,7 @@ describe('RecordMetadataComponent', () => { beforeEach(() => { facade.mapApiLinks$.next(['link']) facade.dataLinks$.next(null) - facade.geospatialLinks$.next(null) + facade.geoDataLinksWithGeometry$.next(null) fixture.detectChanges() tableTab = fixture.debugElement.queryAll(By.css('mat-tab'))[1] chartTab = fixture.debugElement.queryAll(By.css('mat-tab'))[2] diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts index 2352920fbf..1edb1ea1eb 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts @@ -2,15 +2,14 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { SourcesService } from '@geonetwork-ui/feature/catalog' import { SearchService } from '@geonetwork-ui/feature/search' import { ErrorType } from '@geonetwork-ui/ui/elements' -import { BehaviorSubject, combineLatest, from, of } from 'rxjs' -import { filter, map, mergeMap, switchMap } from 'rxjs/operators' +import { BehaviorSubject, combineLatest } from 'rxjs' +import { filter, map, mergeMap } from 'rxjs/operators' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { Keyword, Organization, } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' -import { DataService } from '@geonetwork-ui/feature/dataviz' @Component({ selector: 'datahub-record-metadata', @@ -23,10 +22,10 @@ export class RecordMetadataComponent { displayMap$ = combineLatest([ this.metadataViewFacade.mapApiLinks$, - this.metadataViewFacade.geospatialLinks$, + this.metadataViewFacade.geoDataLinksWithGeometry$, ]).pipe( - map(([mapApiLinks, geospatialLinks]) => { - return mapApiLinks?.length > 0 || geospatialLinks?.length > 0 + map(([mapApiLinks, geoDataLinksWithGeometry]) => { + return mapApiLinks?.length > 0 || geoDataLinksWithGeometry?.length > 0 }) ) @@ -104,8 +103,7 @@ export class RecordMetadataComponent { public metadataViewFacade: MdViewFacade, private searchService: SearchService, private sourceService: SourcesService, - private orgsService: OrganizationsServiceInterface, - private dataService: DataService + private orgsService: OrganizationsServiceInterface ) {} onTabIndexChange(index: number): void { diff --git a/libs/feature/record/src/lib/map-view/map-view.component.spec.ts b/libs/feature/record/src/lib/map-view/map-view.component.spec.ts index 6ad3356fb6..587b0d73e9 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.spec.ts +++ b/libs/feature/record/src/lib/map-view/map-view.component.spec.ts @@ -67,7 +67,7 @@ jest.mock('@geonetwork-ui/util/app-config', () => ({ class MdViewFacadeMock { mapApiLinks$ = new Subject() - geospatialLinks$ = new Subject() + geoDataLinksWithGeometry$ = new Subject() metadata$ = of({ title: 'abcd' }) } @@ -272,7 +272,7 @@ describe('MapViewComponent', () => { describe('with no link compatible with MAP_API or GEODATA usage', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geospatialLinks$.next([]) + mdViewFacade.geoDataLinksWithGeometry$.next([]) tick() fixture.detectChanges() })) @@ -317,7 +317,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wms', }, ]) - mdViewFacade.geospatialLinks$.next([]) + mdViewFacade.geoDataLinksWithGeometry$.next([]) tick() fixture.detectChanges() })) @@ -365,7 +365,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wms', }, ]) - mdViewFacade.geospatialLinks$.next([ + mdViewFacade.geoDataLinksWithGeometry$.next([ { url: new URL('http://abcd.com/wfs'), name: 'featuretype', @@ -419,7 +419,7 @@ describe('MapViewComponent', () => { describe('with a link using WFS protocol', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geospatialLinks$.next([ + mdViewFacade.geoDataLinksWithGeometry$.next([ { url: new URL('http://abcd.com/wfs'), name: 'featuretype', @@ -453,7 +453,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wmts', }, ]) - mdViewFacade.geospatialLinks$.next([]) + mdViewFacade.geoDataLinksWithGeometry$.next([]) tick(200) fixture.detectChanges() })) @@ -474,7 +474,7 @@ describe('MapViewComponent', () => { describe('with a link using ESRI:REST protocol', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geospatialLinks$.next([ + mdViewFacade.geoDataLinksWithGeometry$.next([ { name: 'mes_hdf', url: new URL( @@ -503,7 +503,7 @@ describe('MapViewComponent', () => { describe('with a link using OGC API protocol', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geospatialLinks$.next([ + mdViewFacade.geoDataLinksWithGeometry$.next([ { name: 'ogc layer', url: new URL('http://abcd.com/data/ogcapi'), @@ -530,7 +530,7 @@ describe('MapViewComponent', () => { describe('with a link using WFS which returns an error', () => { beforeEach(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geospatialLinks$.next([ + mdViewFacade.geoDataLinksWithGeometry$.next([ { url: new URL('http://abcd.com/wfs/error'), name: 'featuretype', @@ -548,7 +548,7 @@ describe('MapViewComponent', () => { describe('during download', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geospatialLinks$.next([ + mdViewFacade.geoDataLinksWithGeometry$.next([ { url: new URL('http://abcd.com/data.geojson'), name: 'data.geojson', @@ -571,7 +571,7 @@ describe('MapViewComponent', () => { describe('after download', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geospatialLinks$.next([ + mdViewFacade.geoDataLinksWithGeometry$.next([ { url: new URL('http://abcd.com/data.geojson'), name: 'data.geojson', @@ -605,7 +605,7 @@ describe('MapViewComponent', () => { describe('when receiving several metadata records', () => { beforeEach(fakeAsync(() => { mdViewFacade.mapApiLinks$.next([]) - mdViewFacade.geospatialLinks$.next([ + mdViewFacade.geoDataLinksWithGeometry$.next([ { url: new URL('http://abcd.com/data.geojson'), name: 'data.geojson', @@ -620,7 +620,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wms', }, ]) - mdViewFacade.geospatialLinks$.next([]) + mdViewFacade.geoDataLinksWithGeometry$.next([]) tick() fixture.detectChanges() })) @@ -671,7 +671,7 @@ describe('MapViewComponent', () => { accessServiceProtocol: 'wms', }, ]) - mdViewFacade.geospatialLinks$.next([]) + mdViewFacade.geoDataLinksWithGeometry$.next([]) dropdownComponent.selectValue.emit(1) tick() fixture.detectChanges() @@ -856,7 +856,7 @@ describe('MapViewComponent', () => { describe('changing the map context', () => { beforeEach(() => { jest.spyOn(component, 'resetSelection') - mdViewFacade.geospatialLinks$.next([]) + mdViewFacade.geoDataLinksWithGeometry$.next([]) mdViewFacade.mapApiLinks$.next([]) }) it('resets selection', () => { diff --git a/libs/feature/record/src/lib/map-view/map-view.component.ts b/libs/feature/record/src/lib/map-view/map-view.component.ts index 60f6cc9f62..565d59eaca 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.ts +++ b/libs/feature/record/src/lib/map-view/map-view.component.ts @@ -32,13 +32,10 @@ import { import { catchError, distinctUntilChanged, - filter, finalize, map, - mergeMap, switchMap, tap, - toArray, } from 'rxjs/operators' import { MdViewFacade } from '../state/mdview.facade' import { DataService } from '@geonetwork-ui/feature/dataviz' @@ -58,10 +55,10 @@ export class MapViewComponent implements OnInit, OnDestroy { compatibleMapLinks$ = combineLatest([ this.mdViewFacade.mapApiLinks$, - this.mdViewFacade.geospatialLinks$, + this.mdViewFacade.geoDataLinksWithGeometry$, ]).pipe( - map(([mapApiLinks, geospatialLinks]) => { - return [...mapApiLinks, ...geospatialLinks] + map(([mapApiLinks, geoDataLinksWithGeometry]) => { + return [...mapApiLinks, ...geoDataLinksWithGeometry] }) ) @@ -107,8 +104,8 @@ export class MapViewComponent implements OnInit, OnDestroy { mapContext$ = this.currentLayers$.pipe( switchMap((layers) => from(this.mapUtils.getLayerExtent(layers[0])).pipe( - catchError((error) => { - console.warn(error) // FIXME: report this to the user somehow + catchError(() => { + this.error = 'The layer has no extent' return of(undefined) }), map( diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 5e963f7fed..5cba2dad36 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -1,16 +1,6 @@ import { Injectable } from '@angular/core' import { select, Store } from '@ngrx/store' -import { - defaultIfEmpty, - distinct, - filter, - map, - mergeMap, - scan, - switchMap, - tap, - toArray, -} from 'rxjs/operators' +import { defaultIfEmpty, filter, map, mergeMap, scan } from 'rxjs/operators' import * as MdViewActions from './mdview.actions' import * as MdViewSelectors from './mdview.selectors' import { LinkClassifierService, LinkUsage } from '@geonetwork-ui/util/shared' @@ -104,7 +94,7 @@ export class MdViewFacade { ) ) - geospatialLinks$ = this.allLinks$.pipe( + geoDataLinksWithGeometry$ = this.allLinks$.pipe( mergeMap((links) => { return from(links) }), From 8837a27f0657bde020905d33bf83e68900510eb0 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 30 May 2024 16:38:04 +0200 Subject: [PATCH 07/10] feat: add fallback for failed wms --- .../map/src/lib/utils/map-utils.service.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/libs/feature/map/src/lib/utils/map-utils.service.ts b/libs/feature/map/src/lib/utils/map-utils.service.ts index 2a73022bb8..969c30ee23 100644 --- a/libs/feature/map/src/lib/utils/map-utils.service.ts +++ b/libs/feature/map/src/lib/utils/map-utils.service.ts @@ -219,16 +219,19 @@ export class MapUtilsService { getRecordExtent(record: Partial): Extent { if (!('spatialExtents' in record)) { return null + } else if (record.spatialExtents.length > 0) { + // transform an array of geojson geometries into a bbox + const totalExtent = record.spatialExtents.reduce( + (prev, curr) => { + const geom = GEOJSON.readGeometry(curr.geometry) + return extend(prev, geom.getExtent()) + }, + [Infinity, Infinity, -Infinity, -Infinity] + ) + return transformExtent(totalExtent, 'EPSG:4326', 'EPSG:3857') + } else { + return null } - // transform an array of geojson geometries into a bbox - const totalExtent = record.spatialExtents.reduce( - (prev, curr) => { - const geom = GEOJSON.readGeometry(curr.geometry) - return extend(prev, geom.getExtent()) - }, - [Infinity, Infinity, -Infinity, -Infinity] - ) - return transformExtent(totalExtent, 'EPSG:4326', 'EPSG:3857') } } From a7dccebbf9c8d9735645af8bd784ce170113349d Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 30 May 2024 16:38:22 +0200 Subject: [PATCH 08/10] feat: don't display ogc in map if ogc service down --- libs/feature/dataviz/src/lib/service/data.service.ts | 4 +++- libs/feature/record/src/lib/state/mdview.facade.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libs/feature/dataviz/src/lib/service/data.service.ts b/libs/feature/dataviz/src/lib/service/data.service.ts index c428511bd6..8380d0503e 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.ts @@ -188,7 +188,9 @@ export class DataService { const endpoint = new OgcApiEndpoint(this.proxy.getProxiedUrl(url)) return await endpoint.featureCollections .then((collections) => { - return endpoint.getCollectionItem(collections[0], '1') + return collections.length + ? endpoint.getCollectionItem(collections[0], '1') + : null }) .catch((error) => { throw new Error(`ogc.unreachable.unknown`) diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 5cba2dad36..135d4be9b3 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -106,8 +106,9 @@ export class MdViewFacade { ) { return from(this.dataService.getItemsFromOgcApi(link.url.href)).pipe( map((collectionRecords: OgcApiRecord) => { - const hasGeometry = collectionRecords.geometry - return hasGeometry ? link : null + return collectionRecords && collectionRecords.geometry + ? link + : null }), defaultIfEmpty(null) ) From 2130c4a3c021d34a51439ff804be38948490c656 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 30 May 2024 16:55:29 +0200 Subject: [PATCH 09/10] feat: improve extent function --- .../map/src/lib/utils/map-utils.service.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/libs/feature/map/src/lib/utils/map-utils.service.ts b/libs/feature/map/src/lib/utils/map-utils.service.ts index 969c30ee23..a99f1ee611 100644 --- a/libs/feature/map/src/lib/utils/map-utils.service.ts +++ b/libs/feature/map/src/lib/utils/map-utils.service.ts @@ -217,21 +217,18 @@ export class MapUtilsService { } getRecordExtent(record: Partial): Extent { - if (!('spatialExtents' in record)) { - return null - } else if (record.spatialExtents.length > 0) { - // transform an array of geojson geometries into a bbox - const totalExtent = record.spatialExtents.reduce( - (prev, curr) => { - const geom = GEOJSON.readGeometry(curr.geometry) - return extend(prev, geom.getExtent()) - }, - [Infinity, Infinity, -Infinity, -Infinity] - ) - return transformExtent(totalExtent, 'EPSG:4326', 'EPSG:3857') - } else { + if (!('spatialExtents' in record) || record.spatialExtents.length === 0) { return null } + // transform an array of geojson geometries into a bbox + const totalExtent = record.spatialExtents.reduce( + (prev, curr) => { + const geom = GEOJSON.readGeometry(curr.geometry) + return extend(prev, geom.getExtent()) + }, + [Infinity, Infinity, -Infinity, -Infinity] + ) + return transformExtent(totalExtent, 'EPSG:4326', 'EPSG:3857') } } From d89a1b04479164de59224adfae07f27b9565120b Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Wed, 5 Jun 2024 15:45:32 +0200 Subject: [PATCH 10/10] feat: tests and startwith falsy --- .../record-metadata.component.ts | 5 +- .../fixtures/src/lib/records.fixtures.ts | 8 ++ .../src/lib/service/data.service.spec.ts | 27 +++++ .../src/lib/utils/map-utils.service.spec.ts | 10 ++ .../src/lib/state/mdview.facade.spec.ts | 114 +++++++++++++++++- .../record/src/lib/state/mdview.facade.ts | 4 +- 6 files changed, 163 insertions(+), 5 deletions(-) diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts index 1edb1ea1eb..602cd1b312 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.ts @@ -3,7 +3,7 @@ import { SourcesService } from '@geonetwork-ui/feature/catalog' import { SearchService } from '@geonetwork-ui/feature/search' import { ErrorType } from '@geonetwork-ui/ui/elements' import { BehaviorSubject, combineLatest } from 'rxjs' -import { filter, map, mergeMap } from 'rxjs/operators' +import { filter, map, mergeMap, startWith } from 'rxjs/operators' import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface' import { Keyword, @@ -26,7 +26,8 @@ export class RecordMetadataComponent { ]).pipe( map(([mapApiLinks, geoDataLinksWithGeometry]) => { return mapApiLinks?.length > 0 || geoDataLinksWithGeometry?.length > 0 - }) + }), + startWith(false) ) displayData$ = combineLatest([ diff --git a/libs/common/fixtures/src/lib/records.fixtures.ts b/libs/common/fixtures/src/lib/records.fixtures.ts index e1893abb07..c193335ee6 100644 --- a/libs/common/fixtures/src/lib/records.fixtures.ts +++ b/libs/common/fixtures/src/lib/records.fixtures.ts @@ -117,6 +117,14 @@ Cette section contient des *caractères internationaux* (ainsi que des "caractè description: 'This WFS service offers direct download capability', identifierInService: 'my:featuretype', }, + { + type: 'service', + url: new URL('https://my-org.net/ogc'), + accessServiceProtocol: 'ogcFeatures', + name: 'my:featuretype', + description: 'This OGC service offers direct download capability', + identifierInService: 'my:featuretype', + }, ], lineage: `This record was edited manually to test the conversion processes diff --git a/libs/feature/dataviz/src/lib/service/data.service.spec.ts b/libs/feature/dataviz/src/lib/service/data.service.spec.ts index c54e86d8d7..a26e84a814 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.spec.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.spec.ts @@ -93,6 +93,13 @@ jest.mock('@camptocamp/ogc-client', () => ({ }) } allCollections = Promise.resolve([{ name: 'collection1' }]) + featureCollections = + this.url.indexOf('error.http') > -1 + ? Promise.reject(new Error()) + : Promise.resolve(['collection1', 'collection2']) + getCollectionItem(collection, id) { + return Promise.resolve('item1') + } }, })) @@ -700,5 +707,25 @@ describe('DataService', () => { ) }) }) + describe('#getItemsFromOgcApi', () => { + describe('calling getItemsFromOgcApi() with a valid URL', () => { + it('returns the first collection item when collections array is not empty', async () => { + const item = await service.getItemsFromOgcApi( + 'https://my.ogc.api/features' + ) + expect(item).toBe('item1') + }) + }) + + describe('calling getItemsFromOgcApi() with an erroneous URL', () => { + it('throws an error', async () => { + try { + await service.getItemsFromOgcApi('http://error.http/ogcapi') + } catch (e) { + expect(e.message).toBe('ogc.unreachable.unknown') + } + }) + }) + }) }) }) diff --git a/libs/feature/map/src/lib/utils/map-utils.service.spec.ts b/libs/feature/map/src/lib/utils/map-utils.service.spec.ts index ceb87a5d20..c73f4a053d 100644 --- a/libs/feature/map/src/lib/utils/map-utils.service.spec.ts +++ b/libs/feature/map/src/lib/utils/map-utils.service.spec.ts @@ -327,6 +327,16 @@ describe('MapUtilsService', () => { }) }) + describe('getRecordExtent', () => { + it('should return null if spatialExtents is not present or is an empty array', () => { + const record1: Partial = {} + const record2: Partial = { spatialExtents: [] } + + expect(service.getRecordExtent(record1)).toBeNull() + expect(service.getRecordExtent(record2)).toBeNull() + }) + }) + describe('#prioritizePageScroll', () => { const interactions = defaults() let dragRotate diff --git a/libs/feature/record/src/lib/state/mdview.facade.spec.ts b/libs/feature/record/src/lib/state/mdview.facade.spec.ts index e83b9dbdd4..24957eea0c 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.spec.ts @@ -1,4 +1,4 @@ -import { TestBed } from '@angular/core/testing' +import { TestBed, fakeAsync, tick } from '@angular/core/testing' import { MockStore, provideMockStore } from '@ngrx/store/testing' import { initialMetadataViewState, @@ -13,6 +13,26 @@ import { } from '@geonetwork-ui/common/fixtures' import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model' import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' +import { TestScheduler } from 'rxjs/testing' + +const newEndpointCall = jest.fn() +let testScheduler: TestScheduler + +jest.mock('@camptocamp/ogc-client', () => ({ + _newEndpointCall: jest.fn(), + OgcApiEndpoint: class { + constructor(private url) { + newEndpointCall(url) // to track endpoint creation + } + featureCollections = + this.url.indexOf('error.http') > -1 + ? Promise.reject(new Error()) + : Promise.resolve(['collection1', 'collection2']) + getCollectionItem(collection, id) { + return Promise.resolve('item1') + } + }, +})) describe('MdViewFacade', () => { let store: MockStore @@ -217,4 +237,96 @@ describe('MdViewFacade', () => { expect(store.scannedActions$).toBeObservable(expected) }) }) + + describe('geoDataLinksWithGeometry$', () => { + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected) + }) + store.setState({ + [METADATA_VIEW_FEATURE_STATE_KEY]: { + ...initialMetadataViewState, + metadata: DATASET_RECORDS[0], + }, + }) + }) + it('should return OGC links that have geometry', fakeAsync(() => { + const values = { + a: [ + { + type: 'download', + url: new URL('http://my-org.net/download/2.geojson'), + mimeType: 'application/geo+json', + name: 'Direct download', + }, + { + type: 'service', + url: new URL('https://my-org.net/wfs'), + accessServiceProtocol: 'wfs', + name: 'my:featuretype', // FIXME: same as identifier otherwise it will be lost in iso... + description: 'This WFS service offers direct download capability', + identifierInService: 'my:featuretype', + }, + { + type: 'service', + url: new URL('https://my-org.net/ogc'), + accessServiceProtocol: 'ogcFeatures', + name: 'my:featuretype', + description: 'This OGC service offers direct download capability', + identifierInService: 'my:featuretype', + }, + ], + } + jest.spyOn(facade.dataService, 'getItemsFromOgcApi').mockResolvedValue({ + id: '123', + type: 'Feature', + time: null, + properties: { + type: '', + title: '', + }, + links: [], + geometry: { type: 'MultiPolygon', coordinates: [] }, + }) + let result + facade.geoDataLinksWithGeometry$.subscribe((v) => (result = v)) + tick() + expect(result).toEqual(values.a) + })) + it('should not return OGC links that do not have geometry', fakeAsync(() => { + const values = { + a: [ + { + type: 'download', + url: new URL('http://my-org.net/download/2.geojson'), + mimeType: 'application/geo+json', + name: 'Direct download', + }, + { + type: 'service', + url: new URL('https://my-org.net/wfs'), + accessServiceProtocol: 'wfs', + name: 'my:featuretype', // FIXME: same as identifier otherwise it will be lost in iso... + description: 'This WFS service offers direct download capability', + identifierInService: 'my:featuretype', + }, + ], + } + jest.spyOn(facade.dataService, 'getItemsFromOgcApi').mockResolvedValue({ + id: '123', + type: 'Feature', + time: null, + properties: { + type: '', + title: '', + }, + links: [], + geometry: null, + }) + let result + facade.geoDataLinksWithGeometry$.subscribe((v) => (result = v)) + tick() + expect(result).toEqual(values.a) + })) + }) }) diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 135d4be9b3..814b6a7dcd 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -24,9 +24,9 @@ import { DataService } from '@geonetwork-ui/feature/dataviz' export class MdViewFacade { constructor( private store: Store, - private linkClassifier: LinkClassifierService, + public linkClassifier: LinkClassifierService, private avatarService: AvatarServiceInterface, - private dataService: DataService + public dataService: DataService ) {} isPresent$ = this.store.pipe(