@orbat-mapper/tactical-draw-adapter-maplibre 0.2.0-alpha.22 → 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.
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +147 -47
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -17,6 +17,10 @@ interface MapLibreGroupBundle {
|
|
|
17
17
|
readonly sourceId: string;
|
|
18
18
|
readonly layerIds: Map<PaintChannel, string>;
|
|
19
19
|
features: readonly Feature[];
|
|
20
|
+
/** Features most recently submitted to the native source, after MapLibre-specific promotion. */
|
|
21
|
+
sourceFeatures: readonly Feature[];
|
|
22
|
+
/** Whether the native source has received its first complete dataset. */
|
|
23
|
+
sourceInitialized: boolean;
|
|
20
24
|
/** Unset while the stored value describes older features than `features` holds. */
|
|
21
25
|
featureSignature: string | undefined;
|
|
22
26
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -28,6 +28,49 @@ function sameOrder(a, b) {
|
|
|
28
28
|
return true;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
+
* Replace the contents of shared-id features while adding/removing ids that
|
|
32
|
+
* entered or left a stable group bundle. Remove stale keys explicitly instead
|
|
33
|
+
* of combining `removeAllProperties` with `addOrUpdateProperties`: MapLibre's
|
|
34
|
+
* tile-index diff treats the former as exclusive and otherwise drops the
|
|
35
|
+
* replacement properties, including the promoted feature id.
|
|
36
|
+
*/
|
|
37
|
+
function buildGeoJSONSourceDiff(previousFeatures, nextFeatures) {
|
|
38
|
+
const previousById = new Map(previousFeatures.map((feature) => [feature.id, feature]));
|
|
39
|
+
const nextById = new Map(nextFeatures.map((feature) => [feature.id, feature]));
|
|
40
|
+
const remove = previousFeatures.filter((feature) => !nextById.has(feature.id)).map((feature) => feature.id);
|
|
41
|
+
const add = [];
|
|
42
|
+
const update = [];
|
|
43
|
+
for (const feature of nextFeatures) {
|
|
44
|
+
const previous = previousById.get(feature.id);
|
|
45
|
+
if (!previous) {
|
|
46
|
+
add.push(feature);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (feature.geometry === null) {
|
|
50
|
+
remove.push(feature.id);
|
|
51
|
+
add.push(feature);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const previousProperties = previous.properties ?? {};
|
|
55
|
+
const nextProperties = feature.properties ?? {};
|
|
56
|
+
const removeProperties = Object.keys(previousProperties).filter((key) => !Object.hasOwn(nextProperties, key));
|
|
57
|
+
update.push({
|
|
58
|
+
id: feature.id,
|
|
59
|
+
newGeometry: feature.geometry,
|
|
60
|
+
...removeProperties.length > 0 ? { removeProperties } : {},
|
|
61
|
+
addOrUpdateProperties: Object.entries(nextProperties).map(([key, value]) => ({
|
|
62
|
+
key,
|
|
63
|
+
value
|
|
64
|
+
}))
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
...remove.length > 0 ? { remove } : {},
|
|
69
|
+
...add.length > 0 ? { add } : {},
|
|
70
|
+
...update.length > 0 ? { update } : {}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
31
74
|
* Find the largest subset of bundles whose relative order already matches the
|
|
32
75
|
* desired stack. Reorders then move only the remaining bundles.
|
|
33
76
|
*/
|
|
@@ -189,7 +232,7 @@ const WEB_MERCATOR_CIRCUMFERENCE = 40075016.686;
|
|
|
189
232
|
* matching `getResolution`); the linear ramp between stops approximates the
|
|
190
233
|
* exponential exactly closely enough at 1-zoom granularity.
|
|
191
234
|
*/
|
|
192
|
-
function
|
|
235
|
+
function textSizeAtZoom(text, zoom) {
|
|
193
236
|
const minPx = text.textSizeMinPixels ?? DEFAULT_TEXT_SIZE_MIN_PIXELS;
|
|
194
237
|
const maxPx = text.textSizeMaxPixels ?? DEFAULT_TEXT_SIZE_MAX_PIXELS;
|
|
195
238
|
const screenOrDefault = [
|
|
@@ -197,43 +240,53 @@ function buildTextSizeExpression(text) {
|
|
|
197
240
|
["get", "textSizePixels"],
|
|
198
241
|
14
|
|
199
242
|
];
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
[
|
|
207
252
|
"case",
|
|
208
|
-
["has", "
|
|
253
|
+
["has", "textMaxSizePixels"],
|
|
209
254
|
[
|
|
210
|
-
"
|
|
211
|
-
["
|
|
255
|
+
"min",
|
|
256
|
+
["get", "textMaxSizePixels"],
|
|
257
|
+
groundPx
|
|
258
|
+
],
|
|
259
|
+
[
|
|
260
|
+
"min",
|
|
261
|
+
maxPx,
|
|
212
262
|
[
|
|
213
|
-
"
|
|
214
|
-
|
|
263
|
+
"max",
|
|
264
|
+
minPx,
|
|
215
265
|
groundPx
|
|
216
|
-
],
|
|
217
|
-
[
|
|
218
|
-
"min",
|
|
219
|
-
maxPx,
|
|
220
|
-
[
|
|
221
|
-
"max",
|
|
222
|
-
minPx,
|
|
223
|
-
groundPx
|
|
224
|
-
]
|
|
225
266
|
]
|
|
226
|
-
]
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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) {
|
|
230
280
|
return [
|
|
231
281
|
"interpolate",
|
|
232
282
|
["linear"],
|
|
233
283
|
["zoom"],
|
|
234
|
-
...Array.from({ length: 25 }, (_, zoom) => [zoom,
|
|
284
|
+
...Array.from({ length: 25 }, (_, zoom) => [zoom, at(zoom)]).flat()
|
|
235
285
|
];
|
|
236
286
|
}
|
|
287
|
+
function buildTextSizeExpression(text) {
|
|
288
|
+
return interpolateOverZoom((zoom) => textSizeAtZoom(text, zoom));
|
|
289
|
+
}
|
|
237
290
|
/**
|
|
238
291
|
* Companion to `buildTextSizeExpression` (ADR-0037): drives `text-opacity` to
|
|
239
292
|
* 0 once a ground-anchored Text graphic's derived on-screen size exceeds its
|
|
@@ -398,33 +451,72 @@ function buildTextHaloColorExpression(text) {
|
|
|
398
451
|
* `resolveTextHaloWidth`'s own outward-depth semantics, so the `textHalo`
|
|
399
452
|
* case uses its formula directly (unlike OpenLayers's straddling canvas
|
|
400
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.
|
|
401
463
|
*/
|
|
402
464
|
function buildTextHaloWidthExpression(text) {
|
|
403
|
-
|
|
465
|
+
const fallback = text.strokeColor ? text.strokeWidth ?? 1 : [
|
|
466
|
+
"case",
|
|
467
|
+
HAS_TEXT_BACKGROUND,
|
|
468
|
+
3,
|
|
469
|
+
0
|
|
470
|
+
];
|
|
471
|
+
return interpolateOverZoom((zoom) => [
|
|
404
472
|
"case",
|
|
405
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,
|
|
406
495
|
[
|
|
407
|
-
"
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
"/",
|
|
411
|
-
[
|
|
412
|
-
"coalesce",
|
|
413
|
-
["get", "textSizePixels"],
|
|
414
|
-
24
|
|
415
|
-
],
|
|
416
|
-
8
|
|
417
|
-
]
|
|
418
|
-
],
|
|
419
|
-
text.strokeColor ? text.strokeWidth ?? 1 : [
|
|
420
|
-
"case",
|
|
421
|
-
HAS_TEXT_BACKGROUND,
|
|
422
|
-
3,
|
|
423
|
-
0
|
|
496
|
+
"/",
|
|
497
|
+
textSizeAtZoom(text, zoom),
|
|
498
|
+
8
|
|
424
499
|
]
|
|
425
500
|
];
|
|
426
501
|
}
|
|
427
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
|
+
/**
|
|
428
520
|
* Build the layout and paint objects for a symbol (text) layer.
|
|
429
521
|
* Centralises the rotation convention so `createLayer` and `setLayerStyle`
|
|
430
522
|
* stay in sync. Rotation follows the OpenLayers convention: `π − r` radians
|
|
@@ -525,6 +617,7 @@ function buildSymbolLayer(symbolId, sourceId, text) {
|
|
|
525
617
|
"text-color": textColor,
|
|
526
618
|
"text-halo-color": buildTextHaloColorExpression(text),
|
|
527
619
|
"text-halo-width": buildTextHaloWidthExpression(text),
|
|
620
|
+
"text-halo-blur": buildTextHaloBlurExpression(text),
|
|
528
621
|
"text-opacity": TEXT_OPACITY_EXPRESSION
|
|
529
622
|
}
|
|
530
623
|
};
|
|
@@ -831,6 +924,8 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
831
924
|
sourceId,
|
|
832
925
|
layerIds: /* @__PURE__ */ new Map(),
|
|
833
926
|
features: [],
|
|
927
|
+
sourceFeatures: [],
|
|
928
|
+
sourceInitialized: false,
|
|
834
929
|
featureSignature: "[]",
|
|
835
930
|
requiredIconIds: /* @__PURE__ */ new Set()
|
|
836
931
|
};
|
|
@@ -924,10 +1019,12 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
924
1019
|
const source = this.map.getSource(bundle.sourceId);
|
|
925
1020
|
if (!source) return;
|
|
926
1021
|
const features = [...bundle.requiredIconIds].every((id) => this.map.hasImage(id)) ? bundle.features.map((feature) => this.promoteFeature(feature)) : [];
|
|
927
|
-
const result = source.setData({
|
|
1022
|
+
const result = bundle.sourceInitialized && typeof source.updateData === "function" ? source.updateData(buildGeoJSONSourceDiff(bundle.sourceFeatures, features), true) : source.setData({
|
|
928
1023
|
type: "FeatureCollection",
|
|
929
1024
|
features
|
|
930
1025
|
}, true);
|
|
1026
|
+
bundle.sourceFeatures = features;
|
|
1027
|
+
bundle.sourceInitialized = true;
|
|
931
1028
|
if (isPromiseLike(result)) Promise.resolve(result).catch(() => {});
|
|
932
1029
|
}
|
|
933
1030
|
orderGroupBundles(handle, previousGroupIds, desiredGroupIds, forcedGroupIds) {
|
|
@@ -1220,7 +1317,10 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
1220
1317
|
if (styleUpdate.text.text || styleUpdate.text.labelProperty) this.map.setLayoutProperty(layerId, "text-field", buildTextFieldExpression(resolvedStyle.text));
|
|
1221
1318
|
if (styleUpdate.text.fillColor) this.map.setPaintProperty(layerId, "text-color", resolvedStyle.text.fillColor);
|
|
1222
1319
|
if (styleUpdate.text.strokeColor) this.map.setPaintProperty(layerId, "text-halo-color", buildTextHaloColorExpression(resolvedStyle.text));
|
|
1223
|
-
if (styleUpdate.text.strokeWidth !== void 0)
|
|
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
|
+
}
|
|
1224
1324
|
break;
|
|
1225
1325
|
}
|
|
1226
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.
|
|
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/control-measures": "^0.2.0-alpha.
|
|
41
|
-
"@orbat-mapper/tactical-draw": "^0.2.0-alpha.
|
|
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"
|