@orbat-mapper/tactical-draw-adapter-maplibre 0.2.0-alpha.23 → 0.2.0-alpha.24

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.
Files changed (2) hide show
  1. package/dist/index.mjs +99 -46
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -232,7 +232,7 @@ const WEB_MERCATOR_CIRCUMFERENCE = 40075016.686;
232
232
  * matching `getResolution`); the linear ramp between stops approximates the
233
233
  * exponential exactly closely enough at 1-zoom granularity.
234
234
  */
235
- function buildTextSizeExpression(text) {
235
+ function textSizeAtZoom(text, zoom) {
236
236
  const minPx = text.textSizeMinPixels ?? DEFAULT_TEXT_SIZE_MIN_PIXELS;
237
237
  const maxPx = text.textSizeMaxPixels ?? DEFAULT_TEXT_SIZE_MAX_PIXELS;
238
238
  const screenOrDefault = [
@@ -240,43 +240,53 @@ function buildTextSizeExpression(text) {
240
240
  ["get", "textSizePixels"],
241
241
  14
242
242
  ];
243
- const sizeAtZoom = (zoom) => {
244
- const groundPx = [
245
- "*",
246
- ["get", "textSizeMeters"],
247
- Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
248
- ];
249
- return [
243
+ const groundPx = [
244
+ "*",
245
+ ["get", "textSizeMeters"],
246
+ Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
247
+ ];
248
+ return [
249
+ "case",
250
+ ["has", "textSizeMeters"],
251
+ [
250
252
  "case",
251
- ["has", "textSizeMeters"],
253
+ ["has", "textMaxSizePixels"],
252
254
  [
253
- "case",
254
- ["has", "textMaxSizePixels"],
255
+ "min",
256
+ ["get", "textMaxSizePixels"],
257
+ groundPx
258
+ ],
259
+ [
260
+ "min",
261
+ maxPx,
255
262
  [
256
- "min",
257
- ["get", "textMaxSizePixels"],
263
+ "max",
264
+ minPx,
258
265
  groundPx
259
- ],
260
- [
261
- "min",
262
- maxPx,
263
- [
264
- "max",
265
- minPx,
266
- groundPx
267
- ]
268
266
  ]
269
- ],
270
- screenOrDefault
271
- ];
272
- };
267
+ ]
268
+ ],
269
+ screenOrDefault
270
+ ];
271
+ }
272
+ /**
273
+ * Wrap a per-zoom expression builder in the top-level zoom `interpolate` every
274
+ * zoom-driven property here needs. MapLibre only accepts `["zoom"]` as the
275
+ * input of a top-level `step`/`interpolate`, never nested inside a `case`, so
276
+ * the ramp must be outermost and the per-feature branching evaluated at each
277
+ * stop — not the other way round.
278
+ */
279
+ function interpolateOverZoom(at) {
273
280
  return [
274
281
  "interpolate",
275
282
  ["linear"],
276
283
  ["zoom"],
277
- ...Array.from({ length: 25 }, (_, zoom) => [zoom, sizeAtZoom(zoom)]).flat()
284
+ ...Array.from({ length: 25 }, (_, zoom) => [zoom, at(zoom)]).flat()
278
285
  ];
279
286
  }
287
+ function buildTextSizeExpression(text) {
288
+ return interpolateOverZoom((zoom) => textSizeAtZoom(text, zoom));
289
+ }
280
290
  /**
281
291
  * Companion to `buildTextSizeExpression` (ADR-0037): drives `text-opacity` to
282
292
  * 0 once a ground-anchored Text graphic's derived on-screen size exceeds its
@@ -441,33 +451,72 @@ function buildTextHaloColorExpression(text) {
441
451
  * `resolveTextHaloWidth`'s own outward-depth semantics, so the `textHalo`
442
452
  * case uses its formula directly (unlike OpenLayers's straddling canvas
443
453
  * stroke, which must double it).
454
+ *
455
+ * The width is derived from `textSizeAtZoom` — the same size `text-size`
456
+ * renders at — not from `textSizePixels` alone. A ground-anchored label
457
+ * (ADR-0028) carries only `textSizeMeters`, so reading the pixel size directly
458
+ * would fall back to a constant and pin the halo at one width while the glyphs
459
+ * grew and shrank with zoom: far too heavy on small text, hairline on large.
460
+ * OL and Leaflet already halo off their resolved size; this brings MapLibre in
461
+ * line. The ratio stays well under MapLibre's SDF halo ceiling (~¼ em), so the
462
+ * halo never clips into blocky corners.
444
463
  */
445
464
  function buildTextHaloWidthExpression(text) {
446
- return [
465
+ const fallback = text.strokeColor ? text.strokeWidth ?? 1 : [
466
+ "case",
467
+ HAS_TEXT_BACKGROUND,
468
+ 3,
469
+ 0
470
+ ];
471
+ return interpolateOverZoom((zoom) => [
447
472
  "case",
448
473
  HAS_TEXT_HALO,
474
+ haloWidthAtZoom(text, zoom),
475
+ fallback
476
+ ]);
477
+ }
478
+ /**
479
+ * `text-halo-blur` as a fraction of the halo width. MapLibre's halo edge is
480
+ * hard at blur 0, which reads harsher over a vector basemap than the basemap's
481
+ * own labels — OpenFreeMap Liberty, the preview's default style, blurs nearly
482
+ * every one of its halos. Scaled off the width rather than a flat pixel value
483
+ * so the softness stays proportional as the halo grows with zoom.
484
+ *
485
+ * MapLibre-only: OpenLayers canvas strokes have no blur, and Leaflet's stacked
486
+ * `text-shadow` halo would need a blur radius threaded through each shadow.
487
+ * The three engines' halos therefore differ slightly at the edge.
488
+ */
489
+ const TEXT_HALO_BLUR_RATIO = .25;
490
+ /** The halo's outward depth at one zoom stop; shared by the width and blur builders. */
491
+ function haloWidthAtZoom(text, zoom) {
492
+ return [
493
+ "max",
494
+ 1.5,
449
495
  [
450
- "max",
451
- 1.5,
452
- [
453
- "/",
454
- [
455
- "coalesce",
456
- ["get", "textSizePixels"],
457
- 24
458
- ],
459
- 8
460
- ]
461
- ],
462
- text.strokeColor ? text.strokeWidth ?? 1 : [
463
- "case",
464
- HAS_TEXT_BACKGROUND,
465
- 3,
466
- 0
496
+ "/",
497
+ textSizeAtZoom(text, zoom),
498
+ 8
467
499
  ]
468
500
  ];
469
501
  }
470
502
  /**
503
+ * Per-feature `text-halo-blur`, softening only a `textHalo` halo's edge.
504
+ * Everything else stays at 0 so the `textBackground` backdrop and a
505
+ * layer-level text stroke keep the crisp edge they render today.
506
+ */
507
+ function buildTextHaloBlurExpression(text) {
508
+ return interpolateOverZoom((zoom) => [
509
+ "case",
510
+ HAS_TEXT_HALO,
511
+ [
512
+ "*",
513
+ TEXT_HALO_BLUR_RATIO,
514
+ haloWidthAtZoom(text, zoom)
515
+ ],
516
+ 0
517
+ ]);
518
+ }
519
+ /**
471
520
  * Build the layout and paint objects for a symbol (text) layer.
472
521
  * Centralises the rotation convention so `createLayer` and `setLayerStyle`
473
522
  * stay in sync. Rotation follows the OpenLayers convention: `π − r` radians
@@ -568,6 +617,7 @@ function buildSymbolLayer(symbolId, sourceId, text) {
568
617
  "text-color": textColor,
569
618
  "text-halo-color": buildTextHaloColorExpression(text),
570
619
  "text-halo-width": buildTextHaloWidthExpression(text),
620
+ "text-halo-blur": buildTextHaloBlurExpression(text),
571
621
  "text-opacity": TEXT_OPACITY_EXPRESSION
572
622
  }
573
623
  };
@@ -1267,7 +1317,10 @@ var MapLibreAdapter = class extends BaseMapAdapter {
1267
1317
  if (styleUpdate.text.text || styleUpdate.text.labelProperty) this.map.setLayoutProperty(layerId, "text-field", buildTextFieldExpression(resolvedStyle.text));
1268
1318
  if (styleUpdate.text.fillColor) this.map.setPaintProperty(layerId, "text-color", resolvedStyle.text.fillColor);
1269
1319
  if (styleUpdate.text.strokeColor) this.map.setPaintProperty(layerId, "text-halo-color", buildTextHaloColorExpression(resolvedStyle.text));
1270
- if (styleUpdate.text.strokeWidth !== void 0) this.map.setPaintProperty(layerId, "text-halo-width", buildTextHaloWidthExpression(resolvedStyle.text));
1320
+ if (styleUpdate.text.strokeWidth !== void 0) {
1321
+ this.map.setPaintProperty(layerId, "text-halo-width", buildTextHaloWidthExpression(resolvedStyle.text));
1322
+ this.map.setPaintProperty(layerId, "text-halo-blur", buildTextHaloBlurExpression(resolvedStyle.text));
1323
+ }
1271
1324
  break;
1272
1325
  }
1273
1326
  }
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.23",
3
+ "version": "0.2.0-alpha.24",
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/tactical-draw": "^0.2.0-alpha.38",
41
- "@orbat-mapper/control-measures": "^0.2.0-alpha.26"
40
+ "@orbat-mapper/control-measures": "^0.2.0-alpha.28",
41
+ "@orbat-mapper/tactical-draw": "^0.2.0-alpha.39"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "maplibre-gl": ">=5.8.0 <6.0.0"