From e6a7cec66af30e89dc34152781f8c756301aa6bc Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sun, 8 Nov 2026 00:17:19 -0800 Subject: [PATCH] Stack overlay layers in one fixed order Every overlay used to go "just below the marker layer", so the order between kinds was the order they were added in, and rasters were appended on top of everything, markers included. addStackedLayers() inserts an overlay's layers before the first layer of ours that belongs above it (js-sdk-core overlayStackBeforeId), giving raster < ground image < polygon < circle < polyline < marker, zIndex within a band, equal zIndexes in the order handed over. Marker tiles sit with the markers. Needs the js-sdk-core release with overlayStackBeforeId. --- src/helpers.ts | 56 +++++++++++++++++++++--- src/raster/AzureMapsRasterLayer.ts | 9 ++-- src/vector/AzureMapsVectorControllers.ts | 14 +++--- 3 files changed, 64 insertions(+), 15 deletions(-) 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(); }, };