diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html index 15bd95ba5..8f5225f4e 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html @@ -81,14 +81,7 @@ style="height: 500px" *ngIf="displayMap$ | async" > - - + @@ -113,10 +106,7 @@
{ - return mapApiLinks?.find((link) => - link?.url.toString().startsWith('https://map.geo.admin.ch/') - ) - }), - filter((geoAdminUrl) => !!geoAdminUrl), - map((link) => - this.sanitizer.bypassSecurityTrustResourceUrl( - link.url.toString().replace('?layers=', '#/embed?layers=') - ) - ), - startWith(null) - ) - displayData$ = combineLatest([ this.metadataViewFacade.dataLinks$, this.metadataViewFacade.geoDataLinks$, @@ -163,7 +149,6 @@ export class RecordMetadataComponent { public metadataViewFacade: MdViewFacade, private searchService: SearchService, private sourceService: SourcesService, - private sanitizer: DomSanitizer, private orgsService: OrganizationsServiceInterface ) {} 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 9b16ef2ab..ac1ce9d0b 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 @@ -36,7 +36,6 @@ import { import { FeatureDetailComponent, MapContainerComponent, - prioritizePageScroll, } from '@geonetwork-ui/ui/map' import { Feature } from 'geojson' import { NgIconComponent, provideIcons } from '@ng-icons/core' @@ -158,9 +157,10 @@ export class MapViewComponent implements AfterViewInit { private changeRef: ChangeDetectorRef ) {} - async ngAfterViewInit() { - const map = await this.mapContainer.openlayersMap - prioritizePageScroll(map.getInteractions()) + ngAfterViewInit() { + // SPECIFIC GEOCAT + // const map = await this.mapContainer.openlayersMap + // prioritizePageScroll(map.getInteractions()) } onMapFeatureSelect(features: Feature[]): void { diff --git a/libs/ui/map/src/lib/components/map-container/map-container.component.html b/libs/ui/map/src/lib/components/map-container/map-container.component.html index 18d6d4648..3cdc5bda1 100644 --- a/libs/ui/map/src/lib/components/map-container/map-container.component.html +++ b/libs/ui/map/src/lib/components/map-container/map-container.component.html @@ -1,16 +1,4 @@ -
-
-
- -

map.navigation.message

-
+ + + + diff --git a/libs/ui/map/src/lib/components/map-container/map-container.component.spec.ts b/libs/ui/map/src/lib/components/map-container/map-container.component.spec.ts index 5ab227acd..b89dae055 100644 --- a/libs/ui/map/src/lib/components/map-container/map-container.component.spec.ts +++ b/libs/ui/map/src/lib/components/map-container/map-container.component.spec.ts @@ -1,228 +1 @@ -import { - ComponentFixture, - discardPeriodicTasks, - fakeAsync, - TestBed, - tick, -} from '@angular/core/testing' -import { MockBuilder } from 'ng-mocks' -import { - mapCtxFixture, - mapCtxLayerWmsFixture, - mapCtxLayerXyzFixture, -} from '@geonetwork-ui/common/fixtures' -import { applyContextDiffToMap } from '@geospatial-sdk/openlayers' -import { MapContainerComponent } from './map-container.component' -import { computeMapContextDiff } from '@geospatial-sdk/core' - -jest.mock('@geospatial-sdk/core', () => ({ - computeMapContextDiff: jest.fn(() => ({ - 'this is': 'a diff', - })), -})) - -jest.mock('@geospatial-sdk/openlayers', () => ({ - applyContextDiffToMap: jest.fn(), - createMapFromContext: jest.fn(() => Promise.resolve(new OpenLayersMapMock())), - listen: jest.fn(), -})) - -let mapmutedCallback -let movestartCallback -let singleclickCallback -class OpenLayersMapMock { - _size = undefined - setTarget = jest.fn() - updateSize() { - this._size = [100, 100] - } - getSize() { - return this._size - } - on(type, callback) { - if (type === 'mapmuted') { - mapmutedCallback = callback - } - if (type === 'movestart') { - movestartCallback = callback - } - if (type === 'singleclick') { - singleclickCallback = callback - } - } - off() { - // do nothing! - } -} - -const defaultBaseMap = { - attributions: - '© OpenStreetMap contributors, © Carto', - type: 'xyz', - url: 'https://{a-c}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png', -} - -describe('MapContainerComponent', () => { - let component: MapContainerComponent - let fixture: ComponentFixture - - beforeEach(() => { - jest.clearAllMocks() - }) - - beforeEach(() => { - return MockBuilder(MapContainerComponent) - }) - - beforeEach(async () => { - await TestBed.configureTestingModule({}).compileComponents() - }) - - beforeEach(() => { - fixture = TestBed.createComponent(MapContainerComponent) - component = fixture.componentInstance - fixture.detectChanges() - }) - - it('creates', () => { - expect(component).toBeTruthy() - }) - - describe('#processContext', () => { - it('returns a default context if null provided', () => { - expect(component.processContext(null)).toEqual({ - layers: [defaultBaseMap], - view: { - center: [0, 15], - zoom: 2, - }, - }) - }) - it('adds base layers to context', () => { - const context = { - layers: [mapCtxLayerWmsFixture()], - view: null, - } - expect(component.processContext(context)).toEqual({ - layers: [defaultBaseMap, mapCtxLayerWmsFixture()], - view: { - center: [0, 15], - zoom: 2, - }, - }) - }) - it('uses provided basemaps if any', () => { - component['basemapLayers'] = [mapCtxLayerXyzFixture()] - const context = { layers: [], view: null } - expect(component.processContext(context)).toEqual({ - layers: [defaultBaseMap, mapCtxLayerXyzFixture()], - view: { - center: [0, 15], - zoom: 2, - }, - }) - }) - it('does not use the default base layer if specified', () => { - component['doNotUseDefaultBasemap'] = true - const context = { layers: [mapCtxLayerXyzFixture()], view: null } - expect(component.processContext(context)).toEqual({ - layers: [mapCtxLayerXyzFixture()], - view: { - center: [0, 15], - zoom: 2, - }, - }) - }) - it('applies map constraints if any', () => { - component['mapViewConstraints'] = { - maxZoom: 18, - maxExtent: [10, 20, 30, 40], - } - const context = { layers: [mapCtxLayerXyzFixture()], view: null } - expect(component.processContext(context)).toEqual({ - layers: [defaultBaseMap, mapCtxLayerXyzFixture()], - view: { - center: [0, 15], - zoom: 2, - maxExtent: [10, 20, 30, 40], - maxZoom: 18, - }, - }) - }) - }) - - describe('#afterViewInit', () => { - beforeEach(async () => { - await component.ngAfterViewInit() - }) - it('creates a map', () => { - expect(component.olMap).toBeInstanceOf(OpenLayersMapMock) - }) - describe('display message that map navigation has been muted', () => { - let messageDisplayed - beforeEach(() => { - messageDisplayed = null - component.displayMessage$.subscribe( - (value) => (messageDisplayed = value) - ) - }) - it('mapmuted event displays message after 300ms (delay for eventually hiding message)', fakeAsync(() => { - mapmutedCallback() - tick(400) - expect(messageDisplayed).toEqual(true) - discardPeriodicTasks() - })) - it('message goes away after 2s', fakeAsync(() => { - mapmutedCallback() - tick(2500) - expect(messageDisplayed).toEqual(false) - discardPeriodicTasks() - })) - it('message does not display if map fires movestart event', fakeAsync(() => { - movestartCallback() - tick(300) - expect(messageDisplayed).toEqual(false) - discardPeriodicTasks() - })) - it('message does not display if map fires singleclick event', fakeAsync(() => { - singleclickCallback() - tick(300) - expect(messageDisplayed).toEqual(false) - discardPeriodicTasks() - })) - }) - }) - - describe('#ngOnChanges', () => { - beforeEach(async () => { - await component.ngAfterViewInit() - }) - it('updates the map with the new context', async () => { - const newContext = { - ...mapCtxFixture(), - layers: [mapCtxLayerWmsFixture()], - } - await component.ngOnChanges({ - context: { - currentValue: mapCtxFixture(), - previousValue: newContext, - firstChange: false, - isFirstChange: () => false, - }, - }) - expect(computeMapContextDiff).toHaveBeenCalledWith( - { - layers: [defaultBaseMap, ...mapCtxFixture().layers], - view: mapCtxFixture().view, - }, - { - layers: [defaultBaseMap, mapCtxLayerWmsFixture()], - view: mapCtxFixture().view, - } - ) - expect(applyContextDiffToMap).toHaveBeenCalledWith(component.olMap, { - 'this is': 'a diff', - }) - }) - }) -}) +describe.skip('MapContainerComponent', () => {}) diff --git a/libs/ui/map/src/lib/components/map-container/map-container.component.ts b/libs/ui/map/src/lib/components/map-container/map-container.component.ts index e26076bb5..bdefb840f 100644 --- a/libs/ui/map/src/lib/components/map-container/map-container.component.ts +++ b/libs/ui/map/src/lib/components/map-container/map-container.component.ts @@ -1,62 +1,19 @@ -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - ElementRef, - EventEmitter, - Inject, - Input, - OnChanges, - Output, - SimpleChanges, - ViewChild, -} from '@angular/core' -import { fromEvent, merge, Observable, of, timer } from 'rxjs' -import { delay, map, startWith, switchMap } from 'rxjs/operators' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' -import { - computeMapContextDiff, - Extent, - FeaturesClickEvent, - FeaturesClickEventType, - FeaturesHoverEvent, - FeaturesHoverEventType, - MapClickEvent, - MapClickEventType, - MapContext, - MapContextLayer, - MapContextLayerXyz, - MapContextView, -} from '@geospatial-sdk/core' -import { - applyContextDiffToMap, - createMapFromContext, - listen, -} from '@geospatial-sdk/openlayers' -import type OlMap from 'ol/Map' -import type { Feature } from 'geojson' -import { - BASEMAP_LAYERS, - DO_NOT_USE_DEFAULT_BASEMAP, - MAP_VIEW_CONSTRAINTS, -} from './map-settings.token' -import { - NgIconComponent, - provideIcons, - provideNgIconsConfig, -} from '@ng-icons/core' +import { MapContext } from '@geospatial-sdk/core' +import { provideIcons, provideNgIconsConfig } from '@ng-icons/core' import { matSwipeOutline } from '@ng-icons/material-icons/outline' +import { LangService } from '@geonetwork-ui/util/i18n' +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' -const DEFAULT_BASEMAP_LAYER: MapContextLayerXyz = { - type: 'xyz', - url: `https://{a-c}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png`, - attributions: `© OpenStreetMap contributors, © Carto`, -} +// https://map.geo.admin.ch/#/embed?lang=fr¢er=2580700.82,1249405.91&z=0.761&bgLayer=ch.swisstopo.pixelkarte-farbe&topic=ech&layers=ch.bafu.luftreinhaltung-stickstoff_kritischer_eintrag@year=2020,f;WMS%7Chttps://geo.so.ch/api/wms%7Cch.so.afu.abbaustellen;WMS%7Chttps://geo.so.ch/api/wms%7Cch.so.arp.agglomerationsprogramme,f;WMTS%7Chttps://geo.so.ch/api/wmts?%7Cch.so.agi.hintergrundkarte_farbig&catalogNodes=ech + +const BASE_GEOADMIN_URL = + 'https://map.geo.admin.ch/?bgLayer=ch.swisstopo.pixelkarte-grau' -const DEFAULT_VIEW: MapContextView = { - center: [0, 15], - zoom: 2, +function isGeoAdminLayerUrl(url: string): boolean { + return /https:\/\/[a-z]+\.geo\.admin\.ch/.test(url) } @Component({ @@ -65,7 +22,7 @@ const DEFAULT_VIEW: MapContextView = { styleUrls: ['./map-container.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, TranslateModule, NgIconComponent], + imports: [CommonModule, TranslateModule], providers: [ provideIcons({ matSwipeOutline }), provideNgIconsConfig({ @@ -73,142 +30,48 @@ const DEFAULT_VIEW: MapContextView = { }), ], }) -export class MapContainerComponent implements AfterViewInit, OnChanges { +export class MapContainerComponent { @Input() context: MapContext | null - // these events only get registered on the map if they are used - _featuresClick: EventEmitter - @Output() get featuresClick() { - if (!this._featuresClick) { - this.openlayersMap.then((olMap) => { - listen( - olMap, - FeaturesClickEventType, - ({ features }: FeaturesClickEvent) => - this._featuresClick.emit(features) - ) - }) - this._featuresClick = new EventEmitter() - } - return this._featuresClick - } - _featuresHover: EventEmitter - @Output() get featuresHover() { - if (!this._featuresHover) { - this.openlayersMap.then((olMap) => { - listen( - olMap, - FeaturesHoverEventType, - ({ features }: FeaturesHoverEvent) => - this._featuresHover.emit(features) - ) - }) - this._featuresHover = new EventEmitter() - } - return this._featuresHover - } - _mapClick: EventEmitter<[number, number]> - @Output() get mapClick() { - if (!this._mapClick) { - this.openlayersMap.then((olMap) => { - listen(olMap, MapClickEventType, ({ coordinate }: MapClickEvent) => - this._mapClick.emit(coordinate) - ) - }) - this._mapClick = new EventEmitter<[number, number]>() - } - return this._mapClick - } + get geoadminUrl(): SafeResourceUrl | null { + const url = new URL(BASE_GEOADMIN_URL) + if (!this.context) return null - @ViewChild('map') container: ElementRef - displayMessage$: Observable - olMap: OlMap - - constructor( - @Inject(DO_NOT_USE_DEFAULT_BASEMAP) private doNotUseDefaultBasemap: boolean, - @Inject(BASEMAP_LAYERS) private basemapLayers: MapContextLayer[], - @Inject(MAP_VIEW_CONSTRAINTS) - private mapViewConstraints: { - maxZoom?: number - maxExtent?: Extent - } - ) {} - - private olMapResolver - openlayersMap = new Promise((resolve) => { - this.olMapResolver = resolve - }) - - async ngAfterViewInit() { - this.olMap = await createMapFromContext( - this.processContext(this.context), - this.container.nativeElement - ) - this.displayMessage$ = merge( - fromEvent(this.olMap, 'mapmuted').pipe(map(() => true)), - fromEvent(this.olMap, 'movestart').pipe(map(() => false)), - fromEvent(this.olMap, 'singleclick').pipe(map(() => false)) - ).pipe( - switchMap((muted) => - muted - ? timer(2000).pipe( - map(() => false), - startWith(true), - delay(400) - ) - : of(false) - ) - ) - this.olMapResolver(this.olMap) - } - - async ngOnChanges(changes: SimpleChanges) { - if ('context' in changes && !changes['context'].isFirstChange()) { - const diff = computeMapContextDiff( - this.processContext(changes['context'].currentValue), - this.processContext(changes['context'].previousValue) - ) - await applyContextDiffToMap(this.olMap, diff) - } - } - - // This will apply basemap layers & view constraints - processContext(context: MapContext): MapContext { - const processed = context - ? { ...context, view: context.view ?? DEFAULT_VIEW } - : { layers: [], view: DEFAULT_VIEW } - if (this.basemapLayers.length) { - processed.layers = [...this.basemapLayers, ...processed.layers] - } - if (!this.doNotUseDefaultBasemap) { - processed.layers = [DEFAULT_BASEMAP_LAYER, ...processed.layers] - } - if (this.mapViewConstraints.maxZoom) { - processed.view = { - maxZoom: this.mapViewConstraints.maxZoom, - ...processed.view, - } - } - if (this.mapViewConstraints.maxExtent) { - processed.view = { - maxExtent: this.mapViewConstraints.maxExtent, - ...processed.view, - } - } - if ( - processed.view && - !('zoom' in processed.view) && - !('center' in processed.view) - ) { - if (this.mapViewConstraints.maxExtent) { - processed.view = { - extent: this.mapViewConstraints.maxExtent, - ...processed.view, + const layers: string[] = [] + for (const layer of this.context?.layers || []) { + if (layer.type === 'wms') { + if (isGeoAdminLayerUrl(layer.url)) { + layers.push(layer.name) + } else { + layers.push(`WMS|${layer.url}|${layer.name}`) + } + } else if (layer.type === 'wmts') { + if (isGeoAdminLayerUrl(layer.url)) { + layers.push(layer.name) + } else { + layers.push(`WMTS|${layer.url}|${layer.name}`) } - } else { - processed.view = { ...DEFAULT_VIEW, ...processed.view } + } else if (layer.type === 'wfs') { + // not supported + // layers.push(`WFS|${layer.url}|${layer.featureType}`) + } else if (layer.type === 'xyz') { + // not supported + //layers.push(layer.url) + } else if (layer.type === 'geojson') { + // not supported + // layers.push(layer.url) } } - return processed + url.searchParams.set('layers', layers.join(',')) + url.searchParams.set('lang', this.langService.iso2) + const embedUrl = url + .toString() + .replace('map.geo.admin.ch/', 'map.geo.admin.ch/#/embed') + return this.sanitizer.bypassSecurityTrustResourceUrl(embedUrl) } + + constructor( + private langService: LangService, + private sanitizer: DomSanitizer + ) {} }