@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 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 features = ops.full.map((f) => f.id !== void 0 ? {
431
- ...f,
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
- ...f.properties ?? {},
434
- _fid: f.id
569
+ ...feature.properties ?? {},
570
+ ...extra
435
571
  }
436
- } : f);
437
- source.setData({
438
- type: "FeatureCollection",
439
- features
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") this.map.setPaintProperty(mlId, "fill-color", buildFillPaint(style)["fill-color"]);
615
- else if (type === "line") {
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.11",
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.16",
41
- "@orbat-mapper/tactical-draw": "^0.2.0-alpha.26"
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"