@orbat-mapper/tactical-draw-adapter-maplibre 0.3.20 → 0.3.22

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
@@ -1,7 +1,17 @@
1
1
  import { BaseMapAdapter, EditPointerDriver, FeatureGroup, IconResource, LayerId, MapCursor, NativeEventEntry, PickHandler, PickOptions, PixelCoordinate, StyleOptions } from "@orbat-mapper/tactical-draw";
2
2
  import * as maplibregl from "maplibre-gl";
3
+ import { DataDrivenPropertyValueSpecification } from "maplibre-gl";
3
4
  import { Feature, Position } from "geojson";
4
5
 
6
+ //#region src/text-sizing.d.ts
7
+ type SizeExpression = DataDrivenPropertyValueSpecification<number>;
8
+ interface TextSizing {
9
+ size: SizeExpression;
10
+ haloWidth: SizeExpression;
11
+ haloBlur: SizeExpression;
12
+ opacity: SizeExpression;
13
+ }
14
+ //#endregion
5
15
  //#region src/ml-adapter.d.ts
6
16
  interface MapLibreLayerHandle {
7
17
  sourceId: string;
@@ -40,6 +50,9 @@ interface MapLibrePaintSurface {
40
50
  readonly layerId: string;
41
51
  readonly channel: PaintChannel;
42
52
  readonly featureId: string | number;
53
+ textSizing?: TextSizing;
54
+ /** Signature of the inputs `textSizing`/`icon-size` were derived from. */
55
+ sizingKey?: string;
43
56
  }
44
57
  /**
45
58
  * Last-applied scalar state for a rendered image overlay, used to skip
@@ -81,6 +94,7 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
81
94
  private clearGroupBundles;
82
95
  private requiredIconIds;
83
96
  private reconcileGroupPaintSurfaces;
97
+ private updateSurfaceSizing;
84
98
  private buildGroupLayer;
85
99
  private updateGroupBundleSource;
86
100
  private orderGroupBundles;
package/dist/index.mjs CHANGED
@@ -1,5 +1,115 @@
1
+ import { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, bindAbortable, evaluatePickCandidates, fillPatternImageId, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, pickHitTolerance, pickPointerType, resolveTextHaloWidth } from "@orbat-mapper/tactical-draw";
1
2
  import { project, unproject } from "@orbat-mapper/control-measures";
2
- import { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, HATCH_PATTERNS, PointerCallbackHub, TEXT_HALO_COLOR, TOUCH_HIT_TOLERANCE_PX, bindAbortable, evaluatePickCandidates, fillPatternImageId, makeFillPatternImage, mergeNativeAndCachedPickFeatures, nearestWorldCopyLng, pickHitTolerance, pickPointerType } from "@orbat-mapper/tactical-draw";
3
+ //#region src/mercator.ts
4
+ /**
5
+ * Web Mercator world circumference in meters (2π × 6378137). The single source
6
+ * of truth for this package's meters↔pixels math: `getResolution`, the ground
7
+ * text ramp, and the ground icon ramp all derive from it, so they cannot drift
8
+ * apart.
9
+ */
10
+ const WEB_MERCATOR_CIRCUMFERENCE = 40075016.686;
11
+ /**
12
+ * Pixels per ground meter at zoom 0. MapLibre's world is 512 px wide at zoom 0
13
+ * and doubles each zoom level, so the size at any zoom `z` is this × 2^z —
14
+ * which is exactly what an `["exponential", 2]` zoom interpolation computes.
15
+ */
16
+ const PIXELS_PER_METER_AT_ZOOM_0 = 512 / WEB_MERCATOR_CIRCUMFERENCE;
17
+ //#endregion
18
+ //#region src/text-sizing.ts
19
+ /** `text-halo-blur` as a fraction of the halo's outward depth (MapLibre only). */
20
+ const TEXT_HALO_BLUR_RATIO = .25;
21
+ /**
22
+ * Exact clamped ground scaling, with only the two actual clamp boundaries as
23
+ * stops. MapLibre's glyph renderer interpolates between stops covering the
24
+ * tile's whole zoom interval, so inserting fractional stops into a composite
25
+ * expression does NOT fix its rendering near a clamp. A graphic's own camera
26
+ * expression has no interior kink: base-2 interpolation is exact even when
27
+ * the covering interval spans many zoom levels. No zoom listener is needed.
28
+ *
29
+ * Shared by the ground text ramp and the ground icon ramp — both scale a
30
+ * meters-derived size with zoom and pin it at two per-feature bounds.
31
+ */
32
+ function groundRamp(pixelsAtZoom0, min, max) {
33
+ if (min >= max) return max;
34
+ return [
35
+ "interpolate",
36
+ ["exponential", 2],
37
+ ["zoom"],
38
+ Math.log2(min / pixelsAtZoom0),
39
+ min,
40
+ Math.log2(max / pixelsAtZoom0),
41
+ max
42
+ ];
43
+ }
44
+ /** Each grouped text surface paints exactly one feature (ADR-0039). */
45
+ function buildTextSizing(text, props) {
46
+ const meters = props?.textSizeMeters;
47
+ const pixels = props?.textSizePixels;
48
+ const maximum = props?.textMaxSizePixels;
49
+ const fixedHalo = resolveTextHaloWidth(pixels ?? 14);
50
+ let size = pixels ?? 14;
51
+ let scaledHalo = fixedHalo;
52
+ let scaledBlur = fixedHalo * TEXT_HALO_BLUR_RATIO;
53
+ let opacity = 1;
54
+ if (pixels === void 0 && typeof meters === "number" && meters > 0) {
55
+ const pixelsAtZoom0 = meters * PIXELS_PER_METER_AT_ZOOM_0;
56
+ const min = maximum !== void 0 ? pixelsAtZoom0 : Math.max(pixelsAtZoom0, text.textSizeMinPixels ?? DEFAULT_TEXT_SIZE_MIN_PIXELS);
57
+ const max = maximum ?? text.textSizeMaxPixels ?? DEFAULT_TEXT_SIZE_MAX_PIXELS;
58
+ const haloPixelsAtZoom0 = pixelsAtZoom0 / 8;
59
+ const minHalo = resolveTextHaloWidth(min);
60
+ const maxHalo = resolveTextHaloWidth(max);
61
+ size = groundRamp(pixelsAtZoom0, min, max);
62
+ scaledHalo = groundRamp(haloPixelsAtZoom0, minHalo, maxHalo);
63
+ scaledBlur = groundRamp(haloPixelsAtZoom0 * TEXT_HALO_BLUR_RATIO, minHalo * TEXT_HALO_BLUR_RATIO, maxHalo * TEXT_HALO_BLUR_RATIO);
64
+ if (maximum !== void 0) {
65
+ const hideZoom = Math.log2(maximum / pixelsAtZoom0);
66
+ opacity = maximum <= 0 ? 0 : [
67
+ "step",
68
+ ["zoom"],
69
+ 1,
70
+ hideZoom + Number.EPSILON * Math.max(1, Math.abs(hideZoom)),
71
+ 0
72
+ ];
73
+ }
74
+ }
75
+ if (pixels === void 0 && meters === 0) {
76
+ size = maximum !== void 0 ? 0 : Math.min(text.textSizeMaxPixels ?? DEFAULT_TEXT_SIZE_MAX_PIXELS, Math.max(0, text.textSizeMinPixels ?? DEFAULT_TEXT_SIZE_MIN_PIXELS));
77
+ scaledHalo = resolveTextHaloWidth(size);
78
+ scaledBlur = scaledHalo * TEXT_HALO_BLUR_RATIO;
79
+ }
80
+ const haloWidth = props?.textHalo ? scaledHalo : text.strokeColor ? text.strokeWidth ?? 1 : props?.textBackground ? 3 : 0;
81
+ return {
82
+ size,
83
+ haloWidth,
84
+ haloBlur: props?.textHalo ? scaledBlur : 0,
85
+ opacity
86
+ };
87
+ }
88
+ //#endregion
89
+ //#region src/icon-sizing.ts
90
+ /** Stand-in for "no upper bound", matching the unclamped icon default. */
91
+ const UNBOUNDED_PIXELS = 1e9;
92
+ /**
93
+ * Exact per-feature `icon-size` for a ground-sized point symbol, the icon
94
+ * counterpart of `buildTextSizing` (#522). `icon-size` is a *scale factor* —
95
+ * rendered pixels ÷ the icon image's intrinsic octagon size — so the whole
96
+ * ramp is divided by `iconOctagonSize`; dividing every stop by the same
97
+ * constant leaves the clamp zooms unchanged.
98
+ *
99
+ * Returns `undefined` for a feature that is not ground-sized, so the caller
100
+ * keeps the shared `iconScale` snapshot expression.
101
+ */
102
+ function buildIconSizing(props) {
103
+ const meters = props?.iconSizeMeters;
104
+ if (typeof meters !== "number" || !(meters > 0)) return void 0;
105
+ const octagon = typeof props?.iconOctagonSize === "number" && props.iconOctagonSize > 0 ? props.iconOctagonSize : 1;
106
+ const pixelsAtZoom0 = meters * PIXELS_PER_METER_AT_ZOOM_0;
107
+ const min = Math.max(props?.iconMinPixels ?? 0, pixelsAtZoom0);
108
+ const max = props?.iconMaxPixels ?? UNBOUNDED_PIXELS;
109
+ if (max <= min) return min / octagon;
110
+ return groundRamp(pixelsAtZoom0 / octagon, min / octagon, max / octagon);
111
+ }
112
+ //#endregion
3
113
  //#region src/ml-adapter.ts
4
114
  /** Feature property key for the resolved hatch-pattern image id (see `promoteFeature`). */
5
115
  const FILL_PATTERN_IMAGE_ID_PROPERTY = "_fillPatternImageId";
@@ -8,6 +118,27 @@ const DEFAULT_LINE_WIDTH = 2;
8
118
  function isHatchPattern(value) {
9
119
  return HATCH_PATTERNS.includes(value);
10
120
  }
121
+ /**
122
+ * The sizing-relevant inputs for a surface, joined into a comparable string.
123
+ * `undefined` for channels whose size never varies per feature, which skips
124
+ * the comparison entirely.
125
+ */
126
+ function sizingSignature(channel, props, style) {
127
+ if (channel === "icon") return `${props?.iconSizeMeters}|${props?.iconOctagonSize}|${props?.iconMinPixels}|${props?.iconMaxPixels}`;
128
+ if (channel !== "text") return void 0;
129
+ const text = style?.text;
130
+ return [
131
+ props?.textSizeMeters,
132
+ props?.textSizePixels,
133
+ props?.textMaxSizePixels,
134
+ props?.textHalo,
135
+ props?.textBackground,
136
+ text?.textSizeMinPixels,
137
+ text?.textSizeMaxPixels,
138
+ text?.strokeColor,
139
+ text?.strokeWidth
140
+ ].join("|");
141
+ }
11
142
  function coordinatesEqual(a, b) {
12
143
  for (let index = 0; index < 4; index++) {
13
144
  const pa = a[index];
@@ -180,7 +311,7 @@ function buildCirclePointFilter(style) {
180
311
  ["!has", "iconId"]
181
312
  ];
182
313
  }
183
- function buildIconLayer(id, source) {
314
+ function buildIconLayer(id, source, iconSize = buildIconSizeExpression()) {
184
315
  return {
185
316
  id,
186
317
  type: "symbol",
@@ -196,7 +327,7 @@ function buildIconLayer(id, source) {
196
327
  ],
197
328
  layout: {
198
329
  "icon-image": ["get", "iconId"],
199
- "icon-size": buildIconSizeExpression(),
330
+ "icon-size": iconSize,
200
331
  "icon-rotate": [
201
332
  "*",
202
333
  [
@@ -227,120 +358,8 @@ function centeredSvg(resource) {
227
358
  const height = halfHeight * 2;
228
359
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><g transform="translate(${halfWidth - resource.anchor[0]} ${halfHeight - resource.anchor[1]})">${resource.svg}</g></svg>`;
229
360
  }
230
- /** Web Mercator world circumference in meters (2π × 6378137). */
231
- const WEB_MERCATOR_CIRCUMFERENCE = 40075016.686;
232
- /**
233
- * Per-feature text size (ADR-0028): a ground-anchored label carries
234
- * `textSizeMeters` and renders at `textSizeMeters / resolution`, clamped to
235
- * the layer's `textSizeMinPixels`/`textSizeMaxPixels` band; a screen-anchored
236
- * one carries `textSizePixels` and renders at that constant; neither renders
237
- * the 14px engine default. Layout properties only take zoom as an
238
- * `interpolate`/`step` input, so the ground conversion is evaluated at every
239
- * integer zoom stop (resolution = world circumference / 512·2^zoom pixels,
240
- * matching `getResolution`); the linear ramp between stops approximates the
241
- * exponential exactly closely enough at 1-zoom granularity.
242
- */
243
- function textSizeAtZoom(text, zoom) {
244
- const minPx = text.textSizeMinPixels ?? DEFAULT_TEXT_SIZE_MIN_PIXELS;
245
- const maxPx = text.textSizeMaxPixels ?? DEFAULT_TEXT_SIZE_MAX_PIXELS;
246
- const screenOrDefault = [
247
- "coalesce",
248
- ["get", "textSizePixels"],
249
- 14
250
- ];
251
- const groundPx = [
252
- "*",
253
- ["get", "textSizeMeters"],
254
- Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
255
- ];
256
- return [
257
- "case",
258
- ["has", "textSizeMeters"],
259
- [
260
- "case",
261
- ["has", "textMaxSizePixels"],
262
- [
263
- "min",
264
- ["get", "textMaxSizePixels"],
265
- groundPx
266
- ],
267
- [
268
- "min",
269
- maxPx,
270
- [
271
- "max",
272
- minPx,
273
- groundPx
274
- ]
275
- ]
276
- ],
277
- screenOrDefault
278
- ];
279
- }
280
- /**
281
- * Wrap a per-zoom expression builder in the top-level zoom `interpolate` every
282
- * zoom-driven property here needs. MapLibre only accepts `["zoom"]` as the
283
- * input of a top-level `step`/`interpolate`, never nested inside a `case`, so
284
- * the ramp must be outermost and the per-feature branching evaluated at each
285
- * stop — not the other way round.
286
- */
287
- function interpolateOverZoom(at) {
288
- return [
289
- "interpolate",
290
- ["linear"],
291
- ["zoom"],
292
- ...Array.from({ length: 25 }, (_, zoom) => [zoom, at(zoom)]).flat()
293
- ];
294
- }
295
- function buildTextSizeExpression(text) {
296
- return interpolateOverZoom((zoom) => textSizeAtZoom(text, zoom));
297
- }
298
361
  /**
299
- * Companion to `buildTextSizeExpression` (ADR-0037): drives `text-opacity` to
300
- * 0 once a ground-anchored Text graphic's derived on-screen size exceeds its
301
- * `textMaxSizePixels` threshold, 1 otherwise (including every feature without
302
- * a threshold, e.g. amplifier labels). Layout properties like `text-size`
303
- * cannot represent "hidden", so hiding is a paint-property concern; `step`
304
- * (rather than `interpolate`) is enough since opacity only ever needs to
305
- * flip at a stop, evaluated at the same per-zoom granularity as the size
306
- * expression.
307
- */
308
- const TEXT_OPACITY_EXPRESSION = (() => {
309
- const opacityAtZoom = (zoom) => {
310
- return [
311
- "case",
312
- [
313
- "all",
314
- ["has", "textSizeMeters"],
315
- ["has", "textMaxSizePixels"]
316
- ],
317
- [
318
- "case",
319
- [
320
- ">",
321
- [
322
- "*",
323
- ["get", "textSizeMeters"],
324
- Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
325
- ],
326
- ["get", "textMaxSizePixels"]
327
- ],
328
- 0,
329
- 1
330
- ],
331
- 1
332
- ];
333
- };
334
- const stops = Array.from({ length: 25 }, (_, zoom) => zoom);
335
- return [
336
- "step",
337
- ["zoom"],
338
- opacityAtZoom(0),
339
- ...stops.slice(1).flatMap((zoom) => [zoom, opacityAtZoom(zoom)])
340
- ];
341
- })();
342
- /**
343
- * Per-feature icon size, same zoom-stop scheme as `buildTextSizeExpression`:
362
+ * Per-feature icon size, using integer zoom stops:
344
363
  * a ground-sized point symbol carries `iconSizeMeters`/`iconOctagonSize` (and
345
364
  * optional `iconMinPixels`/`iconMaxPixels`), so the meters→pixels clamp is
346
365
  * evaluated inside the style layer at every integer zoom stop and the symbol
@@ -453,84 +472,12 @@ function buildTextHaloColorExpression(text) {
453
472
  ];
454
473
  }
455
474
  /**
456
- * Per-feature `text-halo-width` value, mirroring
457
- * `buildTextHaloColorExpression`'s precedence. MapLibre's `text-halo-width`
458
- * already extends outward from the glyph edge, matching
459
- * `resolveTextHaloWidth`'s own outward-depth semantics, so the `textHalo`
460
- * case uses its formula directly (unlike OpenLayers's straddling canvas
461
- * stroke, which must double it).
462
- *
463
- * The width is derived from `textSizeAtZoom` — the same size `text-size`
464
- * renders at — not from `textSizePixels` alone. A ground-anchored label
465
- * (ADR-0028) carries only `textSizeMeters`, so reading the pixel size directly
466
- * would fall back to a constant and pin the halo at one width while the glyphs
467
- * grew and shrank with zoom: far too heavy on small text, hairline on large.
468
- * OL and Leaflet already halo off their resolved size; this brings MapLibre in
469
- * line. The ratio stays well under MapLibre's SDF halo ceiling (~¼ em), so the
470
- * halo never clips into blocky corners.
471
- */
472
- function buildTextHaloWidthExpression(text) {
473
- const fallback = text.strokeColor ? text.strokeWidth ?? 1 : [
474
- "case",
475
- HAS_TEXT_BACKGROUND,
476
- 3,
477
- 0
478
- ];
479
- return interpolateOverZoom((zoom) => [
480
- "case",
481
- HAS_TEXT_HALO,
482
- haloWidthAtZoom(text, zoom),
483
- fallback
484
- ]);
485
- }
486
- /**
487
- * `text-halo-blur` as a fraction of the halo width. MapLibre's halo edge is
488
- * hard at blur 0, which reads harsher over a vector basemap than the basemap's
489
- * own labels — OpenFreeMap Liberty, the preview's default style, blurs nearly
490
- * every one of its halos. Scaled off the width rather than a flat pixel value
491
- * so the softness stays proportional as the halo grows with zoom.
492
- *
493
- * MapLibre-only: OpenLayers canvas strokes have no blur, and Leaflet's stacked
494
- * `text-shadow` halo would need a blur radius threaded through each shadow.
495
- * The three engines' halos therefore differ slightly at the edge.
496
- */
497
- const TEXT_HALO_BLUR_RATIO = .25;
498
- /** The halo's outward depth at one zoom stop; shared by the width and blur builders. */
499
- function haloWidthAtZoom(text, zoom) {
500
- return [
501
- "max",
502
- 1.5,
503
- [
504
- "/",
505
- textSizeAtZoom(text, zoom),
506
- 8
507
- ]
508
- ];
509
- }
510
- /**
511
- * Per-feature `text-halo-blur`, softening only a `textHalo` halo's edge.
512
- * Everything else stays at 0 so the `textBackground` backdrop and a
513
- * layer-level text stroke keep the crisp edge they render today.
514
- */
515
- function buildTextHaloBlurExpression(text) {
516
- return interpolateOverZoom((zoom) => [
517
- "case",
518
- HAS_TEXT_HALO,
519
- [
520
- "*",
521
- TEXT_HALO_BLUR_RATIO,
522
- haloWidthAtZoom(text, zoom)
523
- ],
524
- 0
525
- ]);
526
- }
527
- /**
528
475
  * Build the layout and paint objects for a symbol (text) layer.
529
476
  * Centralises the rotation convention so `createLayer` and `setLayerStyle`
530
477
  * stay in sync. Rotation follows the OpenLayers convention: `π − r` radians
531
478
  * (converted to degrees for MapLibre).
532
479
  */
533
- function buildSymbolLayer(symbolId, sourceId, text) {
480
+ function buildSymbolLayer(symbolId, sourceId, text, sizing = buildTextSizing(text, null)) {
534
481
  const textRotate = text.labelProperty ? [
535
482
  "-",
536
483
  180,
@@ -611,7 +558,7 @@ function buildSymbolLayer(symbolId, sourceId, text) {
611
558
  ["literal", ["Open Sans Light"]],
612
559
  ["literal", ["Open Sans Regular"]]
613
560
  ] : ["Open Sans Regular"],
614
- "text-size": buildTextSizeExpression(text),
561
+ "text-size": sizing.size,
615
562
  "text-rotate": textRotate,
616
563
  "text-anchor": textAnchor,
617
564
  "text-justify": textJustify,
@@ -624,9 +571,9 @@ function buildSymbolLayer(symbolId, sourceId, text) {
624
571
  paint: {
625
572
  "text-color": textColor,
626
573
  "text-halo-color": buildTextHaloColorExpression(text),
627
- "text-halo-width": buildTextHaloWidthExpression(text),
628
- "text-halo-blur": buildTextHaloBlurExpression(text),
629
- "text-opacity": TEXT_OPACITY_EXPRESSION
574
+ "text-halo-width": sizing.haloWidth,
575
+ "text-halo-blur": sizing.haloBlur,
576
+ "text-opacity": sizing.opacity
630
577
  }
631
578
  };
632
579
  }
@@ -1019,7 +966,8 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1019
966
  const desired = features.flatMap((feature) => featurePaintChannels(feature, handle.style).map((channel) => ({
1020
967
  key: `${typeof feature.id}:${JSON.stringify(feature.id)}:${channel}`,
1021
968
  featureId: feature.id,
1022
- channel
969
+ channel,
970
+ feature
1023
971
  })));
1024
972
  const desiredKeys = new Set(desired.map(({ key }) => key));
1025
973
  let structureChanged = false;
@@ -1029,11 +977,17 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1029
977
  bundle.paintSurfaces.delete(key);
1030
978
  structureChanged = true;
1031
979
  }
1032
- for (const { key, featureId, channel } of desired) {
1033
- if (bundle.paintSurfaces.has(key)) continue;
980
+ for (const { key, featureId, channel, feature } of desired) {
981
+ const existing = bundle.paintSurfaces.get(key);
982
+ const sizingKey = sizingSignature(channel, feature.properties, handle.style);
983
+ if (existing) {
984
+ if (sizingKey !== void 0 && sizingKey !== existing.sizingKey) this.updateSurfaceSizing(existing, channel, feature.properties, handle.style, sizingKey);
985
+ continue;
986
+ }
1034
987
  const layerId = `${bundle.sourceId}-surface-${++bundle.nextPaintSurfaceId}-${channel}`;
988
+ const textSizing = channel === "text" ? buildTextSizing(handle.style.text, feature.properties) : void 0;
1035
989
  const layer = {
1036
- ...this.buildGroupLayer(layerId, bundle.sourceId, channel, handle.style),
990
+ ...this.buildGroupLayer(layerId, bundle.sourceId, channel, handle.style, textSizing, channel === "icon" ? buildIconSizing(feature.properties) : void 0),
1037
991
  filter: [
1038
992
  "==",
1039
993
  "_fid",
@@ -1044,7 +998,9 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1044
998
  bundle.paintSurfaces.set(key, {
1045
999
  layerId,
1046
1000
  channel,
1047
- featureId
1001
+ featureId,
1002
+ textSizing,
1003
+ sizingKey
1048
1004
  });
1049
1005
  structureChanged = true;
1050
1006
  }
@@ -1053,7 +1009,24 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1053
1009
  bundle.paintOrder = desiredOrder;
1054
1010
  return structureChanged || orderChanged;
1055
1011
  }
1056
- buildGroupLayer(id, sourceId, channel, style) {
1012
+ updateSurfaceSizing(surface, channel, props, style, sizingKey) {
1013
+ surface.sizingKey = sizingKey;
1014
+ if (channel === "icon") {
1015
+ const size = buildIconSizing(props);
1016
+ if (size !== void 0) this.map.setLayoutProperty(surface.layerId, "icon-size", size);
1017
+ return;
1018
+ }
1019
+ const next = buildTextSizing(style.text, props);
1020
+ const previous = surface.textSizing;
1021
+ if (JSON.stringify(previous?.size) !== JSON.stringify(next.size)) this.map.setLayoutProperty(surface.layerId, "text-size", next.size);
1022
+ for (const [key, property] of [
1023
+ ["haloWidth", "text-halo-width"],
1024
+ ["haloBlur", "text-halo-blur"],
1025
+ ["opacity", "text-opacity"]
1026
+ ]) if (JSON.stringify(previous?.[key]) !== JSON.stringify(next[key])) this.map.setPaintProperty(surface.layerId, property, next[key]);
1027
+ surface.textSizing = next;
1028
+ }
1029
+ buildGroupLayer(id, sourceId, channel, style, textSizing, iconSizing) {
1057
1030
  switch (channel) {
1058
1031
  case "fill": return {
1059
1032
  id,
@@ -1099,10 +1072,10 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1099
1072
  filter: buildCirclePointFilter(style),
1100
1073
  paint: buildCirclePaint(style)
1101
1074
  };
1102
- case "icon": return buildIconLayer(id, sourceId);
1075
+ case "icon": return buildIconLayer(id, sourceId, iconSizing);
1103
1076
  case "text":
1104
1077
  if (!style?.text) throw new Error("Cannot create a MapLibre text channel without text style");
1105
- return buildSymbolLayer(id, sourceId, style.text);
1078
+ return buildSymbolLayer(id, sourceId, style.text, textSizing);
1106
1079
  }
1107
1080
  }
1108
1081
  updateGroupBundleSource(bundle) {
@@ -1419,9 +1392,10 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1419
1392
  if (styleUpdate.text.text || styleUpdate.text.labelProperty) this.map.setLayoutProperty(layerId, "text-field", buildTextFieldExpression(resolvedStyle.text));
1420
1393
  if (styleUpdate.text.fillColor) this.map.setPaintProperty(layerId, "text-color", resolvedStyle.text.fillColor);
1421
1394
  if (styleUpdate.text.strokeColor) this.map.setPaintProperty(layerId, "text-halo-color", buildTextHaloColorExpression(resolvedStyle.text));
1422
- if (styleUpdate.text.strokeWidth !== void 0) {
1423
- this.map.setPaintProperty(layerId, "text-halo-width", buildTextHaloWidthExpression(resolvedStyle.text));
1424
- this.map.setPaintProperty(layerId, "text-halo-blur", buildTextHaloBlurExpression(resolvedStyle.text));
1395
+ if (featureId === void 0) {
1396
+ const sizing = buildTextSizing(resolvedStyle.text, null);
1397
+ this.map.setPaintProperty(layerId, "text-halo-width", sizing.haloWidth);
1398
+ this.map.setPaintProperty(layerId, "text-halo-blur", sizing.haloBlur);
1425
1399
  }
1426
1400
  break;
1427
1401
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-draw-adapter-maplibre",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
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.24.0",
41
- "@orbat-mapper/tactical-draw": "^0.8.0"
40
+ "@orbat-mapper/control-measures": "^0.26.0",
41
+ "@orbat-mapper/tactical-draw": "^0.8.2"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "maplibre-gl": ">=5.8.0 <7.0.0"