diff --git a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts
index 088b06fb4..4db6144c2 100644
--- a/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts
+++ b/apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts
@@ -331,10 +331,12 @@ describe('dataset pages', () => {
.children('button')
.should('have.length.gt', 1)
})
- it('should display the map', () => {
+ it('should display the map and the legend', () => {
cy.get('@previewSection')
.find('gn-ui-map-container')
.should('be.visible')
+
+ cy.get('@previewSection').find('gn-ui-map-legend').should('be.visible')
})
it('should display the table', () => {
cy.get('@previewSection')
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 fb1da0662..e85b3dee3 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
@@ -25,14 +25,56 @@
class="top-[1em] right-[1em] p-3 bg-white absolute overflow-y-auto overflow-x-hidden max-h-72 w-56"
[class.hidden]="!selection"
>
-
+
+
+
+
+
+ Legend
+
+
{
useClass: DataServiceMock,
},
],
- imports: [TranslateModule.forRoot()],
+ imports: [TranslateModule.forRoot(), MapLegendComponent],
}).compileComponents()
mdViewFacade = TestBed.inject(MdViewFacade)
})
@@ -768,6 +769,25 @@ describe('MapViewComponent', () => {
})
})
+ describe('display legend', () => {
+ it('should render the MapLegendComponent', () => {
+ const legendComponent = fixture.debugElement.query(
+ By.directive(MapLegendComponent)
+ )
+ expect(legendComponent).toBeTruthy()
+ })
+ it('should handle legendStatusChange event', () => {
+ const legendComponent = fixture.debugElement.query(
+ By.directive(MapLegendComponent)
+ ).componentInstance
+ const legendStatusChangeSpy = jest.spyOn(
+ component,
+ 'onLegendStatusChange'
+ )
+ legendComponent.legendStatusChange.emit(true)
+ expect(legendStatusChangeSpy).toHaveBeenCalledWith(true)
+ })
+ })
describe('map view extent', () => {
describe('if no record extent', () => {
beforeEach(fakeAsync(() => {
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..bc7dfc2a4 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
@@ -22,6 +22,7 @@ import {
distinctUntilChanged,
finalize,
map,
+ shareReplay,
switchMap,
tap,
} from 'rxjs/operators'
@@ -37,12 +38,16 @@ import {
FeatureDetailComponent,
MapContainerComponent,
prioritizePageScroll,
+ MapLegendComponent,
} from '@geonetwork-ui/ui/map'
import { Feature } from 'geojson'
import { NgIconComponent, provideIcons } from '@ng-icons/core'
import { matClose } from '@ng-icons/material-icons/baseline'
import { CommonModule } from '@angular/common'
-import { DropdownSelectorComponent } from '@geonetwork-ui/ui/inputs'
+import {
+ ButtonComponent,
+ DropdownSelectorComponent,
+} from '@geonetwork-ui/ui/inputs'
import { TranslateModule } from '@ngx-translate/core'
import { ExternalViewerButtonComponent } from '../external-viewer-button/external-viewer-button.component'
import {
@@ -66,6 +71,8 @@ import {
LoadingMaskComponent,
NgIconComponent,
ExternalViewerButtonComponent,
+ ButtonComponent,
+ MapLegendComponent,
],
viewProviders: [provideIcons({ matClose })],
})
@@ -73,6 +80,19 @@ export class MapViewComponent implements AfterViewInit {
@ViewChild('mapContainer') mapContainer: MapContainerComponent
selection: Feature
+ showLegend = true
+ legendExists = false
+
+ toggleLegend() {
+ this.showLegend = !this.showLegend
+ }
+
+ onLegendStatusChange(status: boolean) {
+ this.legendExists = status
+ if (!status) {
+ this.showLegend = false
+ }
+ }
compatibleMapLinks$ = combineLatest([
this.mdViewFacade.mapApiLinks$,
@@ -148,7 +168,8 @@ export class MapViewComponent implements AfterViewInit {
...context,
view,
}
- })
+ }),
+ shareReplay(1)
)
constructor(
diff --git a/libs/ui/map/src/index.ts b/libs/ui/map/src/index.ts
index fb10d8a26..921923dda 100644
--- a/libs/ui/map/src/index.ts
+++ b/libs/ui/map/src/index.ts
@@ -1,4 +1,5 @@
export * from './lib/components/map-container/map-container.component'
export * from './lib/components/map-container/map-settings.token'
export * from './lib/components/feature-detail/feature-detail.component'
+export * from './lib/components/map-legend/map-legend.component'
export * from './lib/map-utils'
diff --git a/libs/ui/map/src/lib/components/map-legend/map-legend.component.css b/libs/ui/map/src/lib/components/map-legend/map-legend.component.css
new file mode 100644
index 000000000..91a65cd49
--- /dev/null
+++ b/libs/ui/map/src/lib/components/map-legend/map-legend.component.css
@@ -0,0 +1,5 @@
+.geosdk--legend-container {
+ overflow: auto;
+ white-space: normal;
+ word-wrap: break-word;
+}
diff --git a/libs/ui/map/src/lib/components/map-legend/map-legend.component.html b/libs/ui/map/src/lib/components/map-legend/map-legend.component.html
new file mode 100644
index 000000000..35ed987a8
--- /dev/null
+++ b/libs/ui/map/src/lib/components/map-legend/map-legend.component.html
@@ -0,0 +1 @@
+
diff --git a/libs/ui/map/src/lib/components/map-legend/map-legend.component.spec.ts b/libs/ui/map/src/lib/components/map-legend/map-legend.component.spec.ts
new file mode 100644
index 000000000..1c4cd1f51
--- /dev/null
+++ b/libs/ui/map/src/lib/components/map-legend/map-legend.component.spec.ts
@@ -0,0 +1,150 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { MapLegendComponent } from './map-legend.component'
+import { MapContext } from '@geospatial-sdk/core'
+import { createLegendFromLayer } from '@geospatial-sdk/legend'
+
+jest.mock('@geospatial-sdk/legend', () => ({
+ createLegendFromLayer: jest.fn(),
+}))
+
+describe('MapLegendComponent', () => {
+ let component: MapLegendComponent
+ let fixture: ComponentFixture
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [MapLegendComponent],
+ }).compileComponents()
+ })
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MapLegendComponent)
+ component = fixture.componentInstance
+ fixture.detectChanges()
+ })
+
+ it('should create', () => {
+ expect(component).toBeTruthy()
+ })
+
+ describe('Change of map-context', () => {
+ it('should create legend on first change', async () => {
+ const mockContext: MapContext = {
+ layers: [
+ {
+ id: 'test-layer',
+ },
+ ],
+ } as MapContext
+
+ const mockLegendElement = document.createElement('div')
+ ;(createLegendFromLayer as jest.Mock).mockResolvedValue(mockLegendElement)
+
+ const legendStatusChangeSpy = jest.spyOn(
+ component.legendStatusChange,
+ 'emit'
+ )
+
+ await component.ngOnChanges({
+ context: {
+ currentValue: mockContext,
+ previousValue: null,
+ firstChange: true,
+ isFirstChange: () => true,
+ },
+ })
+
+ expect(createLegendFromLayer).toHaveBeenCalledWith(mockContext.layers[0])
+ expect(component.legendHTML).toBe(mockLegendElement)
+ expect(legendStatusChangeSpy).toHaveBeenCalledWith(true)
+ })
+
+ it('should create legend and emit status on subsequent context changes', async () => {
+ const mockContext: MapContext = {
+ layers: [
+ {
+ id: 'test-layer',
+ },
+ ],
+ } as MapContext
+
+ const mockLegendElement = document.createElement('div')
+ ;(createLegendFromLayer as jest.Mock).mockResolvedValue(mockLegendElement)
+
+ const legendStatusChangeSpy = jest.spyOn(
+ component.legendStatusChange,
+ 'emit'
+ )
+
+ await component.ngOnChanges({
+ context: {
+ currentValue: mockContext,
+ previousValue: {},
+ firstChange: false,
+ isFirstChange: () => false,
+ },
+ })
+
+ expect(createLegendFromLayer).toHaveBeenCalledWith(mockContext.layers[0])
+ expect(component.legendHTML).toBe(mockLegendElement)
+ expect(legendStatusChangeSpy).toHaveBeenCalledWith(true)
+ })
+
+ it('should emit nothing when no legend is created', async () => {
+ const mockContext: MapContext = {
+ layers: [
+ {
+ id: 'test-layer',
+ },
+ ],
+ } as MapContext
+
+ ;(createLegendFromLayer as jest.Mock).mockResolvedValue(false)
+
+ const legendStatusChangeSpy = jest.spyOn(
+ component.legendStatusChange,
+ 'emit'
+ )
+
+ await component.ngOnChanges({
+ context: {
+ currentValue: mockContext,
+ previousValue: {},
+ firstChange: false,
+ isFirstChange: () => false,
+ },
+ })
+
+ expect(createLegendFromLayer).toHaveBeenCalledWith(mockContext.layers[0])
+ expect(component.legendHTML).toBe(false)
+ expect(legendStatusChangeSpy).not.toHaveBeenCalled()
+ })
+
+ it('should handle multiple layers', async () => {
+ const mockContext: MapContext = {
+ layers: [{ id: 'layer-1' }, { id: 'layer-2' }],
+ } as MapContext
+
+ const mockLegendElement = document.createElement('div')
+ ;(createLegendFromLayer as jest.Mock).mockResolvedValue(mockLegendElement)
+
+ const legendStatusChangeSpy = jest.spyOn(
+ component.legendStatusChange,
+ 'emit'
+ )
+
+ await component.ngOnChanges({
+ context: {
+ currentValue: mockContext,
+ previousValue: {},
+ firstChange: false,
+ isFirstChange: () => false,
+ },
+ })
+
+ expect(createLegendFromLayer).toHaveBeenCalledWith(mockContext.layers[0])
+ expect(component.legendHTML).toBe(mockLegendElement)
+ expect(legendStatusChangeSpy).toHaveBeenCalledWith(true)
+ })
+ })
+})
diff --git a/libs/ui/map/src/lib/components/map-legend/map-legend.component.ts b/libs/ui/map/src/lib/components/map-legend/map-legend.component.ts
new file mode 100644
index 000000000..7f635f10e
--- /dev/null
+++ b/libs/ui/map/src/lib/components/map-legend/map-legend.component.ts
@@ -0,0 +1,39 @@
+import {
+ Component,
+ EventEmitter,
+ Input,
+ OnChanges,
+ Output,
+ SimpleChanges,
+ ViewEncapsulation,
+} from '@angular/core'
+import { MapContext } from '@geospatial-sdk/core'
+import { createLegendFromLayer } from '@geospatial-sdk/legend'
+import { NgIf } from '@angular/common'
+
+@Component({
+ selector: 'gn-ui-map-legend',
+ templateUrl: './map-legend.component.html',
+ standalone: true,
+ styleUrls: ['./map-legend.component.css'],
+ encapsulation: ViewEncapsulation.None,
+ imports: [NgIf],
+})
+export class MapLegendComponent implements OnChanges {
+ @Input() context: MapContext | null
+ @Output() legendStatusChange = new EventEmitter()
+ legendHTML: HTMLElement | false
+
+ async ngOnChanges(changes: SimpleChanges) {
+ if ('context' in changes) {
+ const mapContext = changes['context'].currentValue
+ if (mapContext.layers && mapContext.layers.length > 0) {
+ const mapContextLayer = mapContext.layers[0]
+ this.legendHTML = await createLegendFromLayer(mapContextLayer)
+ if (this.legendHTML) {
+ this.legendStatusChange.emit(true)
+ }
+ }
+ }
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 11cb74711..3bfd48281 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,8 +24,9 @@
"@bartholomej/ngx-translate-extract": "^8.0.2",
"@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
"@camptocamp/ogc-client": "1.1.1-dev.3e2d3cc",
- "@geospatial-sdk/core": "0.0.5-dev.29",
- "@geospatial-sdk/geocoding": "0.0.5-dev.29",
+ "@geospatial-sdk/core": "0.0.5-dev.31",
+ "@geospatial-sdk/geocoding": "0.0.5-dev.31",
+ "@geospatial-sdk/legend": "^0.0.5-dev.31",
"@geospatial-sdk/openlayers": "0.0.5-dev.29",
"@ltd/j-toml": "~1.35.2",
"@messageformat/core": "^3.0.1",
@@ -4498,17 +4499,22 @@
}
},
"node_modules/@geospatial-sdk/core": {
- "version": "0.0.5-dev.29",
- "resolved": "https://registry.npmjs.org/@geospatial-sdk/core/-/core-0.0.5-dev.29.tgz",
- "integrity": "sha512-urGQ0glk8fTqK4SkdKpuAGGh4TlFbjm+uW7FmyZ47HEvAOTcE0kvZPOsg/LtNY2VovyXVrnyavdOlUt9Rvqgcg==",
+ "version": "0.0.5-dev.31",
+ "resolved": "https://registry.npmjs.org/@geospatial-sdk/core/-/core-0.0.5-dev.31.tgz",
+ "integrity": "sha512-A3U7GuGgyhFnSneqpqXh80lwNJwQT8lLYBdlvKZnLo9usldI0BSSRFQo+iKgkJ1NxWMTdfpcIbefAVpDdILuqw==",
"dependencies": {
"proj4": "^2.9.2"
}
},
"node_modules/@geospatial-sdk/geocoding": {
- "version": "0.0.5-dev.29",
- "resolved": "https://registry.npmjs.org/@geospatial-sdk/geocoding/-/geocoding-0.0.5-dev.29.tgz",
- "integrity": "sha512-WcxV6Ys6xN7AFLm4UOXkFspj/osCZ2f/OP3LiC63W/0MdwiAg3ajRldVX5pfDyPx2GTKv0ZYcxuFzi8P7ZLFaw=="
+ "version": "0.0.5-dev.31",
+ "resolved": "https://registry.npmjs.org/@geospatial-sdk/geocoding/-/geocoding-0.0.5-dev.31.tgz",
+ "integrity": "sha512-z6Hkb+fktKmyymDt7yS7xpCptjehZJR+BHzn0mNuawuAP2vW4+jRBj78+/jkdEvi4jKBLM+PXs3Tg1WoP2isbQ=="
+ },
+ "node_modules/@geospatial-sdk/legend": {
+ "version": "0.0.5-dev.31",
+ "resolved": "https://registry.npmjs.org/@geospatial-sdk/legend/-/legend-0.0.5-dev.31.tgz",
+ "integrity": "sha512-pxy1bm6KSqkIH1KVSrBBz9fxxhzmv246bKQQDFXfkPmVaaRnNlpytq3QMQoTUb8XiOFP2p7gWR909wfmHfsNDQ=="
},
"node_modules/@geospatial-sdk/openlayers": {
"version": "0.0.5-dev.29",
diff --git a/package.json b/package.json
index ac758e4e4..e3ea18c7d 100644
--- a/package.json
+++ b/package.json
@@ -59,8 +59,9 @@
"@bartholomej/ngx-translate-extract": "^8.0.2",
"@biesbjerg/ngx-translate-extract-marker": "^1.0.0",
"@camptocamp/ogc-client": "1.1.1-dev.3e2d3cc",
- "@geospatial-sdk/core": "0.0.5-dev.29",
- "@geospatial-sdk/geocoding": "0.0.5-dev.29",
+ "@geospatial-sdk/core": "0.0.5-dev.31",
+ "@geospatial-sdk/geocoding": "0.0.5-dev.31",
+ "@geospatial-sdk/legend": "^0.0.5-dev.31",
"@geospatial-sdk/openlayers": "0.0.5-dev.29",
"@ltd/j-toml": "~1.35.2",
"@messageformat/core": "^3.0.1",