Skip to content

Commit

Permalink
Merge pull request #1069 from geonetwork/map-legend
Browse files Browse the repository at this point in the history
[Datahub] Added dynamic legend generation based on map context
  • Loading branch information
ronitjadhav authored Dec 19, 2024
2 parents 23c6e82 + 692832e commit 67e23df
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 18 deletions.
4 changes: 3 additions & 1 deletion apps/datahub-e2e/src/e2e/datasetDetailPage.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
50 changes: 46 additions & 4 deletions libs/feature/record/src/lib/map-view/map-view.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<button
(click)="resetSelection()"
class="rounded bg-primary-opacity-25 text-white absolute right-[0.5em]"
<gn-ui-button
type="light"
(buttonClick)="resetSelection()"
style="
--gn-ui-button-padding: 0px;
--gn-ui-button-width: 24px;
--gn-ui-button-height: 24px;
"
extraClass="absolute right-[0.5em] ml-[8px] mr-[10px]"
>
<ng-icon name="matClose" class="align-middle text-sm"></ng-icon>
</button>
</gn-ui-button>
<gn-ui-feature-detail [feature]="selection"></gn-ui-feature-detail>
</div>

<div
class="top-[1em] p-3 bg-white absolute overflow-y-auto overflow-x-hidden max-h-72 w-56"
[ngClass]="{ 'right-[1em]': !selection, 'right-[16em]': selection }"
[hidden]="!showLegend || !legendExists"
>
<div class="flex justify-between items-center mb-2">
<div class="text-primary font-bold">Legend</div>
<gn-ui-button
type="light"
(buttonClick)="toggleLegend()"
style="
--gn-ui-button-padding: 0px;
--gn-ui-button-width: 24px;
--gn-ui-button-height: 24px;
"
extraClass="ml-[8px] mr-[10px]"
>
<ng-icon name="matClose" class="align-middle text-sm"></ng-icon>
</gn-ui-button>
</div>
<gn-ui-map-legend
[context]="mapContext$ | async"
(legendStatusChange)="onLegendStatusChange($event)"
></gn-ui-map-legend>
</div>

<gn-ui-button
*ngIf="!showLegend && legendExists && !selection"
type="outline"
(buttonClick)="toggleLegend()"
extraClass="absolute top-[1em] right-[1em] rounded p-1 text-xs bg-white"
>
Legend
</gn-ui-button>

<gn-ui-loading-mask
*ngIf="loading"
class="absolute inset-0"
Expand Down
22 changes: 21 additions & 1 deletion libs/feature/record/src/lib/map-view/map-view.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as geoSdkCore from '@geospatial-sdk/core'
import { MapContext } from '@geospatial-sdk/core'
import {
MapContainerComponent,
MapLegendComponent,
prioritizePageScroll,
} from '@geonetwork-ui/ui/map'
import { MockBuilder } from 'ng-mocks'
Expand Down Expand Up @@ -161,7 +162,7 @@ describe('MapViewComponent', () => {
useClass: DataServiceMock,
},
],
imports: [TranslateModule.forRoot()],
imports: [TranslateModule.forRoot(), MapLegendComponent],
}).compileComponents()
mdViewFacade = TestBed.inject(MdViewFacade)
})
Expand Down Expand Up @@ -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(() => {
Expand Down
25 changes: 23 additions & 2 deletions libs/feature/record/src/lib/map-view/map-view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
distinctUntilChanged,
finalize,
map,
shareReplay,
switchMap,
tap,
} from 'rxjs/operators'
Expand All @@ -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 {
Expand All @@ -66,13 +71,28 @@ import {
LoadingMaskComponent,
NgIconComponent,
ExternalViewerButtonComponent,
ButtonComponent,
MapLegendComponent,
],
viewProviders: [provideIcons({ matClose })],
})
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$,
Expand Down Expand Up @@ -148,7 +168,8 @@ export class MapViewComponent implements AfterViewInit {
...context,
view,
}
})
}),
shareReplay(1)
)

constructor(
Expand Down
1 change: 1 addition & 0 deletions libs/ui/map/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.geosdk--legend-container {
overflow: auto;
white-space: normal;
word-wrap: break-word;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div *ngIf="legendHTML" [innerHTML]="legendHTML.outerHTML"></div>
150 changes: 150 additions & 0 deletions libs/ui/map/src/lib/components/map-legend/map-legend.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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<MapLegendComponent>

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)
})
})
})
39 changes: 39 additions & 0 deletions libs/ui/map/src/lib/components/map-legend/map-legend.component.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>()
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)
}
}
}
}
}
Loading

0 comments on commit 67e23df

Please sign in to comment.