From eaadb34a731ac1c9a197ab561608689e55331e68 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 layer this SDK adds now goes in with the beforeId from overlayLayerBeforeId() (js-sdk-core overlayStackBeforeId), so the stack is raster < ground image < polygon < circle < polyline < marker whatever order overlays were added in, re-sent in after clearOverlays(), or put back in after a style swap. Before: - circles went in before the polygon fill, so under the polygons; - ground images and polylines were appended at the top, so a ground image added (or re-sent after clearOverlays()) after the polygons covered them and the polyline; - a raster went under the first basemap label only, not under our layers when those sat below the labels. Rasters keep going under the basemap labels as well (unchanged), and are ordered by zIndex among themselves; a zIndex change moves the layer. Marker tiles, mounted as a raster, sit with the markers. Labels stay under the ground images, shapes and markers (unchanged). OverlayStackOrder.test.mjs drives the real layer classes on a fake GL map: all 120 insertion orders, a ground image and raster re-sent after the others, a style swap, raster zIndex. It fails on the old code (circles under polygons, the re-sent ground image on top) and passes now. Needs the js-sdk-core release with overlayStackBeforeId (fix/overlay-stacking-order). --- api/react-for-tomtom.api.d.ts | 13 ++ src/circle/TomTomCircleLayer.ts | 13 +- .../TomTomGroundImageOverlayRenderer.ts | 24 ++- src/helpers.ts | 84 +++++++- src/polygon/TomTomPolygonLayer.ts | 14 +- src/polyline/TomTomPolylineLayer.ts | 4 +- .../TomTomRasterLayerOverlayRenderer.ts | 45 ++++- test/OverlayStackOrder.test.mjs | 187 ++++++++++++++++++ test/PolygonController.test.mjs | 2 + 9 files changed, 355 insertions(+), 31 deletions(-) create mode 100644 test/OverlayStackOrder.test.mjs diff --git a/api/react-for-tomtom.api.d.ts b/api/react-for-tomtom.api.d.ts index 65e8473..616fd86 100644 --- a/api/react-for-tomtom.api.d.ts +++ b/api/react-for-tomtom.api.d.ts @@ -415,6 +415,12 @@ declare class TomTomGroundImageOverlayRenderer extends AbstractGroundImageOverla /** Sync an already-created image source+layer to the current state (diffed). */ private applyToExisting; removeGroundImage(entity: GroundImageEntity): Promise; + /** + * Under the polygons, circles, polylines and markers, above the rasters, whatever order the + * overlays were added in. Ground images have no zIndex; among themselves they keep the order + * they were handed over in. + */ + private groundImageBeforeId; private removeFromStyle; } @@ -470,6 +476,13 @@ declare class TomTomRasterLayerOverlayRenderer implements RasterLayerOverlayRend onCameraChanged(_mapCameraPosition: MapCameraPosition): Promise; onPostProcess(): Promise; private addLayer; + /** This raster's place in the overlay stack: the lowest band, ordered by zIndex. */ + private stackKey; + /** + * Under every other overlay (the raster band is the lowest), and under the basemap's labels, + * whichever comes first. Marker tiles are drawn as a raster but belong with the markers. + */ + private beforeId; /** * The style's first label layer, so a raster overlay can go under it. * diff --git a/src/circle/TomTomCircleLayer.ts b/src/circle/TomTomCircleLayer.ts index c1a3a6d..2f2b5d3 100644 --- a/src/circle/TomTomCircleLayer.ts +++ b/src/circle/TomTomCircleLayer.ts @@ -3,7 +3,7 @@ import type { LayerSpecification, } from 'maplibre-gl'; import { type CircleEntity } from '@mapconductor/js-sdk-core'; -import { bringMarkerLayersToFront, setSourceFeatures, type FeatureCollection, type PolygonFeature } from '../helpers'; +import { bringMarkerLayersToFront, setSourceFeatures, type FeatureCollection, type PolygonFeature, overlayLayerBeforeId, overlayStackKeyOf } from '../helpers'; import { TomTomMapViewHolder } from '../TomTomMapViewHolder'; export type TomTomActualCircle = PolygonFeature & { id?: string | number }; @@ -73,14 +73,8 @@ export class TomTomCircleLayer { data: { type: 'FeatureCollection', features: [] }, }); } - const beforeId = map.getLayer('polygon-fill-layer') - ? 'polygon-fill-layer' - : map.getLayer('polyline-layer') - ? 'polyline-layer' - : map.getLayer('mc-marker-layer') - ? 'mc-marker-layer' - : undefined; if (!map.getLayer(this.layerId)) { + const beforeId = overlayLayerBeforeId(map, overlayStackKeyOf(map, this.layerId) ?? { kind: 'circle', sub: 0 }); map.addLayer( { id: this.layerId, @@ -97,8 +91,7 @@ export class TomTomCircleLayer { ); } if (!map.getLayer(this.strokeLayerId)) { - // Added with the same beforeId after the fill layer, so it renders - // directly above the fill layer. + const beforeId = overlayLayerBeforeId(map, overlayStackKeyOf(map, this.strokeLayerId) ?? { kind: 'circle', sub: 1 }); map.addLayer( { id: this.strokeLayerId, diff --git a/src/groundimage/TomTomGroundImageOverlayRenderer.ts b/src/groundimage/TomTomGroundImageOverlayRenderer.ts index 1d4ae38..8d59091 100644 --- a/src/groundimage/TomTomGroundImageOverlayRenderer.ts +++ b/src/groundimage/TomTomGroundImageOverlayRenderer.ts @@ -4,7 +4,15 @@ import { type GroundImageEntity, type GroundImageState, } from '@mapconductor/js-sdk-core'; -import { bringMarkerLayersToFront, groundImageCoordinates, removeLayerIfExists, removeSourceIfExists } from '../helpers'; +import { + bringMarkerLayersToFront, + forgetOverlayStackKey, + groundImageCoordinates, + overlayLayerBeforeId, + rememberOverlayStackKey, + removeLayerIfExists, + removeSourceIfExists, +} from '../helpers'; import { TomTomMapViewHolder } from '../TomTomMapViewHolder'; type Coordinates = ReturnType; @@ -66,7 +74,7 @@ export class TomTomGroundImageOverlayRenderer extends AbstractGroundImageOverlay type: 'raster', source: sourceId, paint: { 'raster-opacity': state.opacity }, - } as LayerSpecification); + } as LayerSpecification, this.groundImageBeforeId(layerId)); } bringMarkerLayersToFront(this.holder.map); this.applied.set(state.id, { @@ -88,7 +96,7 @@ export class TomTomGroundImageOverlayRenderer extends AbstractGroundImageOverlay type: 'raster', source: sourceId, paint: { 'raster-opacity': state.opacity }, - } as LayerSpecification); + } as LayerSpecification, this.groundImageBeforeId(layerId)); } this.applyToExisting(state, source, layerId, coordinates); bringMarkerLayersToFront(this.holder.map); @@ -167,7 +175,17 @@ export class TomTomGroundImageOverlayRenderer extends AbstractGroundImageOverlay this.pendingRemovals.clear(); } + /** + * Under the polygons, circles, polylines and markers, above the rasters, whatever order the + * overlays were added in. Ground images have no zIndex; among themselves they keep the order + * they were handed over in. + */ + private groundImageBeforeId(layerId: string): string | undefined { + return overlayLayerBeforeId(this.holder.map, rememberOverlayStackKey(this.holder.map, layerId, 'groundImage')); + } + private removeFromStyle(id: string): void { + forgetOverlayStackKey(this.holder.map, this.layerId(id)); removeLayerIfExists(this.holder.map, this.layerId(id)); removeSourceIfExists(this.holder.map, this.sourceId(id)); } diff --git a/src/helpers.ts b/src/helpers.ts index 48cd857..af04659 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,6 @@ import type * as maplibregl from 'maplibre-gl'; import type { SourceSpecification } from 'maplibre-gl'; -import { createGeoPoint, TileScheme, type GeoPoint, type GroundImageState, type PolygonState, RasterLayerSource } from '@mapconductor/js-sdk-core'; +import { createGeoPoint, TileScheme, type GeoPoint, type GroundImageState, type PolygonState, RasterLayerSource, overlayStackBeforeId, type OverlayKind, type OverlayStackKey } from '@mapconductor/js-sdk-core'; export type Coordinate = [number, number]; export type GeoJSONSourceData = Parameters[0]; @@ -93,6 +93,88 @@ export function bringMarkerLayersToFront(map: maplibregl.Map): void { if (arranged) arrangedOrders.set(map, arranged.join('\n')); } +// ── Overlay stacking ─────────────────────────────────────────────────────── +// +// Bottom to top, on every provider: raster < ground image < polygon < circle < polyline < +// marker (js-sdk-core OVERLAY_STACK_ORDER). Every layer this SDK adds goes in with the +// beforeId from overlayLayerBeforeId(), so the order does not depend on the order things were +// added, re-sent after clearOverlays(), or put back after a style swap. It used to: polygons +// went in "before the polyline layer", circles "before the polygon fill" (under polygons), +// ground images and polylines at the top, so a ground image re-sent after the polygons covered +// them, and a raster added after clearOverlays() could cover everything. + +export const MARKER_TILES_LAYER_ID = 'mc-raster-lyr-mc-marker-tiles'; + +/** Layers with a fixed place: one per kind, holding every overlay of that kind. */ +const FIXED_STACK_KEYS: Record = { + 'polygon-fill-layer': { kind: 'polygon', sub: 0 }, + 'polygon-outline-layer': { kind: 'polygon', sub: 1 }, + 'circle-layer': { kind: 'circle', sub: 0 }, + 'circle-layer-stroke': { kind: 'circle', sub: 1 }, + 'polyline-layer': { kind: 'polyline' }, + [MARKER_TILES_LAYER_ID]: { kind: 'marker', sub: 0 }, + 'mc-marker-layer': { kind: 'marker', sub: 1 }, + 'mc-marker-drag-layer': { kind: 'marker', sub: 2 }, +}; + +// Layers holding one overlay each (rasters, ground images), per map: their zIndex and the +// order they were handed over in. +const itemStackKeys = new WeakMap>(); +const itemSequences = new WeakMap(); + +/** Where a layer of ours belongs in the stack; null for the style's own layers. */ +export function overlayStackKeyOf(map: object, layerId: string): OverlayStackKey | null { + const fixed = FIXED_STACK_KEYS[layerId]; + if (fixed) return fixed; + const item = itemStackKeys.get(map)?.get(layerId); + if (item) return item; + if (layerId.startsWith('mc-raster-lyr-')) return { kind: 'rasterLayer' }; + if (layerId.startsWith('mc-gimg-lyr-')) return { kind: 'groundImage' }; + return null; +} + +/** + * Records the key of a layer that holds one overlay. A layer re-created for the same overlay + * (a raster whose source changed, a style swap) keeps the place it had among equal zIndexes. + */ +export function rememberOverlayStackKey(map: object, layerId: string, kind: OverlayKind, zIndex = 0): OverlayStackKey { + let keys = itemStackKeys.get(map); + if (!keys) { + keys = new Map(); + itemStackKeys.set(map, keys); + } + let sequence = keys.get(layerId)?.sequence; + if (sequence == null) { + sequence = (itemSequences.get(map) ?? 0) + 1; + itemSequences.set(map, sequence); + } + const key: OverlayStackKey = { kind, zIndex, sequence }; + keys.set(layerId, key); + return key; +} + +/** The overlay is gone (not just its layer): a later one with the same id starts over. */ +export function forgetOverlayStackKey(map: object, layerId: string): void { + itemStackKeys.get(map)?.delete(layerId); +} + +/** The beforeId that puts a new layer with this key in its place in the stack. */ +export function overlayLayerBeforeId(map: maplibregl.Map, key: OverlayStackKey): string | undefined { + return overlayStackBeforeId(currentLayerOrder(map) ?? [], (id) => overlayStackKeyOf(map, id), key); +} + +/** Moves an existing layer of ours to its place (after its zIndex changed). */ +export function moveLayerToStackPlace(map: maplibregl.Map, layerId: string, key: OverlayStackKey): void { + const order = currentLayerOrder(map); + if (!order || !order.includes(layerId)) return; + const rest = order.filter((id) => id !== layerId); + const beforeId = overlayStackBeforeId(rest, (id) => overlayStackKeyOf(map, id), key); + const at = order.indexOf(layerId); + const next = order[at + 1]; + if (next === beforeId) return; // already there: moveLayer would fire styledata for nothing + map.moveLayer(layerId, beforeId); +} + // What each GeoJSON source was last given, keyed by the source object itself. A style swap // replaces the source objects, so a lost source never matches and is written again. const writtenFeatures = new WeakMap(); diff --git a/src/polygon/TomTomPolygonLayer.ts b/src/polygon/TomTomPolygonLayer.ts index fb2b911..090c511 100644 --- a/src/polygon/TomTomPolygonLayer.ts +++ b/src/polygon/TomTomPolygonLayer.ts @@ -5,7 +5,7 @@ import type { LineFeature, PolygonFeature, } from '../helpers'; -import { bringMarkerLayersToFront, setSourceFeatures } from '../helpers'; +import { bringMarkerLayersToFront, overlayLayerBeforeId, overlayStackKeyOf, setSourceFeatures } from '../helpers'; import { TomTomMapViewHolder } from '../TomTomMapViewHolder'; export interface TomTomActualPolygon { @@ -101,11 +101,7 @@ export class TomTomPolygonLayer { }); } if (!map.getLayer(this.layerId)) { - const beforeId = map.getLayer('polyline-layer') - ? 'polyline-layer' - : map.getLayer('mc-marker-layer') - ? 'mc-marker-layer' - : undefined; + const beforeId = overlayLayerBeforeId(map, overlayStackKeyOf(map, this.layerId) ?? { kind: 'polygon', sub: 0 }); map.addLayer( { id: this.layerId, @@ -122,11 +118,7 @@ export class TomTomPolygonLayer { ); } if (!map.getLayer(this.outlineLayerId)) { - const beforeId = map.getLayer('polyline-layer') - ? 'polyline-layer' - : map.getLayer('mc-marker-layer') - ? 'mc-marker-layer' - : undefined; + const beforeId = overlayLayerBeforeId(map, overlayStackKeyOf(map, this.outlineLayerId) ?? { kind: 'polygon', sub: 1 }); map.addLayer( { id: this.outlineLayerId, diff --git a/src/polyline/TomTomPolylineLayer.ts b/src/polyline/TomTomPolylineLayer.ts index 3d3b1a0..dd3f6cc 100644 --- a/src/polyline/TomTomPolylineLayer.ts +++ b/src/polyline/TomTomPolylineLayer.ts @@ -1,6 +1,6 @@ import type { GeoJSONSource, LayerSpecification } from 'maplibre-gl'; import type { PolylineEntity } from '@mapconductor/js-sdk-core'; -import { bringMarkerLayersToFront, setSourceFeatures, type FeatureCollection, type LineFeature } from '../helpers'; +import { bringMarkerLayersToFront, setSourceFeatures, type FeatureCollection, type LineFeature, overlayLayerBeforeId, overlayStackKeyOf } from '../helpers'; import { TomTomMapViewHolder } from '../TomTomMapViewHolder'; export type TomTomActualPolyline = LineFeature[]; @@ -79,7 +79,7 @@ export class TomTomPolylineLayer { 'line-width': ['get', TomTomPolylineLayer.Prop.STROKE_WIDTH], }, } as LayerSpecification, - map.getLayer('mc-marker-layer') ? 'mc-marker-layer' : undefined, + overlayLayerBeforeId(map, overlayStackKeyOf(map, this.layerId) ?? { kind: 'polyline' }), ); } bringMarkerLayersToFront(map); diff --git a/src/raster/TomTomRasterLayerOverlayRenderer.ts b/src/raster/TomTomRasterLayerOverlayRenderer.ts index 7133732..e1e6bd4 100644 --- a/src/raster/TomTomRasterLayerOverlayRenderer.ts +++ b/src/raster/TomTomRasterLayerOverlayRenderer.ts @@ -7,7 +7,18 @@ import { type RasterLayerOverlayRenderer, type RasterLayerState, } from '@mapconductor/js-sdk-core'; -import { bringMarkerLayersToFront, createRasterSource, removeLayerIfExists, removeSourceIfExists } from '../helpers'; +import { + bringMarkerLayersToFront, + createRasterSource, + forgetOverlayStackKey, + MARKER_TILES_LAYER_ID, + moveLayerToStackPlace, + overlayLayerBeforeId, + overlayStackKeyOf, + rememberOverlayStackKey, + removeLayerIfExists, + removeSourceIfExists, +} from '../helpers'; import { TomTomMapViewHolder } from '../TomTomMapViewHolder'; /** GL のソース/レイヤー ID の対。android-sdk の TomTomRasterLayerHandle と同一。 */ @@ -53,10 +64,14 @@ export class TomTomRasterLayerOverlayRenderer const { prev } = params; const next = params.current.state; if (prev.state.source !== next.source) { - this.removeLayer(prev.layer); + // Replaced, not removed: the new layer keeps the old one's place among equal zIndexes. + this.removeLayer(prev.layer, { keepStackPlace: true }); return this.addLayer(next); } this.updateLayer(prev.layer, next); + if (prev.state.zIndex !== next.zIndex && prev.layer.layerId !== MARKER_TILES_LAYER_ID && this.canEditStyle()) { + moveLayerToStackPlace(this.holder.map, prev.layer.layerId, this.stackKey(prev.layer.layerId, next)); + } return prev.layer; }); bringMarkerLayersToFront(this.holder.map); @@ -98,7 +113,7 @@ export class TomTomRasterLayerOverlayRenderer source: handle.sourceId, paint: { 'raster-opacity': opacity }, } as LayerSpecification, - this.firstBasemapLabelLayerId(), + this.beforeId(handle.layerId, state), ); } else { this.holder.map.setPaintProperty(handle.layerId, 'raster-opacity', opacity); @@ -106,6 +121,27 @@ export class TomTomRasterLayerOverlayRenderer return handle; } + /** This raster's place in the overlay stack: the lowest band, ordered by zIndex. */ + private stackKey(layerId: string, state: RasterLayerState) { + return rememberOverlayStackKey(this.holder.map, layerId, 'rasterLayer', state.zIndex); + } + + /** + * Under every other overlay (the raster band is the lowest), and under the basemap's labels, + * whichever comes first. Marker tiles are drawn as a raster but belong with the markers. + */ + private beforeId(layerId: string, state: RasterLayerState): string | undefined { + if (layerId === MARKER_TILES_LAYER_ID) { + return overlayLayerBeforeId(this.holder.map, overlayStackKeyOf(this.holder.map, layerId)!); + } + const stack = overlayLayerBeforeId(this.holder.map, this.stackKey(layerId, state)); + const label = this.firstBasemapLabelLayerId(); + if (label == null) return stack; + if (stack == null) return label; + const layers = this.holder.map.getStyle?.()?.layers?.map((layer) => layer.id) ?? []; + return layers.indexOf(label) < layers.indexOf(stack) ? label : stack; + } + /** * The style's first label layer, so a raster overlay can go under it. * @@ -150,7 +186,8 @@ export class TomTomRasterLayerOverlayRenderer } } - private removeLayer(handle: TomTomRasterLayerHandle): void { + private removeLayer(handle: TomTomRasterLayerHandle, { keepStackPlace = false } = {}): void { + if (!keepStackPlace) forgetOverlayStackKey(this.holder.map, handle.layerId); if (!this.canEditStyle()) { this.pendingRemovals.push(handle); return; diff --git a/test/OverlayStackOrder.test.mjs b/test/OverlayStackOrder.test.mjs new file mode 100644 index 0000000..9f62f87 --- /dev/null +++ b/test/OverlayStackOrder.test.mjs @@ -0,0 +1,187 @@ +import assert from 'node:assert/strict'; +import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'; +import { after, test } from 'node:test'; +import { pathToFileURL } from 'node:url'; +import { resolve } from 'node:path'; +import { build } from 'tsup'; +import { + createGeoPoint, + createGeoRectBounds, + createGroundImageState, + createRasterLayerState, + RasterLayerSource, +} from '@mapconductor/js-sdk-core'; + +// The overlay stack, bottom to top, on every provider: raster < ground image < polygon < +// circle < polyline < marker (js-sdk-core OVERLAY_STACK_ORDER), whatever order the overlays +// were added in, re-sent in after clearOverlays(), or put back in after a style swap. +// +// It used to depend on all three: polygons went in "before the polyline layer", circles +// "before the polygon fill" (so under the polygons), ground images and polylines at the top. +// A ground image re-sent after clearOverlays() covered the polygons. +mkdirSync(new URL('../build/', import.meta.url), { recursive: true }); +const outDir = mkdtempSync(new URL('../build/stack-order-tests-', import.meta.url)); +after(() => rmSync(outDir, { recursive: true, force: true })); +await build({ + entry: { + polygon: 'src/polygon/TomTomPolygonLayer.ts', + circle: 'src/circle/TomTomCircleLayer.ts', + polyline: 'src/polyline/TomTomPolylineLayer.ts', + ground: 'src/groundimage/TomTomGroundImageOverlayRenderer.ts', + raster: 'src/raster/TomTomRasterLayerOverlayRenderer.ts', + }, + outDir, format: ['esm'], platform: 'node', splitting: false, + dts: false, sourcemap: false, config: false, silent: true, +}); +const load = async (name) => import(pathToFileURL(resolve(outDir, `${name}.mjs`))); +const { TomTomPolygonLayer } = await load('polygon'); +const { TomTomCircleLayer } = await load('circle'); +const { TomTomPolylineLayer } = await load('polyline'); +const { TomTomGroundImageOverlayRenderer } = await load('ground'); +const { TomTomRasterLayerOverlayRenderer } = await load('raster'); + +const BASEMAP = [ + { id: 'background', type: 'background' }, + { id: 'water', type: 'fill' }, + { id: 'place-label', type: 'symbol' }, +]; + +/** Enough of a GL map: an ordered layer list with addLayer(layer, beforeId). */ +function glMap() { + const sources = new Map(); + const layers = new Map(); + let order = []; + const add = (layer, before) => { + assert.ok(!layers.has(layer.id), `${layer.id} added twice`); + if (before != null) assert.ok(layers.has(before), `beforeId ${before} is not on the map`); + layers.set(layer.id, { paint: {}, ...layer }); + const at = before ? order.indexOf(before) : -1; + if (at >= 0) order.splice(at, 0, layer.id); else order.push(layer.id); + }; + const map = { + order: () => [...order], + swapStyle() { + sources.clear(); layers.clear(); order = []; + for (const layer of BASEMAP) add(layer); + }, + getStyle: () => ({ layers: order.map((id) => layers.get(id)) }), + getLayersOrder: () => [...order], + getSource: (id) => sources.get(id), + getLayer: (id) => layers.get(id), + addSource: (id, source) => { sources.set(id, { ...source, setData() {}, updateImage() {}, setCoordinates() {} }); }, + addLayer: (layer, before) => add(layer, before), + removeLayer: (id) => { layers.delete(id); order = order.filter((x) => x !== id); }, + removeSource: (id) => { sources.delete(id); }, + setPaintProperty: (id, property, value) => { layers.get(id).paint[property] = value; }, + setLayoutProperty() {}, + moveLayer: (id, before) => { + order = order.filter((x) => x !== id); + const at = before ? order.indexOf(before) : -1; + if (at >= 0) order.splice(at, 0, id); else order.push(id); + }, + }; + map.swapStyle(); + return map; +} + +function overlays(map) { + const holder = { map }; + const canEditStyle = () => true; + const polygon = new TomTomPolygonLayer({ holder, canEditStyle }); + const circle = new TomTomCircleLayer({ holder, canEditStyle }); + const polyline = new TomTomPolylineLayer({ holder, canEditStyle }); + const ground = new TomTomGroundImageOverlayRenderer({ holder, canEditStyle }); + const raster = new TomTomRasterLayerOverlayRenderer(holder, canEditStyle); + const gi = createGroundImageState({ + id: 'GI', + bounds: createGeoRectBounds({ + southWest: createGeoPoint({ latitude: 35.66, longitude: 139.73 }), + northEast: createGeoPoint({ latitude: 35.69, longitude: 139.77 }), + }), + imageUrl: '/image.png', + }); + const rl = createRasterLayerState({ + id: 'RL', + source: RasterLayerSource.UrlTemplate({ template: 'https://example.test/{z}/{x}/{y}.png', tileSize: 256 }), + }); + return { + raster: () => raster.onAdd([{ state: rl }]), + ground: () => ground.createGroundImage(gi), + polygon: () => polygon.draw([]), + circle: () => circle.draw([]), + polyline: () => polyline.draw([]), + rasterRenderer: raster, + groundRenderer: ground, + gi, rl, + }; +} + +// Ours, bottom to top. The raster also stays under the basemap's labels (unchanged). +const WANT = [ + 'mc-raster-lyr-RL', + 'place-label', + 'mc-gimg-lyr-GI', + 'polygon-fill-layer', + 'polygon-outline-layer', + 'circle-layer', + 'circle-layer-stroke', + 'polyline-layer', +]; + +const ours = (order) => order.filter((id) => WANT.includes(id)); + +function* permutations(items) { + if (items.length <= 1) { yield items; return; } + for (let i = 0; i < items.length; i++) { + for (const rest of permutations([...items.slice(0, i), ...items.slice(i + 1)])) yield [items[i], ...rest]; + } +} + +const KINDS = ['raster', 'ground', 'polygon', 'circle', 'polyline']; + +test('the stack is the same whatever order the overlays are added in', async () => { + for (const perm of permutations(KINDS)) { + const map = glMap(); + const o = overlays(map); + for (const kind of perm) await o[kind](); + assert.deepEqual(ours(map.order()), WANT, perm.join(' → ')); + } +}); + +test('a ground image and a raster re-sent after the others go back to their bands', async () => { + // What clearOverlays() + sending everything again does: the vector layers stay (their + // sources are only emptied), the ground image and the raster are removed and added again. + const map = glMap(); + const o = overlays(map); + for (const kind of KINDS) await o[kind](); + await o.groundRenderer.removeGroundImage({ groundImage: 'GI', state: o.gi }); + await o.rasterRenderer.onRemove([{ layer: { sourceId: 'mc-raster-src-RL', layerId: 'mc-raster-lyr-RL' }, state: o.rl }]); + await o.ground(); + await o.raster(); + assert.deepEqual(ours(map.order()), WANT); +}); + +test('after a style swap the overlays come back in their bands, in any order', async () => { + for (const perm of [[...KINDS].reverse(), ['polyline', 'raster', 'circle', 'ground', 'polygon']]) { + const map = glMap(); + const o = overlays(map); + for (const kind of KINDS) await o[kind](); + map.swapStyle(); + for (const kind of perm) await o[kind](); + assert.deepEqual(ours(map.order()), WANT, perm.join(' → ')); + } +}); + +test('rasters keep their zIndex order among themselves, under everything else', async () => { + const map = glMap(); + const o = overlays(map); + await o.polygon(); + const state = (id, zIndex) => createRasterLayerState({ + id, zIndex, + source: RasterLayerSource.UrlTemplate({ template: `https://example.test/${id}/{z}/{x}/{y}.png`, tileSize: 256 }), + }); + await o.rasterRenderer.onAdd([{ state: state('high', 2) }, { state: state('low', -1) }, { state: state('mid', 0) }]); + const order = map.order(); + const ids = ['mc-raster-lyr-low', 'mc-raster-lyr-mid', 'mc-raster-lyr-high', 'polygon-fill-layer']; + assert.deepEqual(order.filter((id) => ids.includes(id)), ids); +}); diff --git a/test/PolygonController.test.mjs b/test/PolygonController.test.mjs index 1486caf..f5a6370 100644 --- a/test/PolygonController.test.mjs +++ b/test/PolygonController.test.mjs @@ -38,6 +38,8 @@ function fixture({ existingLayers = [] } = {}) { const map = { getSource: (id) => sources.get(id), getLayer: (id) => order.find((layer) => layer.id === id), + // The real map's layer order: the stacking (overlayStackBeforeId) reads it to place new layers. + getLayersOrder: () => order.map((layer) => layer.id), addSource(id, spec) { assert.ok(ready, 'cannot add a source while the style is unavailable'); const source = { spec, data: spec.data, setData(data) { this.data = data; } };