@orbat-mapper/tactical-draw-adapter-maplibre 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -143,6 +143,13 @@ interface MapLibreLayerHandle {
143
143
  sourceId: string;
144
144
  /** MapLibre layer ids created for this logical layer (fill, line, circle, symbol) */
145
145
  mlLayerIds: string[];
146
+ /**
147
+ * Anchor for overlay-band bundle layers while terrain is active — this
148
+ * handle's own `-circle` layer (ADR-0057). Stored explicitly rather than
149
+ * derived from `mlLayerIds` by position/suffix so the terrain-banding code
150
+ * doesn't depend on `createLayer`'s layer-creation order.
151
+ */
152
+ overlayAnchorId: string;
146
153
  groupBundles: Map<string, MapLibreGroupBundle>;
147
154
  groupOrder: string[];
148
155
  nextGroupBundleId: number;
@@ -214,13 +221,33 @@ interface MapLibreAdapterOptions {
214
221
  * the refit until the animation ends.
215
222
  */
216
223
  viewChangeMode?: "continuous" | "settle";
224
+ /**
225
+ * While MapLibre terrain is active, paint fill/line layers in one band below
226
+ * circle/icon/text/pin layers so terrain draping stays in few passes
227
+ * (ADR-0057). `false` keeps exact per-graphic paint order under terrain, at
228
+ * one draping pass per graphic. @default true
229
+ */
230
+ terrainBanding?: boolean;
217
231
  }
218
232
  declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLibreEventToken, MapLibreViewToken> {
219
233
  private map;
220
234
  private readonly viewChangeMode;
221
235
  private readonly pendingIconImages;
222
236
  readonly scalesGroundPointSymbolsNatively = true;
237
+ private terrainActive;
238
+ private terrainBanding;
239
+ private readonly handleTerrainChange;
223
240
  constructor(map: maplibregl.Map, options?: MapLibreAdapterOptions);
241
+ /** Switches `terrainBanding` at runtime, re-banding every group if the effective state changes. */
242
+ setTerrainBanding(enabled: boolean): void;
243
+ protected destroyEngineResources(): void;
244
+ /**
245
+ * Insertion anchor for a new bundle layer: below the handle's layers, or —
246
+ * for an overlay-band layer under terrain — below the overlay anchor, so it
247
+ * never lands between draped layers and fragments a render-to-texture stack
248
+ * (ADR-0057).
249
+ */
250
+ private insertionAnchor;
224
251
  private toContinuousLng;
225
252
  protected createLayer(layerId: LayerId, style?: StyleOptions): MapLibreLayerHandle;
226
253
  protected destroyLayer(handle: MapLibreLayerHandle): void;
@@ -234,6 +261,21 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
234
261
  private updateSurfaceSizing;
235
262
  private buildGroupLayer;
236
263
  private updateGroupBundleSource;
264
+ /**
265
+ * Reorders bundle layers below `handle.mlLayerIds`. Walks groups back to
266
+ * front with a `beforeId` cursor, the same LIS-based stable-subset
267
+ * optimization as before.
268
+ *
269
+ * While terrain is active (ADR-0057), bundle layers instead split into two
270
+ * independently-tracked bands so every graphic contributes at most two
271
+ * render-to-texture stacks instead of one per graphic: a draped band
272
+ * (image overlays, fill/fill-pattern/line surfaces) anchored below the
273
+ * handle's own `-fill` layer, and an overlay band (the pin layer,
274
+ * circle/icon/text surfaces) anchored below the handle's own `-circle`
275
+ * layer. Without terrain, both cursors stay equal throughout, collapsing
276
+ * back to the single-cursor algorithm and the exact atomic per-graphic
277
+ * order (ADR-0039).
278
+ */
237
279
  private orderGroupBundles;
238
280
  private nativeLayerIds;
239
281
  /**
package/dist/index.mjs CHANGED
@@ -1305,13 +1305,44 @@ function coordinatesEqual(a, b) {
1305
1305
  }
1306
1306
  return true;
1307
1307
  }
1308
- function firstBundleLayerId(bundle) {
1309
- for (const overlay of bundle.overlays.values()) return overlay.layerId;
1308
+ /**
1309
+ * Paint channels MapLibre renders into a terrain render-to-texture stack
1310
+ * (background/fill/line/raster/hillshade) rather than directly onto the
1311
+ * screen. See ADR-0057.
1312
+ */
1313
+ const DRAPED_CHANNELS = new Set([
1314
+ "fill",
1315
+ "fill-pattern",
1316
+ "line"
1317
+ ]);
1318
+ function isDrapedChannel(channel) {
1319
+ return DRAPED_CHANNELS.has(channel);
1320
+ }
1321
+ /**
1322
+ * Bottom-most layer of a bundle, used as the next (earlier) group's anchor in
1323
+ * `orderGroupBundles`. With a `band` (terrain active, ADR-0057), only that
1324
+ * band's layers count: image overlays are always draped, the pin layer is
1325
+ * always overlay, and paint surfaces follow `isDrapedChannel`.
1326
+ */
1327
+ function firstBundleLayerId(bundle, band) {
1328
+ if (bundle.pinLayer && band !== "draped") return bundle.pinLayer.id;
1329
+ if (band !== "overlay") for (const overlay of bundle.overlays.values()) return overlay.layerId;
1310
1330
  for (const key of bundle.paintOrder) {
1311
1331
  const surface = bundle.paintSurfaces.get(key);
1312
- if (surface) return surface.layerId;
1332
+ if (!surface) continue;
1333
+ if (!band || isDrapedChannel(surface.channel) === (band === "draped")) return surface.layerId;
1313
1334
  }
1314
1335
  }
1336
+ /** Hides an always-empty handle-level anchor layer; see `createLayer`. */
1337
+ function hiddenLayer(layer) {
1338
+ return {
1339
+ ...layer,
1340
+ layout: {
1341
+ ..."layout" in layer ? layer.layout : {},
1342
+ visibility: "none"
1343
+ }
1344
+ };
1345
+ }
1315
1346
  function sameOrder(a, b) {
1316
1347
  if (a.length !== b.length) return false;
1317
1348
  for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
@@ -1915,10 +1946,38 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1915
1946
  viewChangeMode;
1916
1947
  pendingIconImages = /* @__PURE__ */ new Set();
1917
1948
  scalesGroundPointSymbolsNatively = true;
1949
+ terrainActive = false;
1950
+ terrainBanding;
1951
+ handleTerrainChange = () => {
1952
+ const active = this.terrainBanding && !!this.map.getTerrain?.();
1953
+ if (active === this.terrainActive) return;
1954
+ this.terrainActive = active;
1955
+ for (const handle of this.layers.values()) this.orderGroupBundles(handle, handle.groupOrder, handle.groupOrder, new Set(handle.groupOrder));
1956
+ };
1918
1957
  constructor(map, options = {}) {
1919
1958
  super();
1920
1959
  this.map = map;
1921
1960
  this.viewChangeMode = options.viewChangeMode ?? "continuous";
1961
+ this.terrainBanding = options.terrainBanding ?? true;
1962
+ this.handleTerrainChange();
1963
+ this.map.on("terrain", this.handleTerrainChange);
1964
+ }
1965
+ /** Switches `terrainBanding` at runtime, re-banding every group if the effective state changes. */
1966
+ setTerrainBanding(enabled) {
1967
+ this.terrainBanding = enabled;
1968
+ this.handleTerrainChange();
1969
+ }
1970
+ destroyEngineResources() {
1971
+ this.map.off("terrain", this.handleTerrainChange);
1972
+ }
1973
+ /**
1974
+ * Insertion anchor for a new bundle layer: below the handle's layers, or —
1975
+ * for an overlay-band layer under terrain — below the overlay anchor, so it
1976
+ * never lands between draped layers and fragments a render-to-texture stack
1977
+ * (ADR-0057).
1978
+ */
1979
+ insertionAnchor(handle, draped) {
1980
+ return this.terrainActive && !draped ? handle.overlayAnchorId : handle.mlLayerIds[0];
1922
1981
  }
1923
1982
  toContinuousLng(lng) {
1924
1983
  return nearestWorldCopyLng(lng, this.map.getCenter().lng);
@@ -1932,25 +1991,25 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1932
1991
  });
1933
1992
  const mlLayerIds = [];
1934
1993
  const fillId = `${layerId}-fill`;
1935
- this.map.addLayer({
1994
+ this.map.addLayer(hiddenLayer({
1936
1995
  id: fillId,
1937
1996
  type: "fill",
1938
1997
  source: sourceId,
1939
1998
  filter: buildFillFilter(false),
1940
1999
  paint: buildFillPaint(style)
1941
- });
2000
+ }));
1942
2001
  mlLayerIds.push(fillId);
1943
2002
  const fillPatternId = `${layerId}-fill-pattern`;
1944
- this.map.addLayer({
2003
+ this.map.addLayer(hiddenLayer({
1945
2004
  id: fillPatternId,
1946
2005
  type: "fill",
1947
2006
  source: sourceId,
1948
2007
  filter: buildFillFilter(true),
1949
2008
  paint: FILL_PATTERN_PAINT
1950
- });
2009
+ }));
1951
2010
  mlLayerIds.push(fillPatternId);
1952
2011
  const lineId = `${layerId}-line`;
1953
- this.map.addLayer({
2012
+ this.map.addLayer(hiddenLayer({
1954
2013
  id: lineId,
1955
2014
  type: "line",
1956
2015
  source: sourceId,
@@ -1972,28 +2031,29 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1972
2031
  "line-cap": "round",
1973
2032
  "line-join": "round"
1974
2033
  }
1975
- });
2034
+ }));
1976
2035
  mlLayerIds.push(lineId);
1977
2036
  const circleId = `${layerId}-circle`;
1978
- this.map.addLayer({
2037
+ this.map.addLayer(hiddenLayer({
1979
2038
  id: circleId,
1980
2039
  type: "circle",
1981
2040
  source: sourceId,
1982
2041
  filter: buildCirclePointFilter(style),
1983
2042
  paint: buildCirclePaint(style)
1984
- });
2043
+ }));
1985
2044
  mlLayerIds.push(circleId);
1986
2045
  const iconId = `${layerId}-icon`;
1987
- this.map.addLayer(buildIconLayer(iconId, sourceId));
2046
+ this.map.addLayer(hiddenLayer(buildIconLayer(iconId, sourceId)));
1988
2047
  mlLayerIds.push(iconId);
1989
2048
  if (style?.text) {
1990
2049
  const symbolId = `${layerId}-symbol`;
1991
- this.map.addLayer(buildSymbolLayer(symbolId, sourceId, style.text));
2050
+ this.map.addLayer(hiddenLayer(buildSymbolLayer(symbolId, sourceId, style.text)));
1992
2051
  mlLayerIds.push(symbolId);
1993
2052
  }
1994
2053
  return {
1995
2054
  sourceId,
1996
2055
  mlLayerIds,
2056
+ overlayAnchorId: circleId,
1997
2057
  groupBundles: /* @__PURE__ */ new Map(),
1998
2058
  groupOrder: [],
1999
2059
  nextGroupBundleId: 0,
@@ -2062,7 +2122,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2062
2122
  if (group.pinBatch) {
2063
2123
  if (!bundle.pinLayer) {
2064
2124
  const custom = new PinLayer(`${bundle.sourceId}-pins`, handle.pinResources, this.emitPinStatus);
2065
- this.map.addLayer(custom, handle.mlLayerIds[0]);
2125
+ this.map.addLayer(custom, this.insertionAnchor(handle, false));
2066
2126
  bundle.pinLayer = custom;
2067
2127
  bundlesNeedingOrder.add(group.id);
2068
2128
  }
@@ -2236,7 +2296,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2236
2296
  featureId
2237
2297
  ]
2238
2298
  };
2239
- this.map.addLayer(layer, handle.mlLayerIds[0]);
2299
+ this.map.addLayer(layer, this.insertionAnchor(handle, isDrapedChannel(channel)));
2240
2300
  bundle.paintSurfaces.set(key, {
2241
2301
  layerId,
2242
2302
  channel,
@@ -2332,23 +2392,44 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2332
2392
  bundle.sourceInitialized = true;
2333
2393
  if (isPromiseLike(result)) Promise.resolve(result).catch(() => {});
2334
2394
  }
2395
+ /**
2396
+ * Reorders bundle layers below `handle.mlLayerIds`. Walks groups back to
2397
+ * front with a `beforeId` cursor, the same LIS-based stable-subset
2398
+ * optimization as before.
2399
+ *
2400
+ * While terrain is active (ADR-0057), bundle layers instead split into two
2401
+ * independently-tracked bands so every graphic contributes at most two
2402
+ * render-to-texture stacks instead of one per graphic: a draped band
2403
+ * (image overlays, fill/fill-pattern/line surfaces) anchored below the
2404
+ * handle's own `-fill` layer, and an overlay band (the pin layer,
2405
+ * circle/icon/text surfaces) anchored below the handle's own `-circle`
2406
+ * layer. Without terrain, both cursors stay equal throughout, collapsing
2407
+ * back to the single-cursor algorithm and the exact atomic per-graphic
2408
+ * order (ADR-0039).
2409
+ */
2335
2410
  orderGroupBundles(handle, previousGroupIds, desiredGroupIds, forcedGroupIds) {
2336
- let beforeId = handle.mlLayerIds[0];
2337
- if (!beforeId) return;
2411
+ const rootBeforeId = handle.mlLayerIds[0];
2412
+ if (!rootBeforeId) return;
2413
+ let drapedBeforeId = rootBeforeId;
2414
+ let overlayBeforeId = this.terrainActive ? handle.overlayAnchorId : rootBeforeId;
2338
2415
  const stableGroupIds = sameOrder(previousGroupIds, desiredGroupIds) ? null : longestStableGroupIds(previousGroupIds, desiredGroupIds);
2339
2416
  for (let index = desiredGroupIds.length - 1; index >= 0; index--) {
2340
2417
  const groupId = desiredGroupIds[index];
2341
2418
  const bundle = handle.groupBundles.get(groupId);
2342
2419
  if (!bundle) continue;
2343
2420
  if (forcedGroupIds.has(groupId) || stableGroupIds !== null && !stableGroupIds.has(groupId)) {
2344
- if (bundle.pinLayer && this.map.getLayer(bundle.pinLayer.id)) this.map.moveLayer(bundle.pinLayer.id, beforeId);
2345
- for (const overlay of bundle.overlays.values()) if (this.map.getLayer(overlay.layerId)) this.map.moveLayer(overlay.layerId, beforeId);
2421
+ if (bundle.pinLayer && this.map.getLayer(bundle.pinLayer.id)) this.map.moveLayer(bundle.pinLayer.id, overlayBeforeId);
2422
+ for (const overlay of bundle.overlays.values()) if (this.map.getLayer(overlay.layerId)) this.map.moveLayer(overlay.layerId, drapedBeforeId);
2346
2423
  for (const key of bundle.paintOrder) {
2347
2424
  const surface = bundle.paintSurfaces.get(key);
2348
- if (surface && this.map.getLayer(surface.layerId)) this.map.moveLayer(surface.layerId, beforeId);
2425
+ if (!surface || !this.map.getLayer(surface.layerId)) continue;
2426
+ this.map.moveLayer(surface.layerId, isDrapedChannel(surface.channel) ? drapedBeforeId : overlayBeforeId);
2349
2427
  }
2350
2428
  }
2351
- beforeId = bundle.pinLayer?.id ?? firstBundleLayerId(bundle) ?? beforeId;
2429
+ if (this.terrainActive) {
2430
+ drapedBeforeId = firstBundleLayerId(bundle, "draped") ?? drapedBeforeId;
2431
+ overlayBeforeId = firstBundleLayerId(bundle, "overlay") ?? overlayBeforeId;
2432
+ } else drapedBeforeId = overlayBeforeId = firstBundleLayerId(bundle) ?? drapedBeforeId;
2352
2433
  }
2353
2434
  }
2354
2435
  nativeLayerIds(handle) {
@@ -2589,7 +2670,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
2589
2670
  for (const bundle of handle.groupBundles.values()) for (const surface of bundle.paintSurfaces.values()) this.applyChannelStyle(surface.layerId, surface.channel, style, resolvedStyle, surface.featureId);
2590
2671
  if (style.text && !handle.mlLayerIds.some((id) => id.endsWith("-symbol"))) {
2591
2672
  const symbolId = `${layerId}-symbol`;
2592
- this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId, resolvedStyle.text));
2673
+ this.map.addLayer(hiddenLayer(buildSymbolLayer(symbolId, handle.sourceId, resolvedStyle.text)));
2593
2674
  handle.mlLayerIds.push(symbolId);
2594
2675
  }
2595
2676
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw-adapter-maplibre",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "MapLibre GL JS engine adapter for @orbat-mapper/tactical-draw. Draw and edit tactical control measures (MIL-STD-2525 / APP-6) on a MapLibre map.",
5
5
  "license": "MIT",
6
6
  "author": "Orbat Mapper",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@orbat-mapper/control-measures": "^0.27.0",
41
- "@orbat-mapper/tactical-draw": "^0.10.0"
41
+ "@orbat-mapper/tactical-draw": "^0.10.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "maplibre-gl": ">=5.8.0 <7.0.0"