@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 +14 -0
- package/dist/index.mjs +178 -204
- package/package.json +3 -3
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
|
-
|
|
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":
|
|
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
|
-
*
|
|
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":
|
|
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":
|
|
628
|
-
"text-halo-blur":
|
|
629
|
-
"text-opacity":
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1423
|
-
|
|
1424
|
-
this.map.setPaintProperty(layerId, "text-halo-
|
|
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.
|
|
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.
|
|
41
|
-
"@orbat-mapper/tactical-draw": "^0.8.
|
|
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"
|