@orbat-mapper/tactical-draw-adapter-maplibre 0.2.0-alpha.15 → 0.2.0-alpha.16

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 +118 -12
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -139,19 +139,29 @@ function buildTextSizeExpression(text) {
139
139
  14
140
140
  ];
141
141
  const sizeAtZoom = (zoom) => {
142
+ const groundPx = [
143
+ "*",
144
+ ["get", "textSizeMeters"],
145
+ Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
146
+ ];
142
147
  return [
143
148
  "case",
144
149
  ["has", "textSizeMeters"],
145
150
  [
146
- "min",
147
- maxPx,
151
+ "case",
152
+ ["has", "textMaxSizePixels"],
148
153
  [
149
- "max",
150
- minPx,
154
+ "min",
155
+ ["get", "textMaxSizePixels"],
156
+ groundPx
157
+ ],
158
+ [
159
+ "min",
160
+ maxPx,
151
161
  [
152
- "*",
153
- ["get", "textSizeMeters"],
154
- Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
162
+ "max",
163
+ minPx,
164
+ groundPx
155
165
  ]
156
166
  ]
157
167
  ],
@@ -166,6 +176,50 @@ function buildTextSizeExpression(text) {
166
176
  ];
167
177
  }
168
178
  /**
179
+ * Companion to `buildTextSizeExpression` (ADR-0037): drives `text-opacity` to
180
+ * 0 once a ground-anchored Text graphic's derived on-screen size exceeds its
181
+ * `textMaxSizePixels` threshold, 1 otherwise (including every feature without
182
+ * a threshold, e.g. amplifier labels). Layout properties like `text-size`
183
+ * cannot represent "hidden", so hiding is a paint-property concern; `step`
184
+ * (rather than `interpolate`) is enough since opacity only ever needs to
185
+ * flip at a stop, evaluated at the same per-zoom granularity as the size
186
+ * expression.
187
+ */
188
+ const TEXT_OPACITY_EXPRESSION = (() => {
189
+ const opacityAtZoom = (zoom) => {
190
+ return [
191
+ "case",
192
+ [
193
+ "all",
194
+ ["has", "textSizeMeters"],
195
+ ["has", "textMaxSizePixels"]
196
+ ],
197
+ [
198
+ "case",
199
+ [
200
+ ">",
201
+ [
202
+ "*",
203
+ ["get", "textSizeMeters"],
204
+ Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
205
+ ],
206
+ ["get", "textMaxSizePixels"]
207
+ ],
208
+ 0,
209
+ 1
210
+ ],
211
+ 1
212
+ ];
213
+ };
214
+ const stops = Array.from({ length: 25 }, (_, zoom) => zoom);
215
+ return [
216
+ "step",
217
+ ["zoom"],
218
+ opacityAtZoom(0),
219
+ ...stops.slice(1).flatMap((zoom) => [zoom, opacityAtZoom(zoom)])
220
+ ];
221
+ })();
222
+ /**
169
223
  * Per-feature icon size, same zoom-stop scheme as `buildTextSizeExpression`:
170
224
  * a ground-sized point symbol carries `iconSizeMeters`/`iconOctagonSize` (and
171
225
  * optional `iconMinPixels`/`iconMaxPixels`), so the meters→pixels clamp is
@@ -224,6 +278,26 @@ function buildIconSizeExpression() {
224
278
  * `properties.style.fillColor` overlay. Mirrors `buildCirclePaint`.
225
279
  */
226
280
  /**
281
+ * Per-feature `text-field` value: the layer's literal text, or the feature's
282
+ * `labelProperty` value, uppercased when the feature's `textStyle` is
283
+ * `"caps"`. Factored out of `buildSymbolLayer` so `setLayerStyle`'s
284
+ * incremental `text-field` update (below) can reuse it instead of
285
+ * regressing to a caps-unaware plain `["get", ...]`.
286
+ */
287
+ function buildTextFieldExpression(text) {
288
+ const rawTextField = text.labelProperty ? ["get", text.labelProperty] : text.text ?? "";
289
+ return text.labelProperty ? [
290
+ "case",
291
+ [
292
+ "==",
293
+ ["get", "textStyle"],
294
+ "caps"
295
+ ],
296
+ ["upcase", rawTextField],
297
+ rawTextField
298
+ ] : rawTextField;
299
+ }
300
+ /**
227
301
  * Build the layout and paint objects for a symbol (text) layer.
228
302
  * Centralises the rotation convention so `createLayer` and `setLayerStyle`
229
303
  * stay in sync. Rotation follows the OpenLayers convention: `π − r` radians
@@ -277,16 +351,48 @@ function buildSymbolLayer(symbolId, sourceId, text) {
277
351
  "right",
278
352
  "center"
279
353
  ] : "center";
354
+ const textJustify = text.labelProperty ? [
355
+ "case",
356
+ [
357
+ "==",
358
+ ["get", "textJustify"],
359
+ "left"
360
+ ],
361
+ "left",
362
+ [
363
+ "==",
364
+ ["get", "textJustify"],
365
+ "right"
366
+ ],
367
+ "right",
368
+ "center"
369
+ ] : "center";
280
370
  return {
281
371
  id: symbolId,
282
372
  type: "symbol",
283
373
  source: sourceId,
284
374
  layout: {
285
- "text-field": text.labelProperty ? ["get", text.labelProperty] : text.text ?? "",
286
- "text-font": ["Open Sans Regular"],
375
+ "text-field": buildTextFieldExpression(text),
376
+ "text-font": text.labelProperty ? [
377
+ "case",
378
+ [
379
+ "==",
380
+ ["get", "textStyle"],
381
+ "italic"
382
+ ],
383
+ ["literal", ["Open Sans Italic"]],
384
+ [
385
+ "==",
386
+ ["get", "textStyle"],
387
+ "light"
388
+ ],
389
+ ["literal", ["Open Sans Light"]],
390
+ ["literal", ["Open Sans Regular"]]
391
+ ] : ["Open Sans Regular"],
287
392
  "text-size": buildTextSizeExpression(text),
288
393
  "text-rotate": textRotate,
289
394
  "text-anchor": textAnchor,
395
+ "text-justify": textJustify,
290
396
  "text-max-width": 9999,
291
397
  "text-rotation-alignment": "map",
292
398
  "text-pitch-alignment": "map",
@@ -305,7 +411,8 @@ function buildSymbolLayer(symbolId, sourceId, text) {
305
411
  hasTextBackground,
306
412
  3,
307
413
  0
308
- ]
414
+ ],
415
+ "text-opacity": TEXT_OPACITY_EXPRESSION
309
416
  }
310
417
  };
311
418
  }
@@ -795,8 +902,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
795
902
  this.map.setPaintProperty(mlId, "circle-stroke-width", paint["circle-stroke-width"]);
796
903
  this.map.setFilter(mlId, buildCirclePointFilter(style));
797
904
  } else if (type === "symbol" && style.text) {
798
- if (style.text.text) this.map.setLayoutProperty(mlId, "text-field", style.text.text);
799
- else if (style.text.labelProperty) this.map.setLayoutProperty(mlId, "text-field", ["get", style.text.labelProperty]);
905
+ if (style.text.text || style.text.labelProperty) this.map.setLayoutProperty(mlId, "text-field", buildTextFieldExpression(style.text));
800
906
  if (style.text.fillColor) this.map.setPaintProperty(mlId, "text-color", style.text.fillColor);
801
907
  if (style.text.strokeColor) this.map.setPaintProperty(mlId, "text-halo-color", style.text.strokeColor);
802
908
  if (style.text.strokeWidth !== void 0) this.map.setPaintProperty(mlId, "text-halo-width", style.text.strokeWidth);
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.15",
3
+ "version": "0.2.0-alpha.16",
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.22",
41
- "@orbat-mapper/tactical-draw": "^0.2.0-alpha.30"
40
+ "@orbat-mapper/control-measures": "^0.2.0-alpha.24",
41
+ "@orbat-mapper/tactical-draw": "^0.2.0-alpha.31"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "maplibre-gl": ">=4.0.0 <6.0.0"