diff --git a/api/react-for-longdo.api.d.ts b/api/react-for-longdo.api.d.ts index cea4a6d..25bfd5b 100644 --- a/api/react-for-longdo.api.d.ts +++ b/api/react-for-longdo.api.d.ts @@ -490,6 +490,12 @@ declare class LongdoGroundImageOverlayRenderer 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; } @@ -545,6 +551,13 @@ declare class LongdoRasterLayerOverlayRenderer 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/LongdoCircleLayer.ts b/src/circle/LongdoCircleLayer.ts index c27e0e5..bd0e375 100644 --- a/src/circle/LongdoCircleLayer.ts +++ b/src/circle/LongdoCircleLayer.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 { LongdoMapViewHolder } from '../LongdoMapViewHolder'; export type LongdoActualCircle = PolygonFeature & { id?: string | number }; @@ -73,14 +73,8 @@ export class LongdoCircleLayer { 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 LongdoCircleLayer { ); } 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/LongdoGroundImageOverlayRenderer.ts b/src/groundimage/LongdoGroundImageOverlayRenderer.ts index 223b85c..63e7b7f 100644 --- a/src/groundimage/LongdoGroundImageOverlayRenderer.ts +++ b/src/groundimage/LongdoGroundImageOverlayRenderer.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 { LongdoMapViewHolder } from '../LongdoMapViewHolder'; type Coordinates = ReturnType; @@ -66,7 +74,7 @@ export class LongdoGroundImageOverlayRenderer 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 LongdoGroundImageOverlayRenderer 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 LongdoGroundImageOverlayRenderer 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/LongdoPolygonLayer.ts b/src/polygon/LongdoPolygonLayer.ts index 2ebf709..e6dfb39 100644 --- a/src/polygon/LongdoPolygonLayer.ts +++ b/src/polygon/LongdoPolygonLayer.ts @@ -5,7 +5,7 @@ import type { LineFeature, PolygonFeature, } from '../helpers'; -import { bringMarkerLayersToFront, setSourceFeatures } from '../helpers'; +import { bringMarkerLayersToFront, overlayLayerBeforeId, overlayStackKeyOf, setSourceFeatures } from '../helpers'; import { LongdoMapViewHolder } from '../LongdoMapViewHolder'; export interface LongdoActualPolygon { @@ -101,11 +101,7 @@ export class LongdoPolygonLayer { }); } 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 LongdoPolygonLayer { ); } 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/LongdoPolylineLayer.ts b/src/polyline/LongdoPolylineLayer.ts index 6fedd38..a57e6d9 100644 --- a/src/polyline/LongdoPolylineLayer.ts +++ b/src/polyline/LongdoPolylineLayer.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 { LongdoMapViewHolder } from '../LongdoMapViewHolder'; export type LongdoActualPolyline = LineFeature[]; @@ -79,7 +79,7 @@ export class LongdoPolylineLayer { 'line-width': ['get', LongdoPolylineLayer.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/LongdoRasterLayerOverlayRenderer.ts b/src/raster/LongdoRasterLayerOverlayRenderer.ts index a6f4892..a980d74 100644 --- a/src/raster/LongdoRasterLayerOverlayRenderer.ts +++ b/src/raster/LongdoRasterLayerOverlayRenderer.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 { LongdoMapViewHolder } from '../LongdoMapViewHolder'; /** GL のソース/レイヤー ID の対。android-sdk の LongdoRasterLayerHandle と同一。 */ @@ -53,10 +64,14 @@ export class LongdoRasterLayerOverlayRenderer 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 LongdoRasterLayerOverlayRenderer 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 LongdoRasterLayerOverlayRenderer 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 LongdoRasterLayerOverlayRenderer } } - private removeLayer(handle: LongdoRasterLayerHandle): void { + private removeLayer(handle: LongdoRasterLayerHandle, { 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..90e580b --- /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/LongdoPolygonLayer.ts', + circle: 'src/circle/LongdoCircleLayer.ts', + polyline: 'src/polyline/LongdoPolylineLayer.ts', + ground: 'src/groundimage/LongdoGroundImageOverlayRenderer.ts', + raster: 'src/raster/LongdoRasterLayerOverlayRenderer.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 { LongdoPolygonLayer } = await load('polygon'); +const { LongdoCircleLayer } = await load('circle'); +const { LongdoPolylineLayer } = await load('polyline'); +const { LongdoGroundImageOverlayRenderer } = await load('ground'); +const { LongdoRasterLayerOverlayRenderer } = 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 LongdoPolygonLayer({ holder, canEditStyle }); + const circle = new LongdoCircleLayer({ holder, canEditStyle }); + const polyline = new LongdoPolylineLayer({ holder, canEditStyle }); + const ground = new LongdoGroundImageOverlayRenderer({ holder, canEditStyle }); + const raster = new LongdoRasterLayerOverlayRenderer(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 b3499ad..f84f15e 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; } };