From b28743fea6bac1ac1a136822145fc91a5ce02264 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-mappls.api.d.ts | 13 ++ src/circle/MapplsCircleLayer.ts | 13 +- .../MapplsGroundImageOverlayRenderer.ts | 24 ++- src/helpers.ts | 84 +++++++- src/polygon/MapplsPolygonLayer.ts | 14 +- src/polyline/MapplsPolylineLayer.ts | 4 +- .../MapplsRasterLayerOverlayRenderer.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-mappls.api.d.ts b/api/react-for-mappls.api.d.ts index b88ea53..b4cbce6 100644 --- a/api/react-for-mappls.api.d.ts +++ b/api/react-for-mappls.api.d.ts @@ -466,6 +466,12 @@ declare class MapplsGroundImageOverlayRenderer 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; } @@ -521,6 +527,13 @@ declare class MapplsRasterLayerOverlayRenderer 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/MapplsCircleLayer.ts b/src/circle/MapplsCircleLayer.ts index 15e3433..ea3b66b 100644 --- a/src/circle/MapplsCircleLayer.ts +++ b/src/circle/MapplsCircleLayer.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 { MapplsMapViewHolder } from '../MapplsMapViewHolder'; export type MapplsActualCircle = PolygonFeature & { id?: string | number }; @@ -73,14 +73,8 @@ export class MapplsCircleLayer { 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 MapplsCircleLayer { ); } 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/MapplsGroundImageOverlayRenderer.ts b/src/groundimage/MapplsGroundImageOverlayRenderer.ts index 31aa4ba..86ffa56 100644 --- a/src/groundimage/MapplsGroundImageOverlayRenderer.ts +++ b/src/groundimage/MapplsGroundImageOverlayRenderer.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 { MapplsMapViewHolder } from '../MapplsMapViewHolder'; type Coordinates = ReturnType; @@ -66,7 +74,7 @@ export class MapplsGroundImageOverlayRenderer 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 MapplsGroundImageOverlayRenderer 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 MapplsGroundImageOverlayRenderer 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/MapplsPolygonLayer.ts b/src/polygon/MapplsPolygonLayer.ts index 31ef258..d73a0d3 100644 --- a/src/polygon/MapplsPolygonLayer.ts +++ b/src/polygon/MapplsPolygonLayer.ts @@ -5,7 +5,7 @@ import type { LineFeature, PolygonFeature, } from '../helpers'; -import { bringMarkerLayersToFront, setSourceFeatures } from '../helpers'; +import { bringMarkerLayersToFront, overlayLayerBeforeId, overlayStackKeyOf, setSourceFeatures } from '../helpers'; import { MapplsMapViewHolder } from '../MapplsMapViewHolder'; export interface MapplsActualPolygon { @@ -101,11 +101,7 @@ export class MapplsPolygonLayer { }); } 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 MapplsPolygonLayer { ); } 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/MapplsPolylineLayer.ts b/src/polyline/MapplsPolylineLayer.ts index 8be9363..705e0b0 100644 --- a/src/polyline/MapplsPolylineLayer.ts +++ b/src/polyline/MapplsPolylineLayer.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 { MapplsMapViewHolder } from '../MapplsMapViewHolder'; export type MapplsActualPolyline = LineFeature[]; @@ -79,7 +79,7 @@ export class MapplsPolylineLayer { 'line-width': ['get', MapplsPolylineLayer.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/MapplsRasterLayerOverlayRenderer.ts b/src/raster/MapplsRasterLayerOverlayRenderer.ts index 2645289..5c0057f 100644 --- a/src/raster/MapplsRasterLayerOverlayRenderer.ts +++ b/src/raster/MapplsRasterLayerOverlayRenderer.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 { MapplsMapViewHolder } from '../MapplsMapViewHolder'; /** GL のソース/レイヤー ID の対。android-sdk の MapplsRasterLayerHandle と同一。 */ @@ -53,10 +64,14 @@ export class MapplsRasterLayerOverlayRenderer 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 MapplsRasterLayerOverlayRenderer 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 MapplsRasterLayerOverlayRenderer 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 MapplsRasterLayerOverlayRenderer } } - private removeLayer(handle: MapplsRasterLayerHandle): void { + private removeLayer(handle: MapplsRasterLayerHandle, { 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..dc307b6 --- /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/MapplsPolygonLayer.ts', + circle: 'src/circle/MapplsCircleLayer.ts', + polyline: 'src/polyline/MapplsPolylineLayer.ts', + ground: 'src/groundimage/MapplsGroundImageOverlayRenderer.ts', + raster: 'src/raster/MapplsRasterLayerOverlayRenderer.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 { MapplsPolygonLayer } = await load('polygon'); +const { MapplsCircleLayer } = await load('circle'); +const { MapplsPolylineLayer } = await load('polyline'); +const { MapplsGroundImageOverlayRenderer } = await load('ground'); +const { MapplsRasterLayerOverlayRenderer } = 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 MapplsPolygonLayer({ holder, canEditStyle }); + const circle = new MapplsCircleLayer({ holder, canEditStyle }); + const polyline = new MapplsPolylineLayer({ holder, canEditStyle }); + const ground = new MapplsGroundImageOverlayRenderer({ holder, canEditStyle }); + const raster = new MapplsRasterLayerOverlayRenderer(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 2e4a058..d60d859 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; } };