From 934413a6602b3fc4b026bcda336b77cc9b9ad282 Mon Sep 17 00:00:00 2001 From: Florent Gravin Date: Tue, 19 Nov 2024 14:07:47 +0100 Subject: [PATCH 01/20] geocat-gps: replace built-in mapviewer with geoadmin.ch embedded iframer show geoadmin iframe in map-container component --- .../src/lib/map-view/map-view.component.html | 4 +- .../src/lib/map-view/map-view.component.ts | 8 +- .../map-container.component.html | 20 +- .../map-container.component.spec.ts | 229 +---------------- .../map-container/map-container.component.ts | 235 ++++-------------- 5 files changed, 62 insertions(+), 434 deletions(-) diff --git a/libs/feature/record/src/lib/map-view/map-view.component.html b/libs/feature/record/src/lib/map-view/map-view.component.html index 899f47e29..ccef483f1 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.html +++ b/libs/feature/record/src/lib/map-view/map-view.component.html @@ -42,7 +42,7 @@
@@ -71,7 +71,7 @@ *ngIf="!showLegend && legendExists && !selection" type="outline" (buttonClick)="toggleLegend()" - extraClass="absolute top-[1em] right-[1em] rounded p-1 text-xs bg-white" + extraClass="absolute top-[60px] right-[1em] rounded p-1 text-xs bg-white" > Legend 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 afc5905f8..a1dd6f599 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 @@ -193,9 +193,11 @@ export class MapViewComponent implements AfterViewInit { private changeRef: ChangeDetectorRef ) {} - async ngAfterViewInit() { - const map = await this.mapContainer.openlayersMap - prioritizePageScroll(map.getInteractions()) + // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method + 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..abb7fb7f1 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,3 @@ -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', () => { + // empty }) 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 + ) {} } From 82640a7f0c2dc5f732c6046923175328d0e9db80 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 17 Feb 2025 13:23:54 +0100 Subject: [PATCH 02/20] Revert "geocat-gps: replace built-in mapviewer with geoadmin.ch embedded iframer" This reverts commit 998c4d6396140cb941c3cb4edc878900d946a5bc. --- .../src/lib/map-view/map-view.component.html | 4 +- .../src/lib/map-view/map-view.component.ts | 8 +- .../map-container.component.html | 20 +- .../map-container.component.spec.ts | 229 ++++++++++++++++- .../map-container/map-container.component.ts | 235 ++++++++++++++---- 5 files changed, 434 insertions(+), 62 deletions(-) diff --git a/libs/feature/record/src/lib/map-view/map-view.component.html b/libs/feature/record/src/lib/map-view/map-view.component.html index ccef483f1..899f47e29 100644 --- a/libs/feature/record/src/lib/map-view/map-view.component.html +++ b/libs/feature/record/src/lib/map-view/map-view.component.html @@ -42,7 +42,7 @@
@@ -71,7 +71,7 @@ *ngIf="!showLegend && legendExists && !selection" type="outline" (buttonClick)="toggleLegend()" - extraClass="absolute top-[60px] right-[1em] rounded p-1 text-xs bg-white" + extraClass="absolute top-[1em] right-[1em] rounded p-1 text-xs bg-white" > Legend 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 a1dd6f599..afc5905f8 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 @@ -193,11 +193,9 @@ export class MapViewComponent implements AfterViewInit { private changeRef: ChangeDetectorRef ) {} - // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method - ngAfterViewInit() { - // SPECIFIC GEOCAT - // const map = await this.mapContainer.openlayersMap - // prioritizePageScroll(map.getInteractions()) + async ngAfterViewInit() { + 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 3cdc5bda1..18d6d4648 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,4 +1,16 @@ - - - - +
+
+
+ +

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 abb7fb7f1..5ab227acd 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,3 +1,228 @@ -describe.skip('MapContainerComponent', () => { - // empty +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', + }) + }) + }) }) 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 bdefb840f..e26076bb5 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,19 +1,62 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +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 { CommonModule } from '@angular/common' import { TranslateModule } from '@ngx-translate/core' -import { MapContext } from '@geospatial-sdk/core' -import { provideIcons, provideNgIconsConfig } from '@ng-icons/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 { matSwipeOutline } from '@ng-icons/material-icons/outline' -import { LangService } from '@geonetwork-ui/util/i18n' -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser' -// 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_BASEMAP_LAYER: MapContextLayerXyz = { + type: 'xyz', + url: `https://{a-c}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png`, + attributions: `© OpenStreetMap contributors, © Carto`, +} -function isGeoAdminLayerUrl(url: string): boolean { - return /https:\/\/[a-z]+\.geo\.admin\.ch/.test(url) +const DEFAULT_VIEW: MapContextView = { + center: [0, 15], + zoom: 2, } @Component({ @@ -22,7 +65,7 @@ function isGeoAdminLayerUrl(url: string): boolean { styleUrls: ['./map-container.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [CommonModule, TranslateModule], + imports: [CommonModule, TranslateModule, NgIconComponent], providers: [ provideIcons({ matSwipeOutline }), provideNgIconsConfig({ @@ -30,48 +73,142 @@ function isGeoAdminLayerUrl(url: string): boolean { }), ], }) -export class MapContainerComponent { +export class MapContainerComponent implements AfterViewInit, OnChanges { @Input() context: MapContext | null - get geoadminUrl(): SafeResourceUrl | null { - const url = new URL(BASE_GEOADMIN_URL) - if (!this.context) return null - - 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 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) - } + // 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() } - 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) + 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 } + @ViewChild('map') container: ElementRef + displayMessage$: Observable + olMap: OlMap + constructor( - private langService: LangService, - private sanitizer: DomSanitizer + @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, + } + } else { + processed.view = { ...DEFAULT_VIEW, ...processed.view } + } + } + return processed + } } From 8d1ae2efd369b70941f6121c7cbe0fcb082e2afd Mon Sep 17 00:00:00 2001 From: Florent Gravin Date: Tue, 19 Nov 2024 14:07:47 +0100 Subject: [PATCH 03/20] gpf - use geoadmin.ch iframe for records which refers a map.geoadmin.ch link --- .../src/app/record/record-metadata/record-metadata.component.ts | 2 -- 1 file changed, 2 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 c9203182e..02d22ce19 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 @@ -5,7 +5,6 @@ import { ErrorComponent, ErrorType, ImageOverlayPreviewComponent, - MetadataCatalogComponent, MetadataContactComponent, MetadataInfoComponent, MetadataQualityComponent, @@ -46,7 +45,6 @@ import { RecordDataPreviewComponent } from '../record-data-preview/record-data-p MetadataInfoComponent, MetadataContactComponent, MetadataQualityComponent, - MetadataCatalogComponent, RecordRelatedRecordsComponent, TranslateModule, RecordDataPreviewComponent, From 123e4e848a55c65834929dae71658d2b26377720 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Fri, 29 Nov 2024 11:18:52 +0100 Subject: [PATCH 04/20] show geoadmin iframe in map-container component --- .../record-metadata.component.ts | 2 + .../src/lib/map-view/map-view.component.ts | 8 +- .../map-container.component.html | 20 +- .../map-container.component.spec.ts | 229 +---------------- .../map-container/map-container.component.ts | 235 ++++-------------- 5 files changed, 62 insertions(+), 432 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 02d22ce19..c9203182e 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 @@ -5,6 +5,7 @@ import { ErrorComponent, ErrorType, ImageOverlayPreviewComponent, + MetadataCatalogComponent, MetadataContactComponent, MetadataInfoComponent, MetadataQualityComponent, @@ -45,6 +46,7 @@ import { RecordDataPreviewComponent } from '../record-data-preview/record-data-p MetadataInfoComponent, MetadataContactComponent, MetadataQualityComponent, + MetadataCatalogComponent, RecordRelatedRecordsComponent, TranslateModule, RecordDataPreviewComponent, 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 afc5905f8..a1dd6f599 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 @@ -193,9 +193,11 @@ export class MapViewComponent implements AfterViewInit { private changeRef: ChangeDetectorRef ) {} - async ngAfterViewInit() { - const map = await this.mapContainer.openlayersMap - prioritizePageScroll(map.getInteractions()) + // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method + 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..abb7fb7f1 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,3 @@ -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', () => { + // empty }) 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 + ) {} } From 4f47550f462e7ce34542b801674a9b613248efb6 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Fri, 29 Nov 2024 12:16:10 +0100 Subject: [PATCH 05/20] publish webcomponents on PR --- .github/workflows/webcomponents.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/webcomponents.yml b/.github/workflows/webcomponents.yml index 551c651b3..796848ec5 100644 --- a/.github/workflows/webcomponents.yml +++ b/.github/workflows/webcomponents.yml @@ -1,11 +1,15 @@ name: Web Components -run-name: 🧩 Build Web Components for ${{ github.event_name == 'issue_comment' && 'PR' || (github.event_name == 'release' && '🏷' || '🌱') }} ${{github.event_name == 'issue_comment' && github.event.issue.number || github.ref_name}} +run-name: 🧩 Build Web Components for ${{ github.event_name == 'issue_comment' && 'PR' || (github.event_name == 'release' && '🏷' || '🌱') }} ${{github.event_name == 'issue_comment' && github.event.issue.number || github.head_ref || github.ref_name}} # This workflow runs whenever a commit is pushed on main or a release is published on: push: branches: - - geocat + - main + tags: + - 'v*.*.*' + pull_request: + types: [opened, synchronize, ready_for_review] release: types: [published] @@ -26,7 +30,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - ref: ${{ needs.checks.outputs.ref }} + ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false fetch-depth: 0 @@ -54,10 +58,10 @@ jobs: tag: ${{ github.ref }} overwrite: true - - name: Publish web component to ${{ env.PUBLISH_BRANCH }} branch + - name: Publish web component to ${{ env.PUBLISH_BRANCH }}-${{ github.head_ref || github.ref_name }} branch uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} force_orphan: true publish_dir: ./wc-dist - publish_branch: ${{ env.PUBLISH_BRANCH }} + publish_branch: ${{ env.PUBLISH_BRANCH }}-${{ github.head_ref || github.ref_name }} From 3dc848b99af3478d766bcbf663fc0808c5f802f5 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Fri, 6 Dec 2024 14:33:53 +0100 Subject: [PATCH 06/20] geocat: define global props when importing the web components module --- .../src/app/webcomponents.module.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/webcomponents/src/app/webcomponents.module.ts b/apps/webcomponents/src/app/webcomponents.module.ts index 9c8511142..294d23b8d 100644 --- a/apps/webcomponents/src/app/webcomponents.module.ts +++ b/apps/webcomponents/src/app/webcomponents.module.ts @@ -1,6 +1,11 @@ import { OverlayContainer } from '@angular/cdk/overlay' import { CommonModule } from '@angular/common' -import { CUSTOM_ELEMENTS_SCHEMA, Injector, NgModule } from '@angular/core' +import { + CUSTOM_ELEMENTS_SCHEMA, + inject, + Injector, + NgModule, +} from '@angular/core' import { createCustomElement } from '@angular/elements' import { BrowserModule } from '@angular/platform-browser' import { Configuration } from '@geonetwork-ui/data-access/gn4' @@ -39,6 +44,8 @@ import { provideGn4 } from '@geonetwork-ui/api/repository' import { GnFigureDatasetsComponent } from './components/gn-figure-datasets/gn-figure-datasets.component' import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' import { GnDatasetViewMapComponent } from './components/gn-dataset-view-map/gn-dataset-view-map.component' +import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ [GnFacetsComponent, 'gn-facets'], @@ -119,6 +126,14 @@ export class WebcomponentsModule { customElements.define(ceTagName, customElement) } }) + + // define global props + const recordsRepository = inject(RecordsRepositoryInterface) + const platformService = inject(PlatformServiceInterface) + window['GNUI'] = { + recordsRepository, + platformService, + } } // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method, @angular-eslint/use-lifecycle-interface, @typescript-eslint/no-empty-function From 83a4825daa08a085c501baa60249a70cfc426192 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Fri, 29 Nov 2024 12:14:23 +0100 Subject: [PATCH 07/20] add geoadmin-demo app --- .prettierignore | 3 +- apps/geoadmin-demo/.eslintrc.json | 36 +++++++++ apps/geoadmin-demo/jest.config.ts | 22 ++++++ apps/geoadmin-demo/project.json | 101 ++++++++++++++++++++++++ apps/geoadmin-demo/src/assets/.gitkeep | 0 apps/geoadmin-demo/src/favicon.ico | Bin 0 -> 15086 bytes apps/geoadmin-demo/src/index.html | 43 ++++++++++ apps/geoadmin-demo/src/main.ts | 1 + apps/geoadmin-demo/src/styles.css | 34 ++++++++ apps/geoadmin-demo/src/test-setup.ts | 8 ++ apps/geoadmin-demo/tsconfig.app.json | 10 +++ apps/geoadmin-demo/tsconfig.editor.json | 7 ++ apps/geoadmin-demo/tsconfig.json | 32 ++++++++ apps/geoadmin-demo/tsconfig.spec.json | 16 ++++ 14 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 apps/geoadmin-demo/.eslintrc.json create mode 100644 apps/geoadmin-demo/jest.config.ts create mode 100644 apps/geoadmin-demo/project.json create mode 100644 apps/geoadmin-demo/src/assets/.gitkeep create mode 100644 apps/geoadmin-demo/src/favicon.ico create mode 100644 apps/geoadmin-demo/src/index.html create mode 100644 apps/geoadmin-demo/src/main.ts create mode 100644 apps/geoadmin-demo/src/styles.css create mode 100644 apps/geoadmin-demo/src/test-setup.ts create mode 100644 apps/geoadmin-demo/tsconfig.app.json create mode 100644 apps/geoadmin-demo/tsconfig.editor.json create mode 100644 apps/geoadmin-demo/tsconfig.json create mode 100644 apps/geoadmin-demo/tsconfig.spec.json diff --git a/.prettierignore b/.prettierignore index 1df66de43..94d3cd2b5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,4 +10,5 @@ /.nx /.nx/cache -/.nx/workspace-data \ No newline at end of file +/.nx/workspace-data +.angular diff --git a/apps/geoadmin-demo/.eslintrc.json b/apps/geoadmin-demo/.eslintrc.json new file mode 100644 index 000000000..c9b6b193a --- /dev/null +++ b/apps/geoadmin-demo/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "geoadmin", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "geoadmin-root", + "style": "kebab-case" + } + ] + }, + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ] + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/apps/geoadmin-demo/jest.config.ts b/apps/geoadmin-demo/jest.config.ts new file mode 100644 index 000000000..c6a603e7d --- /dev/null +++ b/apps/geoadmin-demo/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'geoadmin-demo', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../coverage/apps/geoadmin-demo', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +} diff --git a/apps/geoadmin-demo/project.json b/apps/geoadmin-demo/project.json new file mode 100644 index 000000000..9a304ee1a --- /dev/null +++ b/apps/geoadmin-demo/project.json @@ -0,0 +1,101 @@ +{ + "name": "geoadmin-demo", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "prefix": "geonetwork-ui", + "sourceRoot": "apps/geoadmin-demo/src", + "tags": [], + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:browser", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/geoadmin-demo", + "index": "apps/geoadmin-demo/src/index.html", + "main": "apps/geoadmin-demo/src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "apps/geoadmin-demo/tsconfig.app.json", + "assets": [ + "apps/geoadmin-demo/src/favicon.ico", + "apps/geoadmin-demo/src/assets" + ], + "styles": ["apps/geoadmin-demo/src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "geoadmin-demo:build:production" + }, + "development": { + "browserTarget": "geoadmin-demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "geoadmin-demo:build" + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "apps/geoadmin-demo/**/*.ts", + "apps/geoadmin-demo/**/*.html" + ] + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "apps/geoadmin-demo/jest.config.ts", + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } + }, + "serve-static": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "geoadmin-demo:build" + } + } + } +} diff --git a/apps/geoadmin-demo/src/assets/.gitkeep b/apps/geoadmin-demo/src/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/geoadmin-demo/src/favicon.ico b/apps/geoadmin-demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 zcmeI332;U^%p|z7g|#(P)qFEA@4f!_@qOK2 z_lJl}!lhL!VT_U|uN7%8B2iKH??xhDa;*`g{yjTFWHvXn;2s{4R7kH|pKGdy(7z!K zgftM+Ku7~24TLlh(!g)gz|foI94G^t2^IO$uvX$3(OR0<_5L2sB)lMAMy|+`xodJ{ z_Uh_1m)~h?a;2W{dmhM;u!YGo=)OdmId_B<%^V^{ovI@y`7^g1_V9G}*f# zNzAtvou}I!W1#{M^@ROc(BZ! z+F!!_aR&Px3_reO(EW+TwlW~tv*2zr?iP7(d~a~yA|@*a89IUke+c472NXM0wiX{- zl`UrZC^1XYyf%1u)-Y)jj9;MZ!SLfd2Hl?o|80Su%Z?To_=^g_Jt0oa#CT*tjx>BI z16wec&AOWNK<#i0Qd=1O$fymLRoUR*%;h@*@v7}wApDl^w*h}!sYq%kw+DKDY)@&A z@9$ULEB3qkR#85`lb8#WZw=@})#kQig9oqy^I$dj&k4jU&^2(M3q{n1AKeGUKPFbr z1^<)aH;VsG@J|B&l>UtU#Ejv3GIqERzYgL@UOAWtW<{p#zy`WyJgpCy8$c_e%wYJL zyGHRRx38)HyjU3y{-4z6)pzb>&Q1pR)B&u01F-|&Gx4EZWK$nkUkOI|(D4UHOXg_- zw{OBf!oWQUn)Pe(=f=nt=zkmdjpO^o8ZZ9o_|4tW1ni+Un9iCW47*-ut$KQOww!;u z`0q)$s6IZO!~9$e_P9X!hqLxu`fpcL|2f^I5d4*a@Dq28;@2271v_N+5HqYZ>x;&O z05*7JT)mUe&%S0@UD)@&8SmQrMtsDfZT;fkdA!r(S=}Oz>iP)w=W508=Rc#nNn7ym z1;42c|8($ALY8#a({%1#IXbWn9-Y|0eDY$_L&j{63?{?AH{);EzcqfydD$@-B`Y3<%IIj7S7rK_N}je^=dEk%JQ4c z!tBdTPE3Tse;oYF>cnrapWq*o)m47X1`~6@(!Y29#>-#8zm&LXrXa(3=7Z)ElaQqj z-#0JJy3Fi(C#Rx(`=VXtJ63E2_bZGCz+QRa{W0e2(m3sI?LOcUBx)~^YCqZ{XEPX)C>G>U4tfqeH8L(3|pQR*zbL1 zT9e~4Tb5p9_G}$y4t`i*4t_Mr9QYvL9C&Ah*}t`q*}S+VYh0M6GxTTSXI)hMpMpIq zD1ImYqJLzbj0}~EpE-aH#VCH_udYEW#`P2zYmi&xSPs_{n6tBj=MY|-XrA;SGA_>y zGtU$?HXm$gYj*!N)_nQ59%lQdXtQZS3*#PC-{iB_sm+ytD*7j`D*k(P&IH2GHT}Eh z5697eQECVIGQAUe#eU2I!yI&%0CP#>%6MWV z@zS!p@+Y1i1b^QuuEF*13CuB zu69dve5k7&Wgb+^s|UB08Dr3u`h@yM0NTj4h7MnHo-4@xmyr7(*4$rpPwsCDZ@2be zRz9V^GnV;;?^Lk%ynzq&K(Aix`mWmW`^152Hoy$CTYVehpD-S1-W^#k#{0^L`V6CN+E z!w+xte;2vu4AmVNEFUOBmrBL>6MK@!O2*N|2=d|Y;oN&A&qv=qKn73lDD zI(+oJAdgv>Yr}8(&@ZuAZE%XUXmX(U!N+Z_sjL<1vjy1R+1IeHt`79fnYdOL{$ci7 z%3f0A*;Zt@ED&Gjm|OFTYBDe%bbo*xXAQsFz+Q`fVBH!N2)kaxN8P$c>sp~QXnv>b zwq=W3&Mtmih7xkR$YA)1Yi?avHNR6C99!u6fh=cL|KQ&PwF!n@ud^n(HNIImHD!h87!i*t?G|p0o+eelJ?B@A64_9%SBhNaJ64EvKgD&%LjLCYnNfc; znj?%*p@*?dq#NqcQFmmX($wms@CSAr9#>hUR^=I+=0B)vvGX%T&#h$kmX*s=^M2E!@N9#m?LhMvz}YB+kd zG~mbP|D(;{s_#;hsKK9lbVK&Lo734x7SIFJ9V_}2$@q?zm^7?*XH94w5Qae{7zOMUF z^?%F%)c1Y)Q?Iy?I>knw*8gYW#ok|2gdS=YYZLiD=CW|Nj;n^x!=S#iJ#`~Ld79+xXpVmUK^B(xO_vO!btA9y7w3L3-0j-y4 z?M-V{%z;JI`bk7yFDcP}OcCd*{Q9S5$iGA7*E1@tfkyjAi!;wP^O71cZ^Ep)qrQ)N z#wqw0_HS;T7x3y|`P==i3hEwK%|>fZ)c&@kgKO1~5<5xBSk?iZV?KI6&i72H6S9A* z=U(*e)EqEs?Oc04)V-~K5AUmh|62H4*`UAtItO$O(q5?6jj+K^oD!04r=6#dsxp?~}{`?&sXn#q2 zGuY~7>O2=!u@@Kfu7q=W*4egu@qPMRM>(eyYyaIE<|j%d=iWNdGsx%c!902v#ngNg z@#U-O_4xN$s_9?(`{>{>7~-6FgWpBpqXb`Ydc3OFL#&I}Irse9F_8R@4zSS*Y*o*B zXL?6*Aw!AfkNCgcr#*yj&p3ZDe2y>v$>FUdKIy_2N~}6AbHc7gA3`6$g@1o|dE>vz z4pl(j9;kyMsjaw}lO?(?Xg%4k!5%^t#@5n=WVc&JRa+XT$~#@rldvN3S1rEpU$;XgxVny7mki3 z-Hh|jUCHrUXuLr!)`w>wgO0N%KTB-1di>cj(x3Bav`7v z3G7EIbU$z>`Nad7Rk_&OT-W{;qg)-GXV-aJT#(ozdmnA~Rq3GQ_3mby(>q6Ocb-RgTUhTN)))x>m&eD;$J5Bg zo&DhY36Yg=J=$Z>t}RJ>o|@hAcwWzN#r(WJ52^g$lh^!63@hh+dR$&_dEGu&^CR*< z!oFqSqO@>xZ*nC2oiOd0eS*F^IL~W-rsrO`J`ej{=ou_q^_(<$&-3f^J z&L^MSYWIe{&pYq&9eGaArA~*kA + + + + Geoadmin + Geocat integration demo + + + + + + + + + + + + + diff --git a/apps/geoadmin-demo/src/main.ts b/apps/geoadmin-demo/src/main.ts new file mode 100644 index 000000000..01af04e78 --- /dev/null +++ b/apps/geoadmin-demo/src/main.ts @@ -0,0 +1 @@ +// bla diff --git a/apps/geoadmin-demo/src/styles.css b/apps/geoadmin-demo/src/styles.css new file mode 100644 index 000000000..85944f45b --- /dev/null +++ b/apps/geoadmin-demo/src/styles.css @@ -0,0 +1,34 @@ +/* You can add global styles to this file, and also import other style files */ +html, +body { + height: 100%; +} +body { + margin: 0; + position: relative; +} + +#geoadmin-root { + width: 100%; + height: calc(100% - 4px); + border: 0; +} + +gn-search-input { + position: absolute; + left: 20px; + top: 55px; + width: 40%; + background: white; +} + +gn-results-list { + position: absolute; + left: 20px; + top: 125px; + bottom: 20px; + width: 40%; + padding: 8px; + background: white; + overflow: auto; +} diff --git a/apps/geoadmin-demo/src/test-setup.ts b/apps/geoadmin-demo/src/test-setup.ts new file mode 100644 index 000000000..a5e675acf --- /dev/null +++ b/apps/geoadmin-demo/src/test-setup.ts @@ -0,0 +1,8 @@ +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +} +import 'jest-preset-angular/setup-jest' diff --git a/apps/geoadmin-demo/tsconfig.app.json b/apps/geoadmin-demo/tsconfig.app.json new file mode 100644 index 000000000..fff4a41d4 --- /dev/null +++ b/apps/geoadmin-demo/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"], + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/apps/geoadmin-demo/tsconfig.editor.json b/apps/geoadmin-demo/tsconfig.editor.json new file mode 100644 index 000000000..8ae117d96 --- /dev/null +++ b/apps/geoadmin-demo/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": ["jest", "node"] + } +} diff --git a/apps/geoadmin-demo/tsconfig.json b/apps/geoadmin-demo/tsconfig.json new file mode 100644 index 000000000..e01cf19bd --- /dev/null +++ b/apps/geoadmin-demo/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + }, + { + "path": "./tsconfig.editor.json" + } + ], + "extends": "../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/apps/geoadmin-demo/tsconfig.spec.json b/apps/geoadmin-demo/tsconfig.spec.json new file mode 100644 index 000000000..53fbfcdc1 --- /dev/null +++ b/apps/geoadmin-demo/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} From 52d8102c5d8188c589ed974a3aa0c28a3877bbd5 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Wed, 4 Dec 2024 18:19:37 +0100 Subject: [PATCH 08/20] temp: adjustements to workflow for geocat-gpf * build docker image for geocat-gpf branch * deploy geoadmin-demo app --- .github/workflows/artifacts.yml | 1 + .github/workflows/deploy.yml | 82 +++------------------------------ 2 files changed, 7 insertions(+), 76 deletions(-) diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index a4e8da810..ffee25002 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -7,6 +7,7 @@ on: push: branches: - geocat + - geocat-gpf # TEMP release: types: [published] issue_comment: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b371ef31d..039b4ec20 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,52 +4,23 @@ run-name: 🚀 Deploy to GitHub Pages for ${{ github.event_name == 'issue_commen env: NODE_VERSION: 20.18.1 -# This workflow runs whenever the "deploy affected apps" checkbox is checked (for PR) -# or on every push to main +# This workflow deploys the geoadmin demo app on: push: branches: - - geocat - issue_comment: - types: - - edited - -concurrency: - group: deploy-${{ github.ref }} - cancel-in-progress: true + - geocat-gpf jobs: - checks: - if: github.event_name != 'issue_comment' || github.event.issue.pull_request - name: Check whether a deploy was requested on a PR - runs-on: ubuntu-latest - outputs: - shouldRun: ${{ github.event_name != 'issue_comment' || (contains(github.event.changes.body.from, '- [ ] 🚀 Build and deploy storybook and demo on GitHub Pages') && contains(github.event.comment.body, '- [x] 🚀 Build and deploy storybook and demo on GitHub Pages')) || '' }} - ref: ${{ github.event_name == 'issue_comment' && steps.comment-branch.outputs.head_ref || '' }} - - steps: - - uses: xt0rted/pull-request-comment-branch@v1 - if: github.event_name == 'issue_comment' - id: comment-branch gh-pages: - needs: checks - if: github.event_name != 'issue_comment' || needs.checks.outputs.shouldRun - name: Deploy docs, apps, Storybook to GitHub Pages + name: Deploy geoadmin demo runs-on: ubuntu-latest env: - BRANCH_NAME: ${{needs.checks.outputs.ref || 'geocat'}} + BRANCH_NAME: ${{'geocat-gpf'}} steps: - - name: Dump GitHub event - env: - GITHUB_CONTEXT: ${{ toJson(github.event) }} - run: echo "$GITHUB_CONTEXT" - - name: Checkout uses: actions/checkout@v2 - with: - ref: ${{ needs.checks.outputs.ref }} - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v3 @@ -57,55 +28,14 @@ jobs: node-version: ${{ env.NODE_VERSION }} cache: 'npm' - - name: add initial comment - uses: thollander/actions-comment-pull-request@v2 - if: github.event_name == 'issue_comment' - with: - message: 'GitHub Pages links: - - - 🚧 building in progress... 🚧' - comment_tag: github-links - pr_number: ${{ github.event.issue.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install run: npm ci - - name: Build storybook - run: npm run build:storybook - - - name: Build demo & web components - run: npm run build:demo - - - name: Build metadata-converter app - if: github.event_name != 'issue_comment' # This is not done on PR, only on main branch - run: npx nx build metadata-converter --prod --base-href=./ - - - name: Build docs - run: npm run docs:build -- --base=/geonetwork-ui/${{env.BRANCH_NAME}}/docs/ && mkdir -p dist/docs && mv docs/.vitepress/dist/* dist/docs - - # FIXME: restore a system for testing web components but faster/lighter than storybook - # - name: Build storybook for web components - # run: npm run build:storybook-wc + - name: Build geoadmin-demo app + run: npx nx build geoadmin-demo --prod --base-href=./ - name: Deploy to directory ${{ env.BRANCH_NAME }} run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" npx gh-pages --dist dist/ --dest ${{env.BRANCH_NAME}} --remove "${{env.BRANCH_NAME}}/**" --no-history --repo "https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" - - - name: update PR comment - uses: thollander/actions-comment-pull-request@v2 - if: github.event_name == 'issue_comment' - with: - message: 'GitHub Pages links: - - * (Documentation)[https://geonetwork.github.io/geonetwork-ui/${{env.BRANCH_NAME}}/docs/] - - * (Web components demo)[https://geonetwork.github.io/geonetwork-ui/${{env.BRANCH_NAME}}/demo/webcomponents/] - - * (UI components storybook)[https://geonetwork.github.io/geonetwork-ui/${{env.BRANCH_NAME}}/storybook/demo/]' - comment_tag: github-links - pr_number: ${{ github.event.issue.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 571e5d54f98d2685720235b3982ca3df011aff26 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 9 Dec 2024 13:46:11 +0100 Subject: [PATCH 09/20] add gn-record-view web component --- apps/geoadmin-demo/src/index.html | 23 +++++-- apps/geoadmin-demo/src/styles.css | 13 +++- apps/geoadmin-demo/src/test-setup.ts | 15 ++-- .../gn-record-view.component.css | 10 +++ .../gn-record-view.component.html | 68 +++++++++++++++++++ .../gn-record-view.component.ts | 53 +++++++++++++++ .../gn-record-view/gn-record-view.sample.html | 51 ++++++++++++++ .../src/app/webcomponents.module.ts | 33 ++++++++- 8 files changed, 253 insertions(+), 13 deletions(-) create mode 100644 apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.css create mode 100644 apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html create mode 100644 apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.ts create mode 100644 apps/webcomponents/src/app/components/gn-record-view/gn-record-view.sample.html diff --git a/apps/geoadmin-demo/src/index.html b/apps/geoadmin-demo/src/index.html index 51f87eed0..7f230b875 100644 --- a/apps/geoadmin-demo/src/index.html +++ b/apps/geoadmin-demo/src/index.html @@ -13,19 +13,34 @@ id="geoadmin-root" src="https://map.geo.admin.ch/#embed?bgLayer=ch.swisstopo.pixelkarte-grau" > + + + + - + record-id="417e9f31-d9d7-4165-9a5c-2f12b7191926" + > diff --git a/apps/geoadmin-demo/src/styles.css b/apps/geoadmin-demo/src/styles.css index 85944f45b..bcc021bda 100644 --- a/apps/geoadmin-demo/src/styles.css +++ b/apps/geoadmin-demo/src/styles.css @@ -14,7 +14,7 @@ body { border: 0; } -gn-search-input { +.search-input { position: absolute; left: 20px; top: 55px; @@ -32,3 +32,14 @@ gn-results-list { background: white; overflow: auto; } + +gn-record-view { + position: absolute; + right: 20px; + top: 125px; + bottom: 20px; + width: 40%; + padding: 8px; + background: white; + overflow: auto; +} diff --git a/apps/geoadmin-demo/src/test-setup.ts b/apps/geoadmin-demo/src/test-setup.ts index a5e675acf..5626c49f2 100644 --- a/apps/geoadmin-demo/src/test-setup.ts +++ b/apps/geoadmin-demo/src/test-setup.ts @@ -1,8 +1,9 @@ -// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment -globalThis.ngJest = { - testEnvironmentOptions: { - errorOnUnknownElements: true, - errorOnUnknownProperties: true, - }, -} import 'jest-preset-angular/setup-jest' +import '../../../jest.setup' + +class ResizeObserverMock { + observe = jest.fn() + unobserve = jest.fn() +} + +;(window as any).ResizeObserver = ResizeObserverMock diff --git a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.css b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.css new file mode 100644 index 000000000..eeaf25b47 --- /dev/null +++ b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.css @@ -0,0 +1,10 @@ +@import '../../../styles.css'; + +.mdc-menu-surface.mat-mdc-autocomplete-panel { + margin-top: 10px !important; + border-radius: 8px; +} + +:host { + display: block; +} diff --git a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html new file mode 100644 index 000000000..e7fc7b22a --- /dev/null +++ b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html @@ -0,0 +1,68 @@ + + +
+ {{ record.title }} +
+ + + + + + +
+ record.metadata.download + +
+ + + + +
+ record.metadata.links + +
+ + + + +
+ record.metadata.api + +
+ + + +
+
diff --git a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.ts b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.ts new file mode 100644 index 000000000..357636d3a --- /dev/null +++ b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.ts @@ -0,0 +1,53 @@ +import { + ChangeDetectionStrategy, + Component, + Injector, + Input, + OnInit, + ViewEncapsulation, +} from '@angular/core' +import { SearchFacade } from '@geonetwork-ui/feature/search' +import { BaseComponent } from '../base.component' +import { Observable } from 'rxjs' +import { + CatalogRecord, + OnlineResource, +} from '@geonetwork-ui/common/domain/model/record' + +// TODO in this component: +// - Support metadata quality option +// - show data preview + +@Component({ + selector: 'wc-gn-record-view', + templateUrl: './gn-record-view.component.html', + styleUrls: ['./gn-record-view.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.ShadowDom, + providers: [SearchFacade], +}) +export class GnRecordViewComponent extends BaseComponent implements OnInit { + @Input() recordId!: string + + record$: Observable + + constructor(injector: Injector) { + super(injector) + } + + ngOnInit() { + super.ngOnInit() + // todo + this.record$ = this.recordsRepository.getRecord(this.recordId) + } + + getDownloads(onlineResources: OnlineResource[]) { + return onlineResources.filter((d) => d.type === 'download') + } + getAPIs(onlineResources: OnlineResource[]) { + return onlineResources.filter((d) => d.type === 'service') + } + getLinks(onlineResources: OnlineResource[]) { + return onlineResources.filter((d) => d.type === 'link') + } +} diff --git a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.sample.html b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.sample.html new file mode 100644 index 000000000..52facab6d --- /dev/null +++ b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.sample.html @@ -0,0 +1,51 @@ + + + + + Web Component Demo + + + + + + + + +
+
+ + +
+
+
+ + diff --git a/apps/webcomponents/src/app/webcomponents.module.ts b/apps/webcomponents/src/app/webcomponents.module.ts index 294d23b8d..8478dc777 100644 --- a/apps/webcomponents/src/app/webcomponents.module.ts +++ b/apps/webcomponents/src/app/webcomponents.module.ts @@ -11,7 +11,18 @@ import { BrowserModule } from '@angular/platform-browser' import { Configuration } from '@geonetwork-ui/data-access/gn4' import { FeatureRecordModule } from '@geonetwork-ui/feature/record' import { FeatureSearchModule } from '@geonetwork-ui/feature/search' -import { UiElementsModule } from '@geonetwork-ui/ui/elements' +import { + ApiCardComponent, + ContentGhostComponent, + DownloadItemComponent, + DownloadsListComponent, + ImageOverlayPreviewComponent, + LinkCardComponent, + MetadataContactComponent, + MetadataInfoComponent, + MetadataQualityComponent, + UiElementsModule, +} from '@geonetwork-ui/ui/elements' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' import { UiSearchModule } from '@geonetwork-ui/ui/search' import { @@ -46,6 +57,12 @@ import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' import { GnDatasetViewMapComponent } from './components/gn-dataset-view-map/gn-dataset-view-map.component' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' +import { GnRecordViewComponent } from './components/gn-record-view/gn-record-view.component' +import { LetDirective } from '@ngrx/component' +import { + BlockListComponent, + PreviousNextButtonsComponent, +} from '@geonetwork-ui/ui/layout' const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ [GnFacetsComponent, 'gn-facets'], @@ -57,6 +74,7 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ [GnMapViewerComponent, 'gn-map-viewer'], [GnFigureDatasetsComponent, 'gn-figure-datasets'], [GnDatasetViewMapComponent, 'gn-dataset-view-map'], + [GnRecordViewComponent, 'gn-record-view'], ] @NgModule({ @@ -72,6 +90,7 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ GnMapViewerComponent, GnFigureDatasetsComponent, GnDatasetViewMapComponent, + GnRecordViewComponent, ], imports: [ CommonModule, @@ -98,6 +117,18 @@ const CUSTOM_ELEMENTS: [new (...args) => BaseComponent, string][] = [ BrowserAnimationsModule, MapStateContainerComponent, LayersPanelComponent, + MetadataInfoComponent, + ContentGhostComponent, + LetDirective, + ImageOverlayPreviewComponent, + MetadataContactComponent, + MetadataQualityComponent, + DownloadsListComponent, + BlockListComponent, + LinkCardComponent, + ApiCardComponent, + DownloadItemComponent, + PreviousNextButtonsComponent, ], providers: [ provideGn4(), From eb7ceeb25a6130fa8e5b07643d58fa96cc9fbc04 Mon Sep 17 00:00:00 2001 From: ronitjadhav Date: Wed, 18 Dec 2024 12:38:28 +0100 Subject: [PATCH 10/20] Added Datahub Search and record view --- .github/workflows/deploy.yml | 1 - apps/geoadmin-demo/src/index.html | 141 ++++++++++++++++++++---------- apps/geoadmin-demo/src/styles.css | 140 +++++++++++++++++++++++------ 3 files changed, 208 insertions(+), 74 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 039b4ec20..dd71b0049 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,7 +11,6 @@ on: - geocat-gpf jobs: - gh-pages: name: Deploy geoadmin demo runs-on: ubuntu-latest diff --git a/apps/geoadmin-demo/src/index.html b/apps/geoadmin-demo/src/index.html index 7f230b875..203dea46c 100644 --- a/apps/geoadmin-demo/src/index.html +++ b/apps/geoadmin-demo/src/index.html @@ -2,57 +2,108 @@ - Geoadmin + Geocat integration demo - + Geoadmin + Geocat Search - - - - - - - - - - +
+
+ +
    +
    + +
    diff --git a/apps/geoadmin-demo/src/styles.css b/apps/geoadmin-demo/src/styles.css index bcc021bda..2ef289ba1 100644 --- a/apps/geoadmin-demo/src/styles.css +++ b/apps/geoadmin-demo/src/styles.css @@ -1,45 +1,129 @@ -/* You can add global styles to this file, and also import other style files */ -html, -body { +body, +html { + margin: 0; + padding: 0; height: 100%; + font-family: Arial, sans-serif; } -body { - margin: 0; +.map-container { position: relative; -} - -#geoadmin-root { width: 100%; - height: calc(100% - 4px); - border: 0; + height: 100vh; } - -.search-input { +.search-container { position: absolute; + top: 60px; left: 20px; - top: 55px; - width: 40%; + width: 300px; + z-index: 10; +} +#search { + width: 100%; + padding: 10px; + margin-bottom: 0; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} +#results { background: white; + border: 1px solid #ddd; + border-radius: 8px; + list-style: none; + padding: 0; + margin: 0; + max-height: 400px; + overflow-y: auto; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; } -gn-results-list { - position: absolute; - left: 20px; - top: 125px; - bottom: 20px; - width: 40%; - padding: 8px; - background: white; - overflow: auto; +#results li { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + border-bottom: 1px solid #eee; + transition: background-color 0.2s; + gap: 10px; +} + +#results li div { + flex: 1; + overflow-wrap: break-word; +} + +#results li:hover { + background-color: #f0f8ff; + transform: scale(1.02); +} + +#results li:last-child { + border-bottom: none; +} + +.result-title { + font-size: 14px; + font-weight: bold; + color: #333; + margin-bottom: 5px; + line-height: 1.2; +} + +.open-btn { + flex-shrink: 0; + background-color: #0f4395; + color: white; + border: none; + padding: 5px 10px; + border-radius: 3px; + cursor: pointer; + transition: background-color 0.3s; } -gn-record-view { +.open-btn:hover { + background-color: #0e3d84; +} + +#geoadmin-root { + width: 100%; + height: 100vh; + border: none; +} +.gn-record-view-container { position: absolute; - right: 20px; - top: 125px; - bottom: 20px; + right: 55px; + top: 40px; + bottom: 40px; width: 40%; - padding: 8px; background: white; + padding: 20px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + border-radius: 8px; overflow: auto; + z-index: 100; +} + +.close-btn { + position: sticky; + top: 0; + margin-left: auto; + margin-right: 8px; + background: #050505; + color: white; + border: none; + border-radius: 50%; + width: 30px; + height: 30px; + font-size: 16px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + z-index: 10; +} + +.close-btn:hover { + background: #d32f2f; } From 59b2948c4ee2a202cd5e136c282d5a2f1a22d571 Mon Sep 17 00:00:00 2001 From: ronitjadhav Date: Mon, 23 Dec 2024 14:03:21 +0100 Subject: [PATCH 11/20] Updated the gn-record-view webcomponent --- .../gn-record-view.component.html | 104 ++++++++++-------- .../gn-record-view.component.ts | 42 ++++--- 2 files changed, 85 insertions(+), 61 deletions(-) diff --git a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html index e7fc7b22a..ba12d94ac 100644 --- a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html +++ b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html @@ -4,65 +4,75 @@ [showContent]="!!record?.title" >
    - {{ record.title }} + {{ record?.title || 'No Title Available' }}
    - + + - -
    - record.metadata.download - + + +
    +
    + record.metadata.download + +
    + + +
    - - - -
    - record.metadata.links - +
    +
    + record.metadata.links + +
    + + +
    - - - -
    - record.metadata.api - +
    +
    + record.metadata.api + +
    + + +
    - - - + + +

    No record found for the provided ID.

    +
    diff --git a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.ts b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.ts index 357636d3a..ac5fb466b 100644 --- a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.ts +++ b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core' import { SearchFacade } from '@geonetwork-ui/feature/search' import { BaseComponent } from '../base.component' -import { Observable } from 'rxjs' +import { Observable, map } from 'rxjs' import { CatalogRecord, OnlineResource, @@ -26,28 +26,42 @@ import { encapsulation: ViewEncapsulation.ShadowDom, providers: [SearchFacade], }) -export class GnRecordViewComponent extends BaseComponent implements OnInit { - @Input() recordId!: string - record$: Observable +export class GnRecordViewComponent extends BaseComponent implements OnInit { + @Input() recordId!: string; + record$: Observable; + downloads$: Observable; + links$: Observable; + apis$: Observable; constructor(injector: Injector) { - super(injector) + super(injector); } ngOnInit() { - super.ngOnInit() - // todo - this.record$ = this.recordsRepository.getRecord(this.recordId) + super.ngOnInit(); + this.record$ = this.recordsRepository.getRecord(this.recordId); + + this.downloads$ = this.record$.pipe( + map((record) => this.getDownloads(record?.onlineResources || [])) + ); + this.links$ = this.record$.pipe( + map((record) => this.getLinks(record?.onlineResources || [])) + ); + this.apis$ = this.record$.pipe( + map((record) => this.getAPIs(record?.onlineResources || [])) + ); } - getDownloads(onlineResources: OnlineResource[]) { - return onlineResources.filter((d) => d.type === 'download') + getDownloads(onlineResources: OnlineResource[]): OnlineResource[] { + return onlineResources.filter((resource) => resource.type === 'download'); } - getAPIs(onlineResources: OnlineResource[]) { - return onlineResources.filter((d) => d.type === 'service') + + getLinks(onlineResources: OnlineResource[]): OnlineResource[] { + return onlineResources.filter((resource) => resource.type === 'link'); } - getLinks(onlineResources: OnlineResource[]) { - return onlineResources.filter((d) => d.type === 'link') + + getAPIs(onlineResources: OnlineResource[]): OnlineResource[] { + return onlineResources.filter((resource) => resource.type === 'service'); } } From 903d5ea434d18b5d12e6adb5f5e4e08eecb5bf6a Mon Sep 17 00:00:00 2001 From: ronitjadhav Date: Mon, 30 Dec 2024 14:19:32 +0100 Subject: [PATCH 12/20] Removed unnecessary ngIf's --- .../components/gn-record-view/gn-record-view.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html index ba12d94ac..7e4954909 100644 --- a/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html +++ b/apps/webcomponents/src/app/components/gn-record-view/gn-record-view.component.html @@ -4,10 +4,10 @@ [showContent]="!!record?.title" >
    - {{ record?.title || 'No Title Available' }} + {{ record.title }}
    - + - +
    From fff76394b07b81994eb00e14ba0efafa3a7bd687 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Mon, 6 Jan 2025 14:09:13 +0100 Subject: [PATCH 13/20] correctly initialize API url --- apps/geoadmin-demo/src/index.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/geoadmin-demo/src/index.html b/apps/geoadmin-demo/src/index.html index 203dea46c..8bc9c0c0f 100644 --- a/apps/geoadmin-demo/src/index.html +++ b/apps/geoadmin-demo/src/index.html @@ -7,9 +7,16 @@ + + +
    - +