diff --git a/ui-ngx/package.json b/ui-ngx/package.json index b2e2dc18c3..1536858de8 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -28,6 +28,7 @@ "@flowjs/ngx-flow": "18.0.1", "@geoman-io/leaflet-geoman-free": "2.17.0", "@iplab/ngx-color-picker": "^18.0.1", + "@maplibre/maplibre-gl-leaflet": "^0.0.22", "@mat-datetimepicker/core": "~14.0.0", "@mdi/svg": "^7.4.47", "@messageformat/core": "^3.4.0", @@ -64,6 +65,7 @@ "leaflet.gridlayer.googlemutant": "0.14.1", "leaflet.markercluster": "1.5.3", "libphonenumber-js": "^1.11.15", + "maplibre-gl": "^4.7.1", "marked": "~12.0.2", "moment": "^2.30.1", "moment-timezone": "^0.5.45", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 06d49d0343..df02c46482 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts @@ -17,6 +17,7 @@ import L, { TB } from 'leaflet'; import { guid, isNotEmptyStr } from '@core/utils'; import 'leaflet-providers'; +import '@maplibre/maplibre-gl-leaflet'; import '@geoman-io/leaflet-geoman-free'; import 'leaflet.markercluster'; import { MatIconRegistry } from '@angular/material/icon'; @@ -211,15 +212,19 @@ class LayersControl extends SidebarPaneControl { input.on('click', (e: JQuery.MouseEventBase) => { e.stopPropagation(); - layers.forEach((other) => { - if (other.layer === layerData.layer) { - map.addLayer(other.layer); - map.attributionControl.setPrefix(other.attributionPrefix); - } else { - map.removeLayer(other.layer); + if (!map.hasLayer(layerData.layer)) { + map.addLayer(layerData.layer); + map.attributionControl.setPrefix(layerData.attributionPrefix); + if (layerData.onAdd) { + layerData.onAdd(); } - }); - map.fire('baselayerchange', { layer: layerData.layer }); + layers.forEach((other) => { + if (other.layer !== layerData.layer) { + map.removeLayer(other.layer); + } + }); + map.fire('baselayerchange', { layer: layerData.layer }); + } }); item.on('dblclick', (e) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index 304100828b..87ca69034f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -26,17 +26,34 @@ import { HereMapLayerSettings, MapLayerSettings, MapProvider, - OpenStreetMapLayerSettings, + OpenStreetMapLayerSettings, ReferenceLayerType, TencentMapLayerSettings } from '@shared/models/widget/maps/map.models'; import { WidgetContext } from '@home/models/widget-component.models'; import { DeepPartial } from '@shared/models/common'; import { mergeDeep } from '@core/utils'; -import { Observable, of, switchMap } from 'rxjs'; +import { Observable, of, shareReplay, switchMap } from 'rxjs'; import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe'; import L from 'leaflet'; import { catchError, map } from 'rxjs/operators'; import { ResourcesService } from '@core/services/resources.service'; +import { StyleSpecification, VectorSourceSpecification } from '@maplibre/maplibre-gl-style-spec'; +import { ResourceType } from 'maplibre-gl'; + +const referenceLayerStyleUrlMap = new Map( + [ + [ReferenceLayerType.openstreetmap_hybrid, '/assets/map/openstreetmap_hybrid_reference_style.json'], + [ReferenceLayerType.world_edition_hybrid, '/assets/map/world_edition_hybrid_reference_style.json'] + ] +); + +const referenceLayerCache = new Map>(); + +interface TbMapLayerData { + layer: L.Layer; + attribution: boolean; + onAdd?: () => void; +} export abstract class TbMapLayer { @@ -65,19 +82,23 @@ export abstract class TbMapLayer { } public loadLayer(theMap: L.Map): Observable { - return this.createLayer().pipe( - switchMap((layer) => { - if (layer) { - return this.createLayer().pipe( - map((mini) => { - if (mini) { - const attribution = layer.getAttribution(); - const attributionPrefix = attribution ? theMap.attributionControl.options.prefix as string : null; + return this.generateLayer().pipe( + switchMap((layerData) => { + if (layerData) { + return this.generateLayer().pipe( + map((miniLayerData) => { + if (miniLayerData) { + const attributionPrefix = layerData.attribution ? theMap.attributionControl.options.prefix as string : null; return { title: this.title(), attributionPrefix: attributionPrefix, - layer, - mini + layer: layerData.layer, + mini: miniLayerData.layer, + onAdd: () => { + if (layerData.onAdd) { + layerData.onAdd(); + } + } }; } else { return null; @@ -91,6 +112,77 @@ export abstract class TbMapLayer { ); } + private generateLayer(): Observable { + return this.createLayer().pipe( + switchMap((baseLayer) => { + if (baseLayer) { + if (this.settings.referenceLayer) { + return this.loadReferenceLayer(this.settings.referenceLayer).pipe( + map((referenceLayer) => { + if (referenceLayer) { + const layer = L.featureGroup(); + baseLayer.addTo(layer); + referenceLayer.addTo(layer); + return { + layer, + attribution: !!baseLayer.getAttribution() || !!referenceLayer.getAttribution(), + onAdd: () => { + (referenceLayer as any)._update(); + } + }; + } else { + return { + layer: baseLayer, + attribution: !!baseLayer.getAttribution() + }; + } + })); + } else { + return of({ + layer: baseLayer, + attribution: !!baseLayer.getAttribution() + }); + } + } else { + return of(null); + } + } + )); + } + + private loadReferenceLayer(referenceLayer: ReferenceLayerType): Observable { + let spec$ = referenceLayerCache.get(referenceLayer); + if (!spec$) { + const styleUrl = referenceLayerStyleUrlMap.get(referenceLayer); + spec$ = this.ctx.http.get(styleUrl).pipe( + shareReplay({ + bufferSize: 1, + refCount: true + }) + ); + referenceLayerCache.set(referenceLayer, spec$); + } + return spec$.pipe( + map(spec => { + const sourceSpec = (spec.sources['esri'] as VectorSourceSpecification); + const tileUrl = sourceSpec.url; + const attribution = sourceSpec.attribution; + const transformRequest = (url: string, resourceType: ResourceType) => { + if (resourceType === 'Tile') { + url = tileUrl + '/' + url; + } + return {url} + } + const gl = L.maplibreGL({ + style: spec, + transformRequest + }); + gl.options.attribution = attribution; + return gl; + }) + ); + } + private title(): string { const customTranslate = this.ctx.$injector.get(CustomTranslatePipe); if (this.settings.label) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index 592610f8b2..985bdc17f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss @@ -35,6 +35,10 @@ div.tb-widget .tb-widget-content.tb-no-interaction { .tb-map-container { flex-direction: column; + .leaflet-gl-layer.maplibregl-map { + position: relative; + z-index: 1; + } } .tb-map-layout { @@ -47,6 +51,10 @@ div.tb-widget .tb-widget-content.tb-no-interaction { .tb-map { position: relative; flex: 1; + .leaflet-control-attribution { + font-size: 0.6rem; + background: rgba(255,255,255,0.5); + } &.leaflet-touch { .leaflet-bar { border: 1px solid rgba(0,0,0,0.38); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts index 7112a0c3c2..392c9f9565 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts @@ -123,7 +123,8 @@ export class MapLayerRowComponent implements ControlValueAccessor, OnInit { provider: [null, [Validators.required]], layerType: [null, [Validators.required]], tileUrl: [null, [Validators.required]], - apiKey: [null, [Validators.required]] + apiKey: [null, [Validators.required]], + referenceLayer: [null, []] }); this.layerFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html index c30033f7e1..cf4c4b90c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html @@ -82,6 +82,17 @@ +
+
widgets.maps.layer.reference.reference-layer
+ + + {{ 'widgets.maps.layer.reference.no-layer' | translate }} + + {{ referenceLayerTypeTranslationMap.get(layer) | translate }} + + + +