@orbat-mapper/tactical-draw-adapter-maplibre 0.2.0-alpha.11 → 0.2.0-alpha.13
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 +22 -0
- package/dist/index.mjs +172 -17
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -25,12 +25,34 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
|
|
|
25
25
|
private map;
|
|
26
26
|
private readonly viewChangeMode;
|
|
27
27
|
private readonly pendingIconImages;
|
|
28
|
+
private readonly pendingLayerUpdates;
|
|
29
|
+
private readonly deferredLayerRemovals;
|
|
28
30
|
readonly scalesGroundPointSymbolsNatively = true;
|
|
29
31
|
constructor(map: maplibregl.Map, options?: MapLibreAdapterOptions);
|
|
30
32
|
private toContinuousLng;
|
|
31
33
|
protected createLayer(layerId: LayerId, style?: StyleOptions): MapLibreLayerHandle;
|
|
32
34
|
protected destroyLayer(handle: MapLibreLayerHandle): void;
|
|
33
35
|
protected applyFeatureOps(layerId: LayerId, ops: FeatureOps): void;
|
|
36
|
+
private updateSourceData;
|
|
37
|
+
private flushDeferredLayerRemoval;
|
|
38
|
+
deferLayerRemovalUntilLayerUpdate(layerId: LayerId, updatedLayerId: LayerId): void;
|
|
39
|
+
/**
|
|
40
|
+
* Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
|
|
41
|
+
* and picks can key on the caller's feature id) and, for a hatch-patterned
|
|
42
|
+
* Polygon, `_fillPatternImageId` (registering the tile image on demand). At
|
|
43
|
+
* most one object spread — a feature needing neither passes through
|
|
44
|
+
* untouched.
|
|
45
|
+
*/
|
|
46
|
+
private promoteFeature;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve a hatch-patterned Polygon's `style.fillPattern`/`fillColor` to its
|
|
49
|
+
* hatch-tile image id, registering the image on demand. Returns `undefined`
|
|
50
|
+
* for features without a hatch pattern, so `fillPatternImageId` stays the
|
|
51
|
+
* single source of truth for the id baked into `_fillPatternImageId`.
|
|
52
|
+
*/
|
|
53
|
+
private resolveFillPatternImage;
|
|
54
|
+
/** Register a hatch-tile image once per (color, pattern) pair, keyed by `fillPatternImageId`. */
|
|
55
|
+
private ensureFillPatternImage;
|
|
34
56
|
protected applyIconResources(resources: readonly IconResource[]): void;
|
|
35
57
|
protected removeIconResource(id: string): void;
|
|
36
58
|
protected attachNativeEvent(type: "click" | "pointermove" | "dblclick", onEvent: (lonLat: Position, pixel: PixelCoordinate, originalEvent: unknown) => void): NativeEventEntry<MapLibreEventToken>;
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { project, unproject } from "@orbat-mapper/control-measures";
|
|
2
|
-
import { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, bindAbortable, nearestWorldCopyLng, pickHitTolerance, tryControlMeasureIdFromFeature } from "@orbat-mapper/tactical-draw";
|
|
2
|
+
import { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, bindAbortable, fillPatternImageId, makeFillPatternImage, nearestWorldCopyLng, pickHitTolerance, tryControlMeasureIdFromFeature } from "@orbat-mapper/tactical-draw";
|
|
3
3
|
//#region src/ml-adapter.ts
|
|
4
|
+
/** Feature property key for the resolved hatch-pattern image id (see `promoteFeature`). */
|
|
5
|
+
const FILL_PATTERN_IMAGE_ID_PROPERTY = "_fillPatternImageId";
|
|
6
|
+
function isHatchPattern(value) {
|
|
7
|
+
return HATCH_PATTERNS.includes(value);
|
|
8
|
+
}
|
|
9
|
+
function isPromiseLike(value) {
|
|
10
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
11
|
+
}
|
|
4
12
|
function emptyFC() {
|
|
5
13
|
return {
|
|
6
14
|
type: "FeatureCollection",
|
|
@@ -306,6 +314,60 @@ function buildFillPaint(style) {
|
|
|
306
314
|
"fill-opacity": 1
|
|
307
315
|
};
|
|
308
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* `properties.style.fillPattern` read as a GL expression, defaulting to
|
|
319
|
+
* `"solid"`. `fillPattern` is a per-feature-only field (ADR-0007) — unlike
|
|
320
|
+
* `fillColor`/`strokeColor` it has no layer-level `StyleOptions` default, so
|
|
321
|
+
* this expression takes no `style` argument.
|
|
322
|
+
*/
|
|
323
|
+
function fillPatternField() {
|
|
324
|
+
return [
|
|
325
|
+
"coalesce",
|
|
326
|
+
[
|
|
327
|
+
"get",
|
|
328
|
+
"fillPattern",
|
|
329
|
+
[
|
|
330
|
+
"coalesce",
|
|
331
|
+
["get", "style"],
|
|
332
|
+
["literal", {}]
|
|
333
|
+
]
|
|
334
|
+
],
|
|
335
|
+
"solid"
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Filter for one of the two mutually-exclusive Polygon fill layers, keyed on
|
|
340
|
+
* whether the feature's resolved `fillPattern` is a hatch pattern. The solid
|
|
341
|
+
* `-fill` layer takes `hatched: false` (every Polygon except hatched ones);
|
|
342
|
+
* the `-fill-pattern` layer takes `hatched: true`. Split because MapLibre's
|
|
343
|
+
* `fill-color` and `fill-pattern` paints cannot coexist on one fill layer.
|
|
344
|
+
*/
|
|
345
|
+
function buildFillFilter(hatched) {
|
|
346
|
+
const isHatched = [
|
|
347
|
+
"in",
|
|
348
|
+
fillPatternField(),
|
|
349
|
+
["literal", [...HATCH_PATTERNS]]
|
|
350
|
+
];
|
|
351
|
+
return [
|
|
352
|
+
"all",
|
|
353
|
+
[
|
|
354
|
+
"==",
|
|
355
|
+
["geometry-type"],
|
|
356
|
+
"Polygon"
|
|
357
|
+
],
|
|
358
|
+
hatched ? isHatched : ["!", isHatched]
|
|
359
|
+
];
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Paint for the hatch-pattern fill layer. The image id is baked per-feature
|
|
363
|
+
* into `properties._fillPatternImageId` (see `promoteFeature`), so the paint
|
|
364
|
+
* is a plain `["get", ...]` rather than re-deriving the id as a GL expression,
|
|
365
|
+
* keeping `fillPatternImageId` the single source of truth for the id string.
|
|
366
|
+
*/
|
|
367
|
+
const FILL_PATTERN_PAINT = {
|
|
368
|
+
"fill-pattern": ["get", FILL_PATTERN_IMAGE_ID_PROPERTY],
|
|
369
|
+
"fill-opacity": 1
|
|
370
|
+
};
|
|
309
371
|
function buildLinePaint(style) {
|
|
310
372
|
const colorDefault = style?.strokeColor ?? "#000";
|
|
311
373
|
const widthDefault = style?.strokeWidth ?? 2;
|
|
@@ -340,6 +402,8 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
340
402
|
map;
|
|
341
403
|
viewChangeMode;
|
|
342
404
|
pendingIconImages = /* @__PURE__ */ new Set();
|
|
405
|
+
pendingLayerUpdates = /* @__PURE__ */ new Map();
|
|
406
|
+
deferredLayerRemovals = /* @__PURE__ */ new Map();
|
|
343
407
|
scalesGroundPointSymbolsNatively = true;
|
|
344
408
|
constructor(map, options = {}) {
|
|
345
409
|
super();
|
|
@@ -362,14 +426,19 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
362
426
|
id: fillId,
|
|
363
427
|
type: "fill",
|
|
364
428
|
source: sourceId,
|
|
365
|
-
filter:
|
|
366
|
-
"==",
|
|
367
|
-
"$type",
|
|
368
|
-
"Polygon"
|
|
369
|
-
],
|
|
429
|
+
filter: buildFillFilter(false),
|
|
370
430
|
paint: buildFillPaint(style)
|
|
371
431
|
});
|
|
372
432
|
mlLayerIds.push(fillId);
|
|
433
|
+
const fillPatternId = `${layerId}-fill-pattern`;
|
|
434
|
+
this.map.addLayer({
|
|
435
|
+
id: fillPatternId,
|
|
436
|
+
type: "fill",
|
|
437
|
+
source: sourceId,
|
|
438
|
+
filter: buildFillFilter(true),
|
|
439
|
+
paint: FILL_PATTERN_PAINT
|
|
440
|
+
});
|
|
441
|
+
mlLayerIds.push(fillPatternId);
|
|
373
442
|
const lineId = `${layerId}-line`;
|
|
374
443
|
this.map.addLayer({
|
|
375
444
|
id: lineId,
|
|
@@ -419,25 +488,109 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
419
488
|
};
|
|
420
489
|
}
|
|
421
490
|
destroyLayer(handle) {
|
|
491
|
+
for (const [layerId, layerHandle] of this.layers) {
|
|
492
|
+
if (layerHandle !== handle) continue;
|
|
493
|
+
this.pendingLayerUpdates.get(layerId)?.cancel();
|
|
494
|
+
this.pendingLayerUpdates.delete(layerId);
|
|
495
|
+
this.deferredLayerRemovals.delete(layerId);
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
422
498
|
for (const mlId of handle.mlLayerIds) if (this.map.getLayer(mlId)) this.map.removeLayer(mlId);
|
|
423
499
|
if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
|
|
424
500
|
}
|
|
425
501
|
applyFeatureOps(layerId, ops) {
|
|
502
|
+
const deferred = this.deferredLayerRemovals.get(layerId);
|
|
503
|
+
const isPureRemoval = ops.adds.length === 0 && ops.updates.length === 0 && (ops.removes?.length ?? 0) > 0;
|
|
504
|
+
if (deferred && isPureRemoval) {
|
|
505
|
+
deferred.after.then(() => this.flushDeferredLayerRemoval(layerId, deferred), () => this.flushDeferredLayerRemoval(layerId, deferred));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (deferred) this.deferredLayerRemovals.delete(layerId);
|
|
509
|
+
this.updateSourceData(layerId, ops.full);
|
|
510
|
+
}
|
|
511
|
+
updateSourceData(layerId, features) {
|
|
426
512
|
const handle = this.layers.get(layerId);
|
|
427
513
|
if (!handle) return;
|
|
428
514
|
const source = this.map.getSource(handle.sourceId);
|
|
429
515
|
if (!source) return;
|
|
430
|
-
const
|
|
431
|
-
|
|
516
|
+
const promotedFeatures = features.map((f) => this.promoteFeature(f));
|
|
517
|
+
let resolveSourceData;
|
|
518
|
+
const sourceDataCompletion = new Promise((resolve) => {
|
|
519
|
+
resolveSourceData = resolve;
|
|
520
|
+
});
|
|
521
|
+
const onSourceData = (event) => {
|
|
522
|
+
if (event.sourceDataType !== "content") return;
|
|
523
|
+
source.off("data", onSourceData);
|
|
524
|
+
resolveSourceData();
|
|
525
|
+
};
|
|
526
|
+
source.on("data", onSourceData);
|
|
527
|
+
const result = source.setData({
|
|
528
|
+
type: "FeatureCollection",
|
|
529
|
+
features: promotedFeatures
|
|
530
|
+
}, true);
|
|
531
|
+
const supportsCompletionPromise = isPromiseLike(result);
|
|
532
|
+
if (supportsCompletionPromise) source.off("data", onSourceData);
|
|
533
|
+
const completion = supportsCompletionPromise ? Promise.resolve(result) : sourceDataCompletion;
|
|
534
|
+
const pending = {
|
|
535
|
+
completion,
|
|
536
|
+
cancel() {
|
|
537
|
+
source.off("data", onSourceData);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
this.pendingLayerUpdates.set(layerId, pending);
|
|
541
|
+
completion.catch(() => {});
|
|
542
|
+
}
|
|
543
|
+
flushDeferredLayerRemoval(layerId, deferred) {
|
|
544
|
+
if (this.deferredLayerRemovals.get(layerId) !== deferred) return;
|
|
545
|
+
this.deferredLayerRemovals.delete(layerId);
|
|
546
|
+
this.updateSourceData(layerId, this.getLayerFeatures(layerId));
|
|
547
|
+
}
|
|
548
|
+
deferLayerRemovalUntilLayerUpdate(layerId, updatedLayerId) {
|
|
549
|
+
const pending = this.pendingLayerUpdates.get(updatedLayerId);
|
|
550
|
+
if (!pending || !this.layers.has(layerId)) return;
|
|
551
|
+
this.deferredLayerRemovals.set(layerId, { after: pending.completion });
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
|
|
555
|
+
* and picks can key on the caller's feature id) and, for a hatch-patterned
|
|
556
|
+
* Polygon, `_fillPatternImageId` (registering the tile image on demand). At
|
|
557
|
+
* most one object spread — a feature needing neither passes through
|
|
558
|
+
* untouched.
|
|
559
|
+
*/
|
|
560
|
+
promoteFeature(feature) {
|
|
561
|
+
const extra = {};
|
|
562
|
+
if (feature.id !== void 0) extra._fid = feature.id;
|
|
563
|
+
const imageId = this.resolveFillPatternImage(feature);
|
|
564
|
+
if (imageId !== void 0) extra[FILL_PATTERN_IMAGE_ID_PROPERTY] = imageId;
|
|
565
|
+
if (Object.keys(extra).length === 0) return feature;
|
|
566
|
+
return {
|
|
567
|
+
...feature,
|
|
432
568
|
properties: {
|
|
433
|
-
...
|
|
434
|
-
|
|
569
|
+
...feature.properties ?? {},
|
|
570
|
+
...extra
|
|
435
571
|
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Resolve a hatch-patterned Polygon's `style.fillPattern`/`fillColor` to its
|
|
576
|
+
* hatch-tile image id, registering the image on demand. Returns `undefined`
|
|
577
|
+
* for features without a hatch pattern, so `fillPatternImageId` stays the
|
|
578
|
+
* single source of truth for the id baked into `_fillPatternImageId`.
|
|
579
|
+
*/
|
|
580
|
+
resolveFillPatternImage(feature) {
|
|
581
|
+
const props = feature.properties;
|
|
582
|
+
const pattern = props?.style?.fillPattern;
|
|
583
|
+
if (!isHatchPattern(pattern)) return void 0;
|
|
584
|
+
const color = props?.fillColor ?? props?.style?.fillColor ?? "rgba(0,0,0,0)";
|
|
585
|
+
const imageId = fillPatternImageId(color, pattern);
|
|
586
|
+
this.ensureFillPatternImage(imageId, color, pattern);
|
|
587
|
+
return imageId;
|
|
588
|
+
}
|
|
589
|
+
/** Register a hatch-tile image once per (color, pattern) pair, keyed by `fillPatternImageId`. */
|
|
590
|
+
ensureFillPatternImage(imageId, color, pattern) {
|
|
591
|
+
if (this.map.hasImage(imageId)) return;
|
|
592
|
+
const image = makeFillPatternImage(color, pattern, 2);
|
|
593
|
+
this.map.addImage(imageId, image, { pixelRatio: 2 });
|
|
441
594
|
}
|
|
442
595
|
applyIconResources(resources) {
|
|
443
596
|
if (typeof Image === "undefined") return;
|
|
@@ -611,8 +764,10 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
611
764
|
const layer = this.map.getLayer(mlId);
|
|
612
765
|
if (!layer) continue;
|
|
613
766
|
const type = layer.type;
|
|
614
|
-
if (type === "fill")
|
|
615
|
-
|
|
767
|
+
if (type === "fill") {
|
|
768
|
+
if (mlId.endsWith("-fill-pattern")) continue;
|
|
769
|
+
this.map.setPaintProperty(mlId, "fill-color", buildFillPaint(style)["fill-color"]);
|
|
770
|
+
} else if (type === "line") {
|
|
616
771
|
if (style.strokeColor !== void 0 || style.strokeWidth !== void 0) {
|
|
617
772
|
const paint = buildLinePaint(style);
|
|
618
773
|
if (style.strokeColor !== void 0) this.map.setPaintProperty(mlId, "line-color", paint["line-color"]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-draw-adapter-maplibre",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.13",
|
|
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",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@orbat-mapper/control-measures": "^0.2.0-alpha.
|
|
41
|
-
"@orbat-mapper/tactical-draw": "^0.2.0-alpha.
|
|
40
|
+
"@orbat-mapper/control-measures": "^0.2.0-alpha.20",
|
|
41
|
+
"@orbat-mapper/tactical-draw": "^0.2.0-alpha.28"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"maplibre-gl": ">=4.0.0 <6.0.0"
|