diff --git a/src/helpers.ts b/src/helpers.ts index 86ffd31..38f50ce 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -4,6 +4,9 @@ import { createGeoPoint, type GeoPoint, type GeoPointInterface, + overlayStackBeforeId, + type OverlayKind, + type OverlayStackKey, type PolygonState, } from '@mapconductor/js-sdk-core'; import * as atlas from 'azure-maps-control'; @@ -14,19 +17,58 @@ export type Position = [number, number]; /** Id of the marker `SymbolLayer` (see AzureMapsMarkerOverlayRenderer). */ export const MARKER_SYMBOL_LAYER_ID = 'mc-marker-symbols-layer'; +// Stacking keys of the layers this SDK added, per map, by layer id. +const stackKeys = new WeakMap>(); +const stackSequences = new WeakMap(); + +const MARKER_TILES_STATE_ID = 'mc-marker-tiles'; + +function stackKeyOf(map: atlas.Map, layerId: string): OverlayStackKey | null { + if (layerId === MARKER_SYMBOL_LAYER_ID) return { kind: 'marker', sub: 1 }; + return stackKeys.get(map)?.get(layerId) ?? null; +} + /** - * Add vector-overlay layers just below the marker layer so markers always draw - * on top of shapes (as DOM HtmlMarkers used to). If the marker layer does not - * exist yet, the layers are simply added on top. + * Adds an overlay's layers at its place in the shared stack (js-sdk-core OVERLAY_STACK_ORDER): + * raster < ground image < polygon < circle < polyline < marker, zIndex within the band, and + * equal zIndexes in the order they were handed over. The layers of one overlay (fill, stroke) + * stay together in the order given. + * + * Every overlay used to go "just below the marker layer", so the order between kinds was the + * order they were added in; rasters went on top of everything, markers included. */ -export function addLayersBelowMarkers( +export function addStackedLayers( map: atlas.Map, layers: atlas.layer.Layer | atlas.layer.Layer[], + kind: OverlayKind, + zIndex: number | null | undefined = 0, ): void { - const hasMarkerLayer = map.layers + const list = Array.isArray(layers) ? layers : [layers]; + let keys = stackKeys.get(map); + if (!keys) { + keys = new Map(); + stackKeys.set(map, keys); + } + const sequence = (stackSequences.get(map) ?? 0) + 1; + stackSequences.set(map, sequence); + list.forEach((layer, sub) => keys!.set(layer.getId(), { kind, zIndex: zIndex ?? 0, sequence, sub })); + const order = map.layers .getLayers() - .some(layer => typeof layer.getId === 'function' && layer.getId() === MARKER_SYMBOL_LAYER_ID); - map.layers.add(layers, hasMarkerLayer ? MARKER_SYMBOL_LAYER_ID : undefined); + .map((layer) => (typeof layer.getId === 'function' ? layer.getId() : '')); + const beforeId = overlayStackBeforeId(order, (id) => stackKeyOf(map, id), keys.get(list[0].getId())!); + map.layers.add(list, beforeId); +} + +/** Marker tiles are mounted as a raster but belong with the markers. */ +export function rasterStackKind(stateId: string): OverlayKind { + return stateId === MARKER_TILES_STATE_ID ? 'marker' : 'rasterLayer'; +} + +/** Forget the keys of layers that were removed. */ +export function forgetStackedLayers(map: atlas.Map, layers: atlas.layer.Layer | atlas.layer.Layer[]): void { + const keys = stackKeys.get(map); + if (!keys) return; + for (const layer of Array.isArray(layers) ? layers : [layers]) keys.delete(layer.getId()); } export function toPosition(point: GeoPointInterface): Position { diff --git a/src/raster/AzureMapsRasterLayer.ts b/src/raster/AzureMapsRasterLayer.ts index da156d6..00aba3f 100644 --- a/src/raster/AzureMapsRasterLayer.ts +++ b/src/raster/AzureMapsRasterLayer.ts @@ -1,3 +1,4 @@ +import { addStackedLayers, forgetStackedLayers, rasterStackKind } from '../helpers'; import { RasterLayerController, RasterLayerManager, @@ -60,15 +61,17 @@ export class AzureMapsRasterLayerRenderer { const options = tileLayerOptions(state); if (!options) return null; const layer = new atlas.layer.TileLayer(options); - // Insert raster tiles beneath interactive overlays where possible: place it - // before the first existing non-base layer so markers/vectors stay on top. - this.holder.map.layers.add(layer); + // The lowest overlay band: under ground images, shapes and markers. It used to be added on + // top of everything, markers included. + const kind = rasterStackKind(state.id); + addStackedLayers(this.holder.map, layer, kind, kind === 'marker' ? 0 : state.zIndex); return layer; } private remove(layer: atlas.layer.TileLayer): void { try { this.holder.map.layers.remove(layer); + forgetStackedLayers(this.holder.map, layer); } catch { // Layer may already be gone if the map/style was torn down. } diff --git a/src/vector/AzureMapsVectorControllers.ts b/src/vector/AzureMapsVectorControllers.ts index 9a63f57..07b16ca 100644 --- a/src/vector/AzureMapsVectorControllers.ts +++ b/src/vector/AzureMapsVectorControllers.ts @@ -23,7 +23,7 @@ import { } from '@mapconductor/js-sdk-core'; import * as atlas from 'azure-maps-control'; import { AzureMapsMapViewHolder } from '../AzureMapsMapViewHolder'; -import { addLayersBelowMarkers, closeRingPositions, pathToPositions, polygonRingsToPositions } from '../helpers'; +import { addStackedLayers, closeRingPositions, forgetStackedLayers, pathToPositions, polygonRingsToPositions } from '../helpers'; /** * A rendered vector overlay on Azure Maps: a DataSource (or none, for image @@ -70,7 +70,7 @@ export class AzureMapsCircleRenderer extends AbstractCircleOverlayRenderer { map.layers.remove([fill, stroke]); + forgetStackedLayers(map, [fill, stroke]); map.sources.remove(source); }, }; @@ -131,7 +132,7 @@ export class AzureMapsPolylineRenderer extends AbstractPolylineOverlayRenderer { map.layers.remove(line); + forgetStackedLayers(map, line); map.sources.remove(source); }, }; @@ -192,7 +194,7 @@ export class AzureMapsPolygonRenderer extends AbstractPolygonOverlayRenderer { map.layers.remove([fill, stroke]); + forgetStackedLayers(map, [fill, stroke]); map.sources.remove(source); }, }; @@ -283,7 +286,7 @@ export class AzureMapsGroundImageRenderer extends AbstractGroundImageOverlayRend coordinates, opacity: state.opacity, }); - addLayersBelowMarkers(map, layer); + addStackedLayers(map, layer, 'groundImage'); // Clicks are resolved geometrically from the tapped coordinate in // AzureMapsViewController (bounds containment), not via Azure's per-layer // hit-testing, so no layer click handler is attached here. @@ -292,6 +295,7 @@ export class AzureMapsGroundImageRenderer extends AbstractGroundImageOverlayRend sourceUrl: state.imageUrl, cleanup: () => { map.layers.remove(layer); + forgetStackedLayers(map, layer); image.revoke(); }, };