@orbat-mapper/tactical-draw-adapter-maplibre 0.4.4 → 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 +58 -0
- package/dist/index.mjs +244 -41
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -62,6 +62,20 @@ declare class PinLayer implements CustomLayerInterface {
|
|
|
62
62
|
private atlas;
|
|
63
63
|
private pins;
|
|
64
64
|
private ranges;
|
|
65
|
+
private instanceData;
|
|
66
|
+
private sortedData;
|
|
67
|
+
private paintOrder;
|
|
68
|
+
private gpuOffsets;
|
|
69
|
+
private depths;
|
|
70
|
+
private sortMatrix;
|
|
71
|
+
private sortGlobeMatrix;
|
|
72
|
+
private sortTransition;
|
|
73
|
+
private sortWorld;
|
|
74
|
+
private sortDirty;
|
|
75
|
+
/** View-wide far-to-near paint order; `false` restores canonical layer order. */
|
|
76
|
+
private depthSorted;
|
|
77
|
+
/** Hoisted out of the per-frame sort so no comparator is allocated while panning. */
|
|
78
|
+
private comparePaintOrder;
|
|
65
79
|
private version;
|
|
66
80
|
private resourceRevision;
|
|
67
81
|
private needsDepth;
|
|
@@ -98,6 +112,8 @@ declare class PinLayer implements CustomLayerInterface {
|
|
|
98
112
|
private buildPin;
|
|
99
113
|
private rebuild;
|
|
100
114
|
render(context: WebGLRenderingContext | WebGL2RenderingContext, input: CustomRenderMethodInput): void;
|
|
115
|
+
/** Reorder existing instances only when camera/data changes, preserving atlas batches. */
|
|
116
|
+
private updatePaintOrder;
|
|
101
117
|
private draw;
|
|
102
118
|
/** On-demand GPU pick shares the visual shader including alpha and terrain mask. */
|
|
103
119
|
pick(pixel: PixelCoordinate, tolerance?: number): string | null;
|
|
@@ -127,6 +143,13 @@ interface MapLibreLayerHandle {
|
|
|
127
143
|
sourceId: string;
|
|
128
144
|
/** MapLibre layer ids created for this logical layer (fill, line, circle, symbol) */
|
|
129
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;
|
|
130
153
|
groupBundles: Map<string, MapLibreGroupBundle>;
|
|
131
154
|
groupOrder: string[];
|
|
132
155
|
nextGroupBundleId: number;
|
|
@@ -198,13 +221,33 @@ interface MapLibreAdapterOptions {
|
|
|
198
221
|
* the refit until the animation ends.
|
|
199
222
|
*/
|
|
200
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;
|
|
201
231
|
}
|
|
202
232
|
declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLibreEventToken, MapLibreViewToken> {
|
|
203
233
|
private map;
|
|
204
234
|
private readonly viewChangeMode;
|
|
205
235
|
private readonly pendingIconImages;
|
|
206
236
|
readonly scalesGroundPointSymbolsNatively = true;
|
|
237
|
+
private terrainActive;
|
|
238
|
+
private terrainBanding;
|
|
239
|
+
private readonly handleTerrainChange;
|
|
207
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;
|
|
208
251
|
private toContinuousLng;
|
|
209
252
|
protected createLayer(layerId: LayerId, style?: StyleOptions): MapLibreLayerHandle;
|
|
210
253
|
protected destroyLayer(handle: MapLibreLayerHandle): void;
|
|
@@ -218,6 +261,21 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
|
|
|
218
261
|
private updateSurfaceSizing;
|
|
219
262
|
private buildGroupLayer;
|
|
220
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
|
+
*/
|
|
221
279
|
private orderGroupBundles;
|
|
222
280
|
private nativeLayerIds;
|
|
223
281
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -327,6 +327,12 @@ const PIN_GLOBE_RADIUS = EARTH_RADIUS;
|
|
|
327
327
|
function pinMercator(position) {
|
|
328
328
|
return MercatorCoordinate.fromLngLat([position[0], Math.max(-89.999999, Math.min(89.999999, position[1]))]);
|
|
329
329
|
}
|
|
330
|
+
/** Scratch sphere position reused by the per-pin, per-frame depth() path. */
|
|
331
|
+
const depthSphere = [
|
|
332
|
+
0,
|
|
333
|
+
0,
|
|
334
|
+
0
|
|
335
|
+
];
|
|
330
336
|
const multiply = (m, v) => [
|
|
331
337
|
0,
|
|
332
338
|
1,
|
|
@@ -367,6 +373,34 @@ var PinProjection = class {
|
|
|
367
373
|
this.matrix[12 + row] = fallback[12 + row] + fallback[row] * xyScale * origin[0] * world + fallback[4 + row] * xyScale * origin[1] * world;
|
|
368
374
|
}
|
|
369
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Globe-sphere position for a world/elevation anchor, written into `out` so the
|
|
378
|
+
* per-pin depth path stays allocation-free. Single source of the sphere maths
|
|
379
|
+
* shared with project().
|
|
380
|
+
*/
|
|
381
|
+
sphere(worldX, worldY, elevation, out) {
|
|
382
|
+
const angle = (worldX / this.world + this.origin[0]) * Math.PI * 2 + Math.PI;
|
|
383
|
+
const t = Math.exp(Math.PI - (worldY / this.world + this.origin[1]) * Math.PI * 2);
|
|
384
|
+
const scale = 1 + elevation / PIN_GLOBE_RADIUS;
|
|
385
|
+
out[0] = Math.sin(angle) * 2 * t / (t * t + 1) * scale;
|
|
386
|
+
out[1] = (t * t - 1) / (t * t + 1) * scale;
|
|
387
|
+
out[2] = Math.cos(angle) * 2 * t / (t * t + 1) * scale;
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
/** Allocation-free depth-only projection for sorting large pin batches. */
|
|
391
|
+
depth(worldX, worldY, elevation) {
|
|
392
|
+
const m = this.matrix;
|
|
393
|
+
let z = m[2] * worldX + m[6] * worldY + m[10] * elevation + m[14];
|
|
394
|
+
let w = m[3] * worldX + m[7] * worldY + m[11] * elevation + m[15];
|
|
395
|
+
if (this.transition !== 0) {
|
|
396
|
+
const s = this.sphere(worldX, worldY, elevation, depthSphere);
|
|
397
|
+
const g = this.globeMatrix;
|
|
398
|
+
const blend = this.transition;
|
|
399
|
+
z = z * (1 - blend) + (g[2] * s[0] + g[6] * s[1] + g[10] * s[2] + g[14]) * blend;
|
|
400
|
+
w = w * (1 - blend) + (g[3] * s[0] + g[7] * s[1] + g[11] * s[2] + g[15]) * blend;
|
|
401
|
+
}
|
|
402
|
+
return z / w;
|
|
403
|
+
}
|
|
370
404
|
project(worldX, worldY, elevation) {
|
|
371
405
|
const flat = multiply(this.matrix, [
|
|
372
406
|
worldX,
|
|
@@ -378,16 +412,11 @@ var PinProjection = class {
|
|
|
378
412
|
clip: flat,
|
|
379
413
|
horizon: 1
|
|
380
414
|
};
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const sphere = [
|
|
387
|
-
Math.sin(angle) * 2 * t / (t * t + 1),
|
|
388
|
-
(t * t - 1) / (t * t + 1),
|
|
389
|
-
Math.cos(angle) * 2 * t / (t * t + 1)
|
|
390
|
-
].map((n) => n * scale);
|
|
415
|
+
const sphere = this.sphere(worldX, worldY, elevation, [
|
|
416
|
+
0,
|
|
417
|
+
0,
|
|
418
|
+
0
|
|
419
|
+
]);
|
|
391
420
|
const globe = multiply(this.globeMatrix, [...sphere, 1]);
|
|
392
421
|
const horizon = 1 - (1 - sphere.reduce((sum, n, i) => sum + n * this.clippingPlane[i], this.clippingPlane[3])) * Math.max(0, (this.transition - .2) / .8);
|
|
393
422
|
return {
|
|
@@ -439,22 +468,34 @@ function shader(gl, type, source) {
|
|
|
439
468
|
}
|
|
440
469
|
return s;
|
|
441
470
|
}
|
|
471
|
+
/** Copies `source` into `cache`, reporting whether any element differed. */
|
|
472
|
+
function cacheChanged(cache, source) {
|
|
473
|
+
let changed = false;
|
|
474
|
+
for (let i = 0; i < cache.length; i++) {
|
|
475
|
+
if (cache[i] !== source[i]) changed = true;
|
|
476
|
+
cache[i] = source[i];
|
|
477
|
+
}
|
|
478
|
+
return changed;
|
|
479
|
+
}
|
|
442
480
|
const elevationOrSeaLevel = (value) => Number.isFinite(value) ? value : 0;
|
|
443
481
|
/**
|
|
444
482
|
* Appends one instance's worth of vertex-shader input to `data` and extends `ranges`
|
|
445
483
|
* accordingly. Hoisted out of rebuild()'s per-pin loop (ADR n/a) so pushing 10k+ pins
|
|
446
484
|
* never allocates a fresh closure per pin — every varying value is an explicit param.
|
|
447
485
|
*/
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const first = data.length / STRIDE;
|
|
486
|
+
/** Extends the trailing draw range when it shares an atlas page, else starts a new one. */
|
|
487
|
+
function pushRange(ranges, page, first, count) {
|
|
451
488
|
const previous = ranges[ranges.length - 1];
|
|
452
|
-
if (previous?.page ===
|
|
489
|
+
if (previous?.page === page) previous.count += count;
|
|
453
490
|
else ranges.push({
|
|
454
|
-
page
|
|
491
|
+
page,
|
|
455
492
|
first,
|
|
456
|
-
count
|
|
493
|
+
count
|
|
457
494
|
});
|
|
495
|
+
}
|
|
496
|
+
function appendPinInstance(data, ranges, image, highlightOnly, instanceId, occlusionTerrain, kind, shape, color, a, target, mode) {
|
|
497
|
+
if (highlightOnly && kind !== PIN_KIND.HIGHLIGHT) return;
|
|
498
|
+
pushRange(ranges, image.page, data.length / STRIDE, 1);
|
|
458
499
|
data.push(...a, ...target, ...shape, ...color, ...image.uv, kind, mode, occlusionTerrain, instanceId);
|
|
459
500
|
}
|
|
460
501
|
/**
|
|
@@ -493,6 +534,20 @@ var PinLayer = class {
|
|
|
493
534
|
atlas = new PinAtlas();
|
|
494
535
|
pins = [];
|
|
495
536
|
ranges = [];
|
|
537
|
+
instanceData = new Float32Array(0);
|
|
538
|
+
sortedData = new Float32Array(0);
|
|
539
|
+
paintOrder = [];
|
|
540
|
+
gpuOffsets = [];
|
|
541
|
+
depths = new Float64Array(0);
|
|
542
|
+
sortMatrix = new Float64Array(16).fill(NaN);
|
|
543
|
+
sortGlobeMatrix = new Float64Array(16).fill(NaN);
|
|
544
|
+
sortTransition = NaN;
|
|
545
|
+
sortWorld = NaN;
|
|
546
|
+
sortDirty = true;
|
|
547
|
+
/** View-wide far-to-near paint order; `false` restores canonical layer order. */
|
|
548
|
+
depthSorted = false;
|
|
549
|
+
/** Hoisted out of the per-frame sort so no comparator is allocated while panning. */
|
|
550
|
+
comparePaintOrder = (a, b) => (this.depthSorted ? this.depths[b] - this.depths[a] : 0) || a - b;
|
|
496
551
|
version = 0;
|
|
497
552
|
resourceRevision = -1;
|
|
498
553
|
needsDepth = false;
|
|
@@ -646,6 +701,7 @@ var PinLayer = class {
|
|
|
646
701
|
}
|
|
647
702
|
this.pins = pins;
|
|
648
703
|
this.prioritizesSelection = pins[0]?.presentation.prioritizePins ?? false;
|
|
704
|
+
this.depthSorted = pins[0]?.presentation.depthSort ?? false;
|
|
649
705
|
if (this.elevationCache.size > pins.length * 60 + 1e3) this.elevationCache.clear();
|
|
650
706
|
if (!this.program) {
|
|
651
707
|
this.onStatus?.({
|
|
@@ -817,6 +873,7 @@ var PinLayer = class {
|
|
|
817
873
|
}
|
|
818
874
|
rebuild() {
|
|
819
875
|
if (!this.ready) return;
|
|
876
|
+
this.sortDirty = true;
|
|
820
877
|
if (!this.dirty) {
|
|
821
878
|
const previousPending = new Set(this.pendingElevation);
|
|
822
879
|
const center = {
|
|
@@ -833,8 +890,10 @@ var PinLayer = class {
|
|
|
833
890
|
this.dirty = true;
|
|
834
891
|
break;
|
|
835
892
|
}
|
|
893
|
+
const patch = new Float32Array(data);
|
|
894
|
+
this.instanceData.set(patch, slice.offset);
|
|
836
895
|
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
|
|
837
|
-
this.gl.bufferSubData(this.gl.ARRAY_BUFFER,
|
|
896
|
+
this.gl.bufferSubData(this.gl.ARRAY_BUFFER, this.gpuOffsets[i] * Float32Array.BYTES_PER_ELEMENT, patch);
|
|
838
897
|
}
|
|
839
898
|
if (!this.dirty) {
|
|
840
899
|
this.changedPins.clear();
|
|
@@ -874,7 +933,11 @@ var PinLayer = class {
|
|
|
874
933
|
});
|
|
875
934
|
this.ranges = ranges;
|
|
876
935
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
|
|
877
|
-
|
|
936
|
+
this.instanceData = new Float32Array(data);
|
|
937
|
+
this.paintOrder = this.pins.map((_, i) => i);
|
|
938
|
+
this.gpuOffsets = this.slices.map((slice) => slice.offset);
|
|
939
|
+
this.depths = new Float64Array(this.pins.length);
|
|
940
|
+
gl.bufferData(gl.ARRAY_BUFFER, this.instanceData, gl.DYNAMIC_DRAW);
|
|
878
941
|
this.dirty = false;
|
|
879
942
|
this.changedPins.clear();
|
|
880
943
|
}
|
|
@@ -893,8 +956,52 @@ var PinLayer = class {
|
|
|
893
956
|
if (this.status.status !== "supported") return;
|
|
894
957
|
this.frame = input;
|
|
895
958
|
this.projection.update(input, this.origin, this.map.getCenter(), 512 * 2 ** this.map.getZoom(), this.map.getProjection()?.type === "globe");
|
|
959
|
+
this.updatePaintOrder();
|
|
896
960
|
this.draw(context, false);
|
|
897
961
|
}
|
|
962
|
+
/** Reorder existing instances only when camera/data changes, preserving atlas batches. */
|
|
963
|
+
updatePaintOrder() {
|
|
964
|
+
const sorted = this.depthSorted;
|
|
965
|
+
let changed = this.sortDirty;
|
|
966
|
+
if (sorted) {
|
|
967
|
+
if (cacheChanged(this.sortMatrix, this.matrix)) changed = true;
|
|
968
|
+
if (cacheChanged(this.sortGlobeMatrix, this.projection.globeMatrix)) changed = true;
|
|
969
|
+
if (this.sortTransition !== this.projection.transition) changed = true;
|
|
970
|
+
if (this.sortWorld !== this.projection.world) changed = true;
|
|
971
|
+
this.sortTransition = this.projection.transition;
|
|
972
|
+
this.sortWorld = this.projection.world;
|
|
973
|
+
}
|
|
974
|
+
if (!changed) return;
|
|
975
|
+
this.sortDirty = false;
|
|
976
|
+
if (sorted) for (let i = 0; i < this.pins.length; i++) {
|
|
977
|
+
const offset = this.slices[i].offset;
|
|
978
|
+
const data = this.instanceData;
|
|
979
|
+
const depth = this.projection.depth(data[offset] * this.projection.world, data[offset + 1] * this.projection.world, this.elevations[i]);
|
|
980
|
+
this.depths[i] = Number.isFinite(depth) ? depth : -Infinity;
|
|
981
|
+
}
|
|
982
|
+
const compare = this.comparePaintOrder;
|
|
983
|
+
let ordered = true;
|
|
984
|
+
for (let i = 1; i < this.paintOrder.length; i++) if (compare(this.paintOrder[i - 1], this.paintOrder[i]) > 0) {
|
|
985
|
+
ordered = false;
|
|
986
|
+
break;
|
|
987
|
+
}
|
|
988
|
+
if (ordered) return;
|
|
989
|
+
this.paintOrder.sort(compare);
|
|
990
|
+
if (this.sortedData.length !== this.instanceData.length) this.sortedData = new Float32Array(this.instanceData.length);
|
|
991
|
+
const ranges = [];
|
|
992
|
+
let offset = 0;
|
|
993
|
+
for (const i of this.paintOrder) {
|
|
994
|
+
const slice = this.slices[i];
|
|
995
|
+
this.sortedData.set(this.instanceData.subarray(slice.offset, slice.offset + slice.length), offset);
|
|
996
|
+
this.gpuOffsets[i] = offset;
|
|
997
|
+
const count = slice.length / STRIDE;
|
|
998
|
+
if (count) pushRange(ranges, slice.page, offset / STRIDE, count);
|
|
999
|
+
offset += slice.length;
|
|
1000
|
+
}
|
|
1001
|
+
this.ranges = ranges;
|
|
1002
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
|
|
1003
|
+
this.gl.bufferSubData(this.gl.ARRAY_BUFFER, 0, this.sortedData);
|
|
1004
|
+
}
|
|
898
1005
|
draw(gl, pick) {
|
|
899
1006
|
const u = this.uniforms ?? this.resolveUniforms(gl);
|
|
900
1007
|
gl.useProgram(this.program);
|
|
@@ -985,7 +1092,10 @@ var PinLayer = class {
|
|
|
985
1092
|
const pixels = new Uint8Array(rw * rh * 4);
|
|
986
1093
|
gl.readPixels(x, y, rw, rh, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
987
1094
|
let id = 0;
|
|
988
|
-
for (let i = 0; i < pixels.length; i += 4)
|
|
1095
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1096
|
+
const candidate = pixels[i] + pixels[i + 1] * 256 + pixels[i + 2] * 65536;
|
|
1097
|
+
if (candidate && (!id || this.gpuOffsets[candidate - 1] > this.gpuOffsets[id - 1])) id = candidate;
|
|
1098
|
+
}
|
|
989
1099
|
gl.disable(gl.SCISSOR_TEST);
|
|
990
1100
|
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
|
|
991
1101
|
this.map.painter.context.setDirty();
|
|
@@ -1195,13 +1305,44 @@ function coordinatesEqual(a, b) {
|
|
|
1195
1305
|
}
|
|
1196
1306
|
return true;
|
|
1197
1307
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
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;
|
|
1200
1330
|
for (const key of bundle.paintOrder) {
|
|
1201
1331
|
const surface = bundle.paintSurfaces.get(key);
|
|
1202
|
-
if (surface)
|
|
1332
|
+
if (!surface) continue;
|
|
1333
|
+
if (!band || isDrapedChannel(surface.channel) === (band === "draped")) return surface.layerId;
|
|
1203
1334
|
}
|
|
1204
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
|
+
}
|
|
1205
1346
|
function sameOrder(a, b) {
|
|
1206
1347
|
if (a.length !== b.length) return false;
|
|
1207
1348
|
for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
|
|
@@ -1805,10 +1946,38 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1805
1946
|
viewChangeMode;
|
|
1806
1947
|
pendingIconImages = /* @__PURE__ */ new Set();
|
|
1807
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
|
+
};
|
|
1808
1957
|
constructor(map, options = {}) {
|
|
1809
1958
|
super();
|
|
1810
1959
|
this.map = map;
|
|
1811
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];
|
|
1812
1981
|
}
|
|
1813
1982
|
toContinuousLng(lng) {
|
|
1814
1983
|
return nearestWorldCopyLng(lng, this.map.getCenter().lng);
|
|
@@ -1822,25 +1991,25 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1822
1991
|
});
|
|
1823
1992
|
const mlLayerIds = [];
|
|
1824
1993
|
const fillId = `${layerId}-fill`;
|
|
1825
|
-
this.map.addLayer({
|
|
1994
|
+
this.map.addLayer(hiddenLayer({
|
|
1826
1995
|
id: fillId,
|
|
1827
1996
|
type: "fill",
|
|
1828
1997
|
source: sourceId,
|
|
1829
1998
|
filter: buildFillFilter(false),
|
|
1830
1999
|
paint: buildFillPaint(style)
|
|
1831
|
-
});
|
|
2000
|
+
}));
|
|
1832
2001
|
mlLayerIds.push(fillId);
|
|
1833
2002
|
const fillPatternId = `${layerId}-fill-pattern`;
|
|
1834
|
-
this.map.addLayer({
|
|
2003
|
+
this.map.addLayer(hiddenLayer({
|
|
1835
2004
|
id: fillPatternId,
|
|
1836
2005
|
type: "fill",
|
|
1837
2006
|
source: sourceId,
|
|
1838
2007
|
filter: buildFillFilter(true),
|
|
1839
2008
|
paint: FILL_PATTERN_PAINT
|
|
1840
|
-
});
|
|
2009
|
+
}));
|
|
1841
2010
|
mlLayerIds.push(fillPatternId);
|
|
1842
2011
|
const lineId = `${layerId}-line`;
|
|
1843
|
-
this.map.addLayer({
|
|
2012
|
+
this.map.addLayer(hiddenLayer({
|
|
1844
2013
|
id: lineId,
|
|
1845
2014
|
type: "line",
|
|
1846
2015
|
source: sourceId,
|
|
@@ -1862,28 +2031,29 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1862
2031
|
"line-cap": "round",
|
|
1863
2032
|
"line-join": "round"
|
|
1864
2033
|
}
|
|
1865
|
-
});
|
|
2034
|
+
}));
|
|
1866
2035
|
mlLayerIds.push(lineId);
|
|
1867
2036
|
const circleId = `${layerId}-circle`;
|
|
1868
|
-
this.map.addLayer({
|
|
2037
|
+
this.map.addLayer(hiddenLayer({
|
|
1869
2038
|
id: circleId,
|
|
1870
2039
|
type: "circle",
|
|
1871
2040
|
source: sourceId,
|
|
1872
2041
|
filter: buildCirclePointFilter(style),
|
|
1873
2042
|
paint: buildCirclePaint(style)
|
|
1874
|
-
});
|
|
2043
|
+
}));
|
|
1875
2044
|
mlLayerIds.push(circleId);
|
|
1876
2045
|
const iconId = `${layerId}-icon`;
|
|
1877
|
-
this.map.addLayer(buildIconLayer(iconId, sourceId));
|
|
2046
|
+
this.map.addLayer(hiddenLayer(buildIconLayer(iconId, sourceId)));
|
|
1878
2047
|
mlLayerIds.push(iconId);
|
|
1879
2048
|
if (style?.text) {
|
|
1880
2049
|
const symbolId = `${layerId}-symbol`;
|
|
1881
|
-
this.map.addLayer(buildSymbolLayer(symbolId, sourceId, style.text));
|
|
2050
|
+
this.map.addLayer(hiddenLayer(buildSymbolLayer(symbolId, sourceId, style.text)));
|
|
1882
2051
|
mlLayerIds.push(symbolId);
|
|
1883
2052
|
}
|
|
1884
2053
|
return {
|
|
1885
2054
|
sourceId,
|
|
1886
2055
|
mlLayerIds,
|
|
2056
|
+
overlayAnchorId: circleId,
|
|
1887
2057
|
groupBundles: /* @__PURE__ */ new Map(),
|
|
1888
2058
|
groupOrder: [],
|
|
1889
2059
|
nextGroupBundleId: 0,
|
|
@@ -1897,8 +2067,14 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1897
2067
|
if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
|
|
1898
2068
|
}
|
|
1899
2069
|
applyFeatureGroups(layerId, input) {
|
|
2070
|
+
const depthSorted = input.find((group) => group.pin)?.pin.presentation.depthSort ?? false;
|
|
2071
|
+
const sortedPins = [];
|
|
1900
2072
|
const groups = [];
|
|
1901
2073
|
for (const group of input) if (group.pin && this.getPinPresentationCapability().status !== "unsupported") {
|
|
2074
|
+
if (depthSorted) {
|
|
2075
|
+
sortedPins.push(group.pin);
|
|
2076
|
+
continue;
|
|
2077
|
+
}
|
|
1902
2078
|
const previous = groups[groups.length - 1];
|
|
1903
2079
|
if (previous?.pinBatch) previous.pinBatch.push(group.pin);
|
|
1904
2080
|
else groups.push({
|
|
@@ -1911,6 +2087,12 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1911
2087
|
...group,
|
|
1912
2088
|
id: `ordinary:${group.id}`
|
|
1913
2089
|
});
|
|
2090
|
+
if (sortedPins.length) groups.push({
|
|
2091
|
+
id: "pin-depth-sorted",
|
|
2092
|
+
features: [],
|
|
2093
|
+
iconResources: [],
|
|
2094
|
+
pinBatch: sortedPins
|
|
2095
|
+
});
|
|
1914
2096
|
const handle = this.layers.get(layerId);
|
|
1915
2097
|
if (!handle) return;
|
|
1916
2098
|
const allPins = groups.flatMap((group) => group.pinBatch ?? []);
|
|
@@ -1940,7 +2122,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1940
2122
|
if (group.pinBatch) {
|
|
1941
2123
|
if (!bundle.pinLayer) {
|
|
1942
2124
|
const custom = new PinLayer(`${bundle.sourceId}-pins`, handle.pinResources, this.emitPinStatus);
|
|
1943
|
-
this.map.addLayer(custom, handle
|
|
2125
|
+
this.map.addLayer(custom, this.insertionAnchor(handle, false));
|
|
1944
2126
|
bundle.pinLayer = custom;
|
|
1945
2127
|
bundlesNeedingOrder.add(group.id);
|
|
1946
2128
|
}
|
|
@@ -2114,7 +2296,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
2114
2296
|
featureId
|
|
2115
2297
|
]
|
|
2116
2298
|
};
|
|
2117
|
-
this.map.addLayer(layer, handle
|
|
2299
|
+
this.map.addLayer(layer, this.insertionAnchor(handle, isDrapedChannel(channel)));
|
|
2118
2300
|
bundle.paintSurfaces.set(key, {
|
|
2119
2301
|
layerId,
|
|
2120
2302
|
channel,
|
|
@@ -2210,23 +2392,44 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
2210
2392
|
bundle.sourceInitialized = true;
|
|
2211
2393
|
if (isPromiseLike(result)) Promise.resolve(result).catch(() => {});
|
|
2212
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
|
+
*/
|
|
2213
2410
|
orderGroupBundles(handle, previousGroupIds, desiredGroupIds, forcedGroupIds) {
|
|
2214
|
-
|
|
2215
|
-
if (!
|
|
2411
|
+
const rootBeforeId = handle.mlLayerIds[0];
|
|
2412
|
+
if (!rootBeforeId) return;
|
|
2413
|
+
let drapedBeforeId = rootBeforeId;
|
|
2414
|
+
let overlayBeforeId = this.terrainActive ? handle.overlayAnchorId : rootBeforeId;
|
|
2216
2415
|
const stableGroupIds = sameOrder(previousGroupIds, desiredGroupIds) ? null : longestStableGroupIds(previousGroupIds, desiredGroupIds);
|
|
2217
2416
|
for (let index = desiredGroupIds.length - 1; index >= 0; index--) {
|
|
2218
2417
|
const groupId = desiredGroupIds[index];
|
|
2219
2418
|
const bundle = handle.groupBundles.get(groupId);
|
|
2220
2419
|
if (!bundle) continue;
|
|
2221
2420
|
if (forcedGroupIds.has(groupId) || stableGroupIds !== null && !stableGroupIds.has(groupId)) {
|
|
2222
|
-
if (bundle.pinLayer && this.map.getLayer(bundle.pinLayer.id)) this.map.moveLayer(bundle.pinLayer.id,
|
|
2223
|
-
for (const overlay of bundle.overlays.values()) if (this.map.getLayer(overlay.layerId)) this.map.moveLayer(overlay.layerId,
|
|
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);
|
|
2224
2423
|
for (const key of bundle.paintOrder) {
|
|
2225
2424
|
const surface = bundle.paintSurfaces.get(key);
|
|
2226
|
-
if (surface
|
|
2425
|
+
if (!surface || !this.map.getLayer(surface.layerId)) continue;
|
|
2426
|
+
this.map.moveLayer(surface.layerId, isDrapedChannel(surface.channel) ? drapedBeforeId : overlayBeforeId);
|
|
2227
2427
|
}
|
|
2228
2428
|
}
|
|
2229
|
-
|
|
2429
|
+
if (this.terrainActive) {
|
|
2430
|
+
drapedBeforeId = firstBundleLayerId(bundle, "draped") ?? drapedBeforeId;
|
|
2431
|
+
overlayBeforeId = firstBundleLayerId(bundle, "overlay") ?? overlayBeforeId;
|
|
2432
|
+
} else drapedBeforeId = overlayBeforeId = firstBundleLayerId(bundle) ?? drapedBeforeId;
|
|
2230
2433
|
}
|
|
2231
2434
|
}
|
|
2232
2435
|
nativeLayerIds(handle) {
|
|
@@ -2467,7 +2670,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
2467
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);
|
|
2468
2671
|
if (style.text && !handle.mlLayerIds.some((id) => id.endsWith("-symbol"))) {
|
|
2469
2672
|
const symbolId = `${layerId}-symbol`;
|
|
2470
|
-
this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId, resolvedStyle.text));
|
|
2673
|
+
this.map.addLayer(hiddenLayer(buildSymbolLayer(symbolId, handle.sourceId, resolvedStyle.text)));
|
|
2471
2674
|
handle.mlLayerIds.push(symbolId);
|
|
2472
2675
|
}
|
|
2473
2676
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-draw-adapter-maplibre",
|
|
3
|
-
"version": "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.
|
|
41
|
+
"@orbat-mapper/tactical-draw": "^0.10.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"maplibre-gl": ">=5.8.0 <7.0.0"
|