@orbat-mapper/tactical-draw-adapter-maplibre 0.2.0-alpha.8 → 0.2.0
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 +69 -5
- package/dist/index.mjs +858 -86
- package/package.json +6 -5
package/dist/index.d.mts
CHANGED
|
@@ -1,37 +1,101 @@
|
|
|
1
|
-
import { BaseMapAdapter, EditPointerDriver,
|
|
1
|
+
import { BaseMapAdapter, EditPointerDriver, FeatureGroup, IconResource, LayerId, MapCursor, NativeEventEntry, PickHandler, PickOptions, PixelCoordinate, StyleOptions } from "@orbat-mapper/tactical-draw";
|
|
2
2
|
import maplibregl from "maplibre-gl";
|
|
3
|
-
import { Position } from "geojson";
|
|
3
|
+
import { Feature, Position } from "geojson";
|
|
4
4
|
|
|
5
5
|
//#region src/ml-adapter.d.ts
|
|
6
6
|
interface MapLibreLayerHandle {
|
|
7
7
|
sourceId: string;
|
|
8
8
|
/** MapLibre layer ids created for this logical layer (fill, line, circle, symbol) */
|
|
9
9
|
mlLayerIds: string[];
|
|
10
|
+
groupBundles: Map<string, MapLibreGroupBundle>;
|
|
11
|
+
groupOrder: string[];
|
|
12
|
+
nextGroupBundleId: number;
|
|
10
13
|
style?: StyleOptions;
|
|
11
14
|
}
|
|
15
|
+
type PaintChannel = "fill" | "fill-pattern" | "line" | "circle" | "icon" | "text";
|
|
16
|
+
interface MapLibreGroupBundle {
|
|
17
|
+
readonly sourceId: string;
|
|
18
|
+
readonly layerIds: Map<PaintChannel, string>;
|
|
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;
|
|
24
|
+
/** Unset while the stored value describes older features than `features` holds. */
|
|
25
|
+
featureSignature: string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Last `FeatureGroup.revision` seen for this bundle, when the producer
|
|
28
|
+
* supplied one. Two consecutive groups with the same `revision` are known
|
|
29
|
+
* to carry byte-identical `features`, so it stands in for `featureSignature`
|
|
30
|
+
* without paying to re-stringify the whole group. A missing revision on
|
|
31
|
+
* either side falls back to the signature comparison — see `applyFeatureGroups`.
|
|
32
|
+
*/
|
|
33
|
+
revision?: number;
|
|
34
|
+
requiredIconIds: ReadonlySet<string>;
|
|
35
|
+
}
|
|
12
36
|
type MapLibreEventToken = (e: any) => void;
|
|
13
37
|
interface MapLibreViewToken {
|
|
14
38
|
fn: () => void;
|
|
15
39
|
}
|
|
40
|
+
interface MapLibreAdapterOptions {
|
|
41
|
+
/**
|
|
42
|
+
* How view changes propagate to view-state refits. "continuous" (default)
|
|
43
|
+
* refits on every frame of an animated zoom/rotate/pitch; "settle" defers
|
|
44
|
+
* the refit until the animation ends.
|
|
45
|
+
*/
|
|
46
|
+
viewChangeMode?: "continuous" | "settle";
|
|
47
|
+
}
|
|
16
48
|
declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLibreEventToken, MapLibreViewToken> {
|
|
17
49
|
private map;
|
|
50
|
+
private readonly viewChangeMode;
|
|
18
51
|
private readonly pendingIconImages;
|
|
19
|
-
|
|
52
|
+
readonly scalesGroundPointSymbolsNatively = true;
|
|
53
|
+
constructor(map: maplibregl.Map, options?: MapLibreAdapterOptions);
|
|
20
54
|
private toContinuousLng;
|
|
21
55
|
protected createLayer(layerId: LayerId, style?: StyleOptions): MapLibreLayerHandle;
|
|
22
56
|
protected destroyLayer(handle: MapLibreLayerHandle): void;
|
|
23
|
-
protected
|
|
57
|
+
protected applyFeatureGroups(layerId: LayerId, groups: readonly FeatureGroup[]): void;
|
|
58
|
+
private createGroupBundle;
|
|
59
|
+
private destroyGroupBundle;
|
|
60
|
+
private clearGroupBundles;
|
|
61
|
+
private requiredIconIds;
|
|
62
|
+
private reconcileGroupChannels;
|
|
63
|
+
private buildGroupLayer;
|
|
64
|
+
private updateGroupBundleSource;
|
|
65
|
+
private orderGroupBundles;
|
|
66
|
+
private nativeLayerIds;
|
|
67
|
+
/**
|
|
68
|
+
* Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
|
|
69
|
+
* and picks can key on the caller's feature id) and, for a hatch-patterned
|
|
70
|
+
* Polygon, `_fillPatternImageId` (registering the tile image on demand). At
|
|
71
|
+
* most one object spread — a feature needing neither passes through
|
|
72
|
+
* untouched.
|
|
73
|
+
*/
|
|
74
|
+
private promoteFeature;
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a hatch-patterned Polygon's `style.fillPattern`/`fillColor` to its
|
|
77
|
+
* hatch-tile image id, registering the image on demand. Returns `undefined`
|
|
78
|
+
* for features without a hatch pattern, so `fillPatternImageId` stays the
|
|
79
|
+
* single source of truth for the id baked into `_fillPatternImageId`.
|
|
80
|
+
*/
|
|
81
|
+
private resolveFillPatternImage;
|
|
82
|
+
/** Register a hatch-tile image once per (color, pattern) pair, keyed by `fillPatternImageId`. */
|
|
83
|
+
private ensureFillPatternImage;
|
|
24
84
|
protected applyIconResources(resources: readonly IconResource[]): void;
|
|
25
85
|
protected removeIconResource(id: string): void;
|
|
26
86
|
protected attachNativeEvent(type: "click" | "pointermove" | "dblclick", onEvent: (lonLat: Position, pixel: PixelCoordinate, originalEvent: unknown) => void): NativeEventEntry<MapLibreEventToken>;
|
|
27
87
|
protected detachNativeEvent(entry: NativeEventEntry<MapLibreEventToken>): void;
|
|
28
88
|
protected attachViewChange(handler: () => void): MapLibreViewToken;
|
|
29
89
|
protected detachViewChange(token: MapLibreViewToken): void;
|
|
90
|
+
private viewChangeEvents;
|
|
30
91
|
createEditPointerDriver(): EditPointerDriver;
|
|
31
92
|
setLayerStyle(layerId: LayerId, style: StyleOptions): void;
|
|
93
|
+
private legacyPaintChannel;
|
|
94
|
+
private applyChannelStyle;
|
|
32
95
|
toLonLat(coordinate: number[]): Position;
|
|
33
96
|
fromLonLat(position: Position): number[];
|
|
34
97
|
getPixelFromCoordinate(position: Position): PixelCoordinate | null;
|
|
98
|
+
getCoordinateFromPixel(pixel: PixelCoordinate): Position | null;
|
|
35
99
|
getViewportSize(): {
|
|
36
100
|
width: number;
|
|
37
101
|
height: number;
|
|
@@ -48,4 +112,4 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
|
|
|
48
112
|
setGlobeMode(enabled: boolean): void;
|
|
49
113
|
}
|
|
50
114
|
//#endregion
|
|
51
|
-
export { MapLibreAdapter };
|
|
115
|
+
export { MapLibreAdapter, type MapLibreAdapterOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,115 @@
|
|
|
1
1
|
import { project, unproject } from "@orbat-mapper/control-measures";
|
|
2
|
-
import { BaseMapAdapter, DEFAULT_TEXT_SIZE_MAX_PIXELS, DEFAULT_TEXT_SIZE_MIN_PIXELS, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, bindAbortable, nearestWorldCopyLng, pickHitTolerance,
|
|
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
3
|
//#region src/ml-adapter.ts
|
|
4
|
+
/** Feature property key for the resolved hatch-pattern image id (see `promoteFeature`). */
|
|
5
|
+
const FILL_PATTERN_IMAGE_ID_PROPERTY = "_fillPatternImageId";
|
|
6
|
+
const DEFAULT_LINE_COLOR = "#000";
|
|
7
|
+
const DEFAULT_LINE_WIDTH = 2;
|
|
8
|
+
function isHatchPattern(value) {
|
|
9
|
+
return HATCH_PATTERNS.includes(value);
|
|
10
|
+
}
|
|
11
|
+
const PAINT_CHANNELS = [
|
|
12
|
+
"fill",
|
|
13
|
+
"fill-pattern",
|
|
14
|
+
"line",
|
|
15
|
+
"circle",
|
|
16
|
+
"icon",
|
|
17
|
+
"text"
|
|
18
|
+
];
|
|
19
|
+
function firstBundleLayerId(bundle) {
|
|
20
|
+
for (const channel of PAINT_CHANNELS) {
|
|
21
|
+
const layerId = bundle.layerIds.get(channel);
|
|
22
|
+
if (layerId) return layerId;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function sameOrder(a, b) {
|
|
26
|
+
if (a.length !== b.length) return false;
|
|
27
|
+
for (let index = 0; index < a.length; index++) if (a[index] !== b[index]) return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
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
|
+
/**
|
|
74
|
+
* Find the largest subset of bundles whose relative order already matches the
|
|
75
|
+
* desired stack. Reorders then move only the remaining bundles.
|
|
76
|
+
*/
|
|
77
|
+
function longestStableGroupIds(previousGroupIds, desiredGroupIds) {
|
|
78
|
+
const previousIndices = new Map(previousGroupIds.map((groupId, index) => [groupId, index]));
|
|
79
|
+
const sequence = desiredGroupIds.flatMap((groupId) => {
|
|
80
|
+
const previousIndex = previousIndices.get(groupId);
|
|
81
|
+
return previousIndex === void 0 ? [] : [{
|
|
82
|
+
groupId,
|
|
83
|
+
previousIndex
|
|
84
|
+
}];
|
|
85
|
+
});
|
|
86
|
+
const tails = [];
|
|
87
|
+
const tailSequenceIndices = [];
|
|
88
|
+
const predecessors = new Array(sequence.length).fill(-1);
|
|
89
|
+
for (let sequenceIndex = 0; sequenceIndex < sequence.length; sequenceIndex++) {
|
|
90
|
+
const previousIndex = sequence[sequenceIndex].previousIndex;
|
|
91
|
+
let low = 0;
|
|
92
|
+
let high = tails.length;
|
|
93
|
+
while (low < high) {
|
|
94
|
+
const middle = low + high >>> 1;
|
|
95
|
+
if (tails[middle] < previousIndex) low = middle + 1;
|
|
96
|
+
else high = middle;
|
|
97
|
+
}
|
|
98
|
+
if (low > 0) predecessors[sequenceIndex] = tailSequenceIndices[low - 1];
|
|
99
|
+
tails[low] = previousIndex;
|
|
100
|
+
tailSequenceIndices[low] = sequenceIndex;
|
|
101
|
+
}
|
|
102
|
+
const stableGroupIds = /* @__PURE__ */ new Set();
|
|
103
|
+
let sequenceIndex = tailSequenceIndices.at(-1);
|
|
104
|
+
while (sequenceIndex !== void 0 && sequenceIndex >= 0) {
|
|
105
|
+
stableGroupIds.add(sequence[sequenceIndex].groupId);
|
|
106
|
+
sequenceIndex = predecessors[sequenceIndex];
|
|
107
|
+
}
|
|
108
|
+
return stableGroupIds;
|
|
109
|
+
}
|
|
110
|
+
function isPromiseLike(value) {
|
|
111
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
112
|
+
}
|
|
4
113
|
function emptyFC() {
|
|
5
114
|
return {
|
|
6
115
|
type: "FeatureCollection",
|
|
@@ -80,11 +189,7 @@ function buildIconLayer(id, source) {
|
|
|
80
189
|
],
|
|
81
190
|
layout: {
|
|
82
191
|
"icon-image": ["get", "iconId"],
|
|
83
|
-
"icon-size":
|
|
84
|
-
"coalesce",
|
|
85
|
-
["get", "iconScale"],
|
|
86
|
-
1
|
|
87
|
-
],
|
|
192
|
+
"icon-size": buildIconSizeExpression(),
|
|
88
193
|
"icon-rotate": [
|
|
89
194
|
"*",
|
|
90
195
|
[
|
|
@@ -97,7 +202,8 @@ function buildIconLayer(id, source) {
|
|
|
97
202
|
"icon-rotation-alignment": "viewport",
|
|
98
203
|
"icon-pitch-alignment": "viewport",
|
|
99
204
|
"icon-allow-overlap": true,
|
|
100
|
-
"icon-ignore-placement": true
|
|
205
|
+
"icon-ignore-placement": true,
|
|
206
|
+
"symbol-z-order": "source"
|
|
101
207
|
},
|
|
102
208
|
paint: { "icon-opacity": [
|
|
103
209
|
"coalesce",
|
|
@@ -126,7 +232,7 @@ const WEB_MERCATOR_CIRCUMFERENCE = 40075016.686;
|
|
|
126
232
|
* matching `getResolution`); the linear ramp between stops approximates the
|
|
127
233
|
* exponential exactly closely enough at 1-zoom granularity.
|
|
128
234
|
*/
|
|
129
|
-
function
|
|
235
|
+
function textSizeAtZoom(text, zoom) {
|
|
130
236
|
const minPx = text.textSizeMinPixels ?? DEFAULT_TEXT_SIZE_MIN_PIXELS;
|
|
131
237
|
const maxPx = text.textSizeMaxPixels ?? DEFAULT_TEXT_SIZE_MAX_PIXELS;
|
|
132
238
|
const screenOrDefault = [
|
|
@@ -134,31 +240,149 @@ function buildTextSizeExpression(text) {
|
|
|
134
240
|
["get", "textSizePixels"],
|
|
135
241
|
14
|
|
136
242
|
];
|
|
137
|
-
const
|
|
138
|
-
|
|
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
|
+
[
|
|
139
252
|
"case",
|
|
140
|
-
["has", "
|
|
253
|
+
["has", "textMaxSizePixels"],
|
|
254
|
+
[
|
|
255
|
+
"min",
|
|
256
|
+
["get", "textMaxSizePixels"],
|
|
257
|
+
groundPx
|
|
258
|
+
],
|
|
141
259
|
[
|
|
142
260
|
"min",
|
|
143
261
|
maxPx,
|
|
144
262
|
[
|
|
145
263
|
"max",
|
|
146
264
|
minPx,
|
|
265
|
+
groundPx
|
|
266
|
+
]
|
|
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) {
|
|
280
|
+
return [
|
|
281
|
+
"interpolate",
|
|
282
|
+
["linear"],
|
|
283
|
+
["zoom"],
|
|
284
|
+
...Array.from({ length: 25 }, (_, zoom) => [zoom, at(zoom)]).flat()
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
function buildTextSizeExpression(text) {
|
|
288
|
+
return interpolateOverZoom((zoom) => textSizeAtZoom(text, zoom));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Companion to `buildTextSizeExpression` (ADR-0037): drives `text-opacity` to
|
|
292
|
+
* 0 once a ground-anchored Text graphic's derived on-screen size exceeds its
|
|
293
|
+
* `textMaxSizePixels` threshold, 1 otherwise (including every feature without
|
|
294
|
+
* a threshold, e.g. amplifier labels). Layout properties like `text-size`
|
|
295
|
+
* cannot represent "hidden", so hiding is a paint-property concern; `step`
|
|
296
|
+
* (rather than `interpolate`) is enough since opacity only ever needs to
|
|
297
|
+
* flip at a stop, evaluated at the same per-zoom granularity as the size
|
|
298
|
+
* expression.
|
|
299
|
+
*/
|
|
300
|
+
const TEXT_OPACITY_EXPRESSION = (() => {
|
|
301
|
+
const opacityAtZoom = (zoom) => {
|
|
302
|
+
return [
|
|
303
|
+
"case",
|
|
304
|
+
[
|
|
305
|
+
"all",
|
|
306
|
+
["has", "textSizeMeters"],
|
|
307
|
+
["has", "textMaxSizePixels"]
|
|
308
|
+
],
|
|
309
|
+
[
|
|
310
|
+
"case",
|
|
311
|
+
[
|
|
312
|
+
">",
|
|
147
313
|
[
|
|
148
314
|
"*",
|
|
149
315
|
["get", "textSizeMeters"],
|
|
150
316
|
Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
|
|
317
|
+
],
|
|
318
|
+
["get", "textMaxSizePixels"]
|
|
319
|
+
],
|
|
320
|
+
0,
|
|
321
|
+
1
|
|
322
|
+
],
|
|
323
|
+
1
|
|
324
|
+
];
|
|
325
|
+
};
|
|
326
|
+
const stops = Array.from({ length: 25 }, (_, zoom) => zoom);
|
|
327
|
+
return [
|
|
328
|
+
"step",
|
|
329
|
+
["zoom"],
|
|
330
|
+
opacityAtZoom(0),
|
|
331
|
+
...stops.slice(1).flatMap((zoom) => [zoom, opacityAtZoom(zoom)])
|
|
332
|
+
];
|
|
333
|
+
})();
|
|
334
|
+
/**
|
|
335
|
+
* Per-feature icon size, same zoom-stop scheme as `buildTextSizeExpression`:
|
|
336
|
+
* a ground-sized point symbol carries `iconSizeMeters`/`iconOctagonSize` (and
|
|
337
|
+
* optional `iconMinPixels`/`iconMaxPixels`), so the meters→pixels clamp is
|
|
338
|
+
* evaluated inside the style layer at every integer zoom stop and the symbol
|
|
339
|
+
* scales smoothly through animated zooms. Anything else falls back to the
|
|
340
|
+
* host-computed `iconScale` snapshot.
|
|
341
|
+
*/
|
|
342
|
+
function buildIconSizeExpression() {
|
|
343
|
+
const minExpr = [
|
|
344
|
+
"coalesce",
|
|
345
|
+
["get", "iconMinPixels"],
|
|
346
|
+
0
|
|
347
|
+
];
|
|
348
|
+
const maxExpr = [
|
|
349
|
+
"coalesce",
|
|
350
|
+
["get", "iconMaxPixels"],
|
|
351
|
+
1e9
|
|
352
|
+
];
|
|
353
|
+
const scaleAtZoom = (zoom) => {
|
|
354
|
+
return [
|
|
355
|
+
"case",
|
|
356
|
+
["has", "iconSizeMeters"],
|
|
357
|
+
[
|
|
358
|
+
"/",
|
|
359
|
+
[
|
|
360
|
+
"max",
|
|
361
|
+
minExpr,
|
|
362
|
+
[
|
|
363
|
+
"min",
|
|
364
|
+
maxExpr,
|
|
365
|
+
[
|
|
366
|
+
"*",
|
|
367
|
+
["get", "iconSizeMeters"],
|
|
368
|
+
Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
|
|
369
|
+
]
|
|
151
370
|
]
|
|
152
|
-
]
|
|
371
|
+
],
|
|
372
|
+
["get", "iconOctagonSize"]
|
|
153
373
|
],
|
|
154
|
-
|
|
374
|
+
[
|
|
375
|
+
"coalesce",
|
|
376
|
+
["get", "iconScale"],
|
|
377
|
+
1
|
|
378
|
+
]
|
|
155
379
|
];
|
|
156
380
|
};
|
|
157
381
|
return [
|
|
158
382
|
"interpolate",
|
|
159
383
|
["linear"],
|
|
160
384
|
["zoom"],
|
|
161
|
-
...Array.from({ length: 25 }, (_, zoom) => [zoom,
|
|
385
|
+
...Array.from({ length: 25 }, (_, zoom) => [zoom, scaleAtZoom(zoom)]).flat()
|
|
162
386
|
];
|
|
163
387
|
}
|
|
164
388
|
/**
|
|
@@ -166,6 +390,133 @@ function buildTextSizeExpression(text) {
|
|
|
166
390
|
* `properties.style.fillColor` overlay. Mirrors `buildCirclePaint`.
|
|
167
391
|
*/
|
|
168
392
|
/**
|
|
393
|
+
* Per-feature `text-field` value: the layer's literal text, or the feature's
|
|
394
|
+
* `labelProperty` value, uppercased when the feature's `textStyle` is
|
|
395
|
+
* `"caps"`. Factored out of `buildSymbolLayer` so `setLayerStyle`'s
|
|
396
|
+
* incremental `text-field` update (below) can reuse it instead of
|
|
397
|
+
* regressing to a caps-unaware plain `["get", ...]`.
|
|
398
|
+
*/
|
|
399
|
+
function buildTextFieldExpression(text) {
|
|
400
|
+
const rawTextField = text.labelProperty ? ["get", text.labelProperty] : text.text ?? "";
|
|
401
|
+
return text.labelProperty ? [
|
|
402
|
+
"case",
|
|
403
|
+
[
|
|
404
|
+
"==",
|
|
405
|
+
["get", "textStyle"],
|
|
406
|
+
"caps"
|
|
407
|
+
],
|
|
408
|
+
["upcase", rawTextField],
|
|
409
|
+
rawTextField
|
|
410
|
+
] : rawTextField;
|
|
411
|
+
}
|
|
412
|
+
const HAS_TEXT_HALO = [
|
|
413
|
+
"boolean",
|
|
414
|
+
["get", "textHalo"],
|
|
415
|
+
false
|
|
416
|
+
];
|
|
417
|
+
const HAS_TEXT_BACKGROUND = [
|
|
418
|
+
"boolean",
|
|
419
|
+
["get", "textBackground"],
|
|
420
|
+
false
|
|
421
|
+
];
|
|
422
|
+
/**
|
|
423
|
+
* Per-feature `text-halo-color` value: `textHalo` wins over both the layer's
|
|
424
|
+
* `text.strokeColor` and the `textBackground` halo, matching the OL/Leaflet
|
|
425
|
+
* adapters' precedence. Factored out so `setLayerStyle`'s incremental
|
|
426
|
+
* `text-halo-color` update (below) can reuse it instead of dropping the
|
|
427
|
+
* `textHalo` case.
|
|
428
|
+
*/
|
|
429
|
+
function buildTextHaloColorExpression(text) {
|
|
430
|
+
const fallback = text.strokeColor ? text.strokeColor : [
|
|
431
|
+
"case",
|
|
432
|
+
HAS_TEXT_BACKGROUND,
|
|
433
|
+
"#ffffff",
|
|
434
|
+
"rgba(255,255,255,0)"
|
|
435
|
+
];
|
|
436
|
+
return [
|
|
437
|
+
"case",
|
|
438
|
+
HAS_TEXT_HALO,
|
|
439
|
+
[
|
|
440
|
+
"coalesce",
|
|
441
|
+
["get", "textHaloColor"],
|
|
442
|
+
TEXT_HALO_COLOR
|
|
443
|
+
],
|
|
444
|
+
fallback
|
|
445
|
+
];
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Per-feature `text-halo-width` value, mirroring
|
|
449
|
+
* `buildTextHaloColorExpression`'s precedence. MapLibre's `text-halo-width`
|
|
450
|
+
* already extends outward from the glyph edge, matching
|
|
451
|
+
* `resolveTextHaloWidth`'s own outward-depth semantics, so the `textHalo`
|
|
452
|
+
* case uses its formula directly (unlike OpenLayers's straddling canvas
|
|
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.
|
|
463
|
+
*/
|
|
464
|
+
function buildTextHaloWidthExpression(text) {
|
|
465
|
+
const fallback = text.strokeColor ? text.strokeWidth ?? 1 : [
|
|
466
|
+
"case",
|
|
467
|
+
HAS_TEXT_BACKGROUND,
|
|
468
|
+
3,
|
|
469
|
+
0
|
|
470
|
+
];
|
|
471
|
+
return interpolateOverZoom((zoom) => [
|
|
472
|
+
"case",
|
|
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,
|
|
495
|
+
[
|
|
496
|
+
"/",
|
|
497
|
+
textSizeAtZoom(text, zoom),
|
|
498
|
+
8
|
|
499
|
+
]
|
|
500
|
+
];
|
|
501
|
+
}
|
|
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
|
+
/**
|
|
169
520
|
* Build the layout and paint objects for a symbol (text) layer.
|
|
170
521
|
* Centralises the rotation convention so `createLayer` and `setLayerStyle`
|
|
171
522
|
* stay in sync. Rotation follows the OpenLayers convention: `π − r` radians
|
|
@@ -214,26 +565,60 @@ function buildSymbolLayer(symbolId, sourceId, text) {
|
|
|
214
565
|
"right",
|
|
215
566
|
"center"
|
|
216
567
|
] : "center";
|
|
568
|
+
const textJustify = text.labelProperty ? [
|
|
569
|
+
"case",
|
|
570
|
+
[
|
|
571
|
+
"==",
|
|
572
|
+
["get", "textJustify"],
|
|
573
|
+
"left"
|
|
574
|
+
],
|
|
575
|
+
"left",
|
|
576
|
+
[
|
|
577
|
+
"==",
|
|
578
|
+
["get", "textJustify"],
|
|
579
|
+
"right"
|
|
580
|
+
],
|
|
581
|
+
"right",
|
|
582
|
+
"center"
|
|
583
|
+
] : "center";
|
|
217
584
|
return {
|
|
218
585
|
id: symbolId,
|
|
219
586
|
type: "symbol",
|
|
220
587
|
source: sourceId,
|
|
221
588
|
layout: {
|
|
222
|
-
"text-field": text
|
|
223
|
-
"text-font":
|
|
589
|
+
"text-field": buildTextFieldExpression(text),
|
|
590
|
+
"text-font": text.labelProperty ? [
|
|
591
|
+
"case",
|
|
592
|
+
[
|
|
593
|
+
"==",
|
|
594
|
+
["get", "textStyle"],
|
|
595
|
+
"italic"
|
|
596
|
+
],
|
|
597
|
+
["literal", ["Open Sans Italic"]],
|
|
598
|
+
[
|
|
599
|
+
"==",
|
|
600
|
+
["get", "textStyle"],
|
|
601
|
+
"light"
|
|
602
|
+
],
|
|
603
|
+
["literal", ["Open Sans Light"]],
|
|
604
|
+
["literal", ["Open Sans Regular"]]
|
|
605
|
+
] : ["Open Sans Regular"],
|
|
224
606
|
"text-size": buildTextSizeExpression(text),
|
|
225
607
|
"text-rotate": textRotate,
|
|
226
608
|
"text-anchor": textAnchor,
|
|
609
|
+
"text-justify": textJustify,
|
|
610
|
+
"text-max-width": 9999,
|
|
227
611
|
"text-rotation-alignment": "map",
|
|
228
612
|
"text-pitch-alignment": "map",
|
|
229
|
-
"text-allow-overlap": true
|
|
613
|
+
"text-allow-overlap": true,
|
|
614
|
+
"symbol-z-order": "source"
|
|
230
615
|
},
|
|
231
616
|
paint: {
|
|
232
617
|
"text-color": textColor,
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
618
|
+
"text-halo-color": buildTextHaloColorExpression(text),
|
|
619
|
+
"text-halo-width": buildTextHaloWidthExpression(text),
|
|
620
|
+
"text-halo-blur": buildTextHaloBlurExpression(text),
|
|
621
|
+
"text-opacity": TEXT_OPACITY_EXPRESSION
|
|
237
622
|
}
|
|
238
623
|
};
|
|
239
624
|
}
|
|
@@ -255,14 +640,79 @@ function buildFillPaint(style) {
|
|
|
255
640
|
"fill-opacity": 1
|
|
256
641
|
};
|
|
257
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* `properties.style.fillPattern` read as a GL expression, defaulting to
|
|
645
|
+
* `"solid"`. `fillPattern` is a per-feature-only field (ADR-0007) — unlike
|
|
646
|
+
* `fillColor`/`strokeColor` it has no layer-level `StyleOptions` default, so
|
|
647
|
+
* this expression takes no `style` argument.
|
|
648
|
+
*/
|
|
649
|
+
function fillPatternField() {
|
|
650
|
+
return [
|
|
651
|
+
"coalesce",
|
|
652
|
+
[
|
|
653
|
+
"get",
|
|
654
|
+
"fillPattern",
|
|
655
|
+
[
|
|
656
|
+
"coalesce",
|
|
657
|
+
["get", "style"],
|
|
658
|
+
["literal", {}]
|
|
659
|
+
]
|
|
660
|
+
],
|
|
661
|
+
"solid"
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* Filter for one of the two mutually-exclusive Polygon fill layers, keyed on
|
|
666
|
+
* whether the feature's resolved `fillPattern` is a hatch pattern. The solid
|
|
667
|
+
* `-fill` layer takes `hatched: false` (every Polygon except hatched ones);
|
|
668
|
+
* the `-fill-pattern` layer takes `hatched: true`. Split because MapLibre's
|
|
669
|
+
* `fill-color` and `fill-pattern` paints cannot coexist on one fill layer.
|
|
670
|
+
*/
|
|
671
|
+
function buildFillFilter(hatched) {
|
|
672
|
+
const isHatched = [
|
|
673
|
+
"in",
|
|
674
|
+
fillPatternField(),
|
|
675
|
+
["literal", [...HATCH_PATTERNS]]
|
|
676
|
+
];
|
|
677
|
+
return [
|
|
678
|
+
"all",
|
|
679
|
+
[
|
|
680
|
+
"==",
|
|
681
|
+
["geometry-type"],
|
|
682
|
+
"Polygon"
|
|
683
|
+
],
|
|
684
|
+
hatched ? isHatched : ["!", isHatched]
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Paint for the hatch-pattern fill layer. The image id is baked per-feature
|
|
689
|
+
* into `properties._fillPatternImageId` (see `promoteFeature`), so the paint
|
|
690
|
+
* is a plain `["get", ...]` rather than re-deriving the id as a GL expression,
|
|
691
|
+
* keeping `fillPatternImageId` the single source of truth for the id string.
|
|
692
|
+
*/
|
|
693
|
+
const FILL_PATTERN_PAINT = {
|
|
694
|
+
"fill-pattern": ["get", FILL_PATTERN_IMAGE_ID_PROPERTY],
|
|
695
|
+
"fill-opacity": 1
|
|
696
|
+
};
|
|
697
|
+
const SOLID_LINE_DASH = [1, 0];
|
|
258
698
|
function buildLinePaint(style) {
|
|
259
|
-
const colorDefault = style?.strokeColor ??
|
|
260
|
-
const widthDefault = style?.strokeWidth ??
|
|
699
|
+
const colorDefault = style?.strokeColor ?? DEFAULT_LINE_COLOR;
|
|
700
|
+
const widthDefault = style?.strokeWidth ?? DEFAULT_LINE_WIDTH;
|
|
701
|
+
const dashDefault = style?.strokeDash?.length ? [...style.strokeDash] : SOLID_LINE_DASH;
|
|
261
702
|
const styleObj = [
|
|
262
703
|
"coalesce",
|
|
263
704
|
["get", "style"],
|
|
264
705
|
["literal", {}]
|
|
265
706
|
];
|
|
707
|
+
const dashExpr = [
|
|
708
|
+
"coalesce",
|
|
709
|
+
[
|
|
710
|
+
"get",
|
|
711
|
+
"strokeDash",
|
|
712
|
+
styleObj
|
|
713
|
+
],
|
|
714
|
+
["literal", dashDefault]
|
|
715
|
+
];
|
|
266
716
|
return {
|
|
267
717
|
"line-color": [
|
|
268
718
|
"coalesce",
|
|
@@ -282,15 +732,63 @@ function buildLinePaint(style) {
|
|
|
282
732
|
],
|
|
283
733
|
widthDefault
|
|
284
734
|
],
|
|
285
|
-
|
|
735
|
+
"line-dasharray": [
|
|
736
|
+
"case",
|
|
737
|
+
[
|
|
738
|
+
">",
|
|
739
|
+
["length", dashExpr],
|
|
740
|
+
0
|
|
741
|
+
],
|
|
742
|
+
dashExpr,
|
|
743
|
+
["literal", SOLID_LINE_DASH]
|
|
744
|
+
]
|
|
286
745
|
};
|
|
287
746
|
}
|
|
747
|
+
function geometryIncludes(feature, geometryTypes) {
|
|
748
|
+
const geometry = feature.geometry;
|
|
749
|
+
if (!geometry) return false;
|
|
750
|
+
if (geometry.type === "GeometryCollection") return geometry.geometries.some((candidate) => geometryTypes.has(candidate.type));
|
|
751
|
+
return geometryTypes.has(geometry.type);
|
|
752
|
+
}
|
|
753
|
+
const POLYGON_GEOMETRIES = new Set(["Polygon", "MultiPolygon"]);
|
|
754
|
+
const LINE_GEOMETRIES = new Set([
|
|
755
|
+
"LineString",
|
|
756
|
+
"MultiLineString",
|
|
757
|
+
"Polygon",
|
|
758
|
+
"MultiPolygon"
|
|
759
|
+
]);
|
|
760
|
+
const POINT_GEOMETRIES = new Set(["Point", "MultiPoint"]);
|
|
761
|
+
function featureStyle(feature) {
|
|
762
|
+
const style = feature.properties?.style;
|
|
763
|
+
return style && typeof style === "object" ? style : {};
|
|
764
|
+
}
|
|
765
|
+
function groupPaintChannels(features, style) {
|
|
766
|
+
const channels = /* @__PURE__ */ new Set();
|
|
767
|
+
for (const feature of features) {
|
|
768
|
+
if (geometryIncludes(feature, POLYGON_GEOMETRIES)) {
|
|
769
|
+
const fillPattern = featureStyle(feature).fillPattern;
|
|
770
|
+
if (featureStyle(feature).fillColor !== void 0 || style?.fillColor !== void 0 || isHatchPattern(fillPattern)) channels.add(isHatchPattern(fillPattern) ? "fill-pattern" : "fill");
|
|
771
|
+
}
|
|
772
|
+
if (geometryIncludes(feature, LINE_GEOMETRIES)) channels.add("line");
|
|
773
|
+
if (!geometryIncludes(feature, POINT_GEOMETRIES)) continue;
|
|
774
|
+
const properties = feature.properties ?? {};
|
|
775
|
+
const hasIcon = properties.iconId != null;
|
|
776
|
+
const hasText = style?.text !== void 0 && (style.text.labelProperty ? properties[style.text.labelProperty] != null : style.text.text !== void 0);
|
|
777
|
+
if (hasIcon) channels.add("icon");
|
|
778
|
+
if (hasText) channels.add("text");
|
|
779
|
+
if (!hasIcon && !hasText) channels.add("circle");
|
|
780
|
+
}
|
|
781
|
+
return channels;
|
|
782
|
+
}
|
|
288
783
|
var MapLibreAdapter = class extends BaseMapAdapter {
|
|
289
784
|
map;
|
|
785
|
+
viewChangeMode;
|
|
290
786
|
pendingIconImages = /* @__PURE__ */ new Set();
|
|
291
|
-
|
|
787
|
+
scalesGroundPointSymbolsNatively = true;
|
|
788
|
+
constructor(map, options = {}) {
|
|
292
789
|
super();
|
|
293
790
|
this.map = map;
|
|
791
|
+
this.viewChangeMode = options.viewChangeMode ?? "continuous";
|
|
294
792
|
}
|
|
295
793
|
toContinuousLng(lng) {
|
|
296
794
|
return nearestWorldCopyLng(lng, this.map.getCenter().lng);
|
|
@@ -308,14 +806,19 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
308
806
|
id: fillId,
|
|
309
807
|
type: "fill",
|
|
310
808
|
source: sourceId,
|
|
311
|
-
filter:
|
|
312
|
-
"==",
|
|
313
|
-
"$type",
|
|
314
|
-
"Polygon"
|
|
315
|
-
],
|
|
809
|
+
filter: buildFillFilter(false),
|
|
316
810
|
paint: buildFillPaint(style)
|
|
317
811
|
});
|
|
318
812
|
mlLayerIds.push(fillId);
|
|
813
|
+
const fillPatternId = `${layerId}-fill-pattern`;
|
|
814
|
+
this.map.addLayer({
|
|
815
|
+
id: fillPatternId,
|
|
816
|
+
type: "fill",
|
|
817
|
+
source: sourceId,
|
|
818
|
+
filter: buildFillFilter(true),
|
|
819
|
+
paint: FILL_PATTERN_PAINT
|
|
820
|
+
});
|
|
821
|
+
mlLayerIds.push(fillPatternId);
|
|
319
822
|
const lineId = `${layerId}-line`;
|
|
320
823
|
this.map.addLayer({
|
|
321
824
|
id: lineId,
|
|
@@ -361,29 +864,228 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
361
864
|
return {
|
|
362
865
|
sourceId,
|
|
363
866
|
mlLayerIds,
|
|
867
|
+
groupBundles: /* @__PURE__ */ new Map(),
|
|
868
|
+
groupOrder: [],
|
|
869
|
+
nextGroupBundleId: 0,
|
|
364
870
|
style
|
|
365
871
|
};
|
|
366
872
|
}
|
|
367
873
|
destroyLayer(handle) {
|
|
874
|
+
this.clearGroupBundles(handle);
|
|
368
875
|
for (const mlId of handle.mlLayerIds) if (this.map.getLayer(mlId)) this.map.removeLayer(mlId);
|
|
369
876
|
if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
|
|
370
877
|
}
|
|
371
|
-
|
|
878
|
+
applyFeatureGroups(layerId, groups) {
|
|
372
879
|
const handle = this.layers.get(layerId);
|
|
373
880
|
if (!handle) return;
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
881
|
+
const desiredGroupIds = new Set(groups.map((group) => group.id));
|
|
882
|
+
const previousGroupOrder = handle.groupOrder.filter((groupId) => desiredGroupIds.has(groupId));
|
|
883
|
+
const bundlesNeedingOrder = /* @__PURE__ */ new Set();
|
|
884
|
+
for (const [groupId, bundle] of handle.groupBundles) {
|
|
885
|
+
if (desiredGroupIds.has(groupId)) continue;
|
|
886
|
+
this.destroyGroupBundle(bundle);
|
|
887
|
+
handle.groupBundles.delete(groupId);
|
|
888
|
+
}
|
|
889
|
+
for (const group of groups) {
|
|
890
|
+
let bundle = handle.groupBundles.get(group.id);
|
|
891
|
+
const isNewBundle = bundle === void 0;
|
|
892
|
+
if (!bundle) {
|
|
893
|
+
bundle = this.createGroupBundle(layerId, handle);
|
|
894
|
+
handle.groupBundles.set(group.id, bundle);
|
|
895
|
+
}
|
|
896
|
+
let contentChanged;
|
|
897
|
+
if (!isNewBundle && bundle.revision !== void 0 && group.revision !== void 0) {
|
|
898
|
+
contentChanged = bundle.revision !== group.revision;
|
|
899
|
+
if (contentChanged) bundle.featureSignature = void 0;
|
|
900
|
+
} else {
|
|
901
|
+
const featureSignature = JSON.stringify(group.features);
|
|
902
|
+
contentChanged = isNewBundle || bundle.featureSignature !== featureSignature;
|
|
903
|
+
bundle.featureSignature = featureSignature;
|
|
904
|
+
}
|
|
905
|
+
const requiredIconIds = this.requiredIconIds(group);
|
|
906
|
+
const resourcesChanged = bundle.requiredIconIds.size !== requiredIconIds.size || [...bundle.requiredIconIds].some((id) => !requiredIconIds.has(id));
|
|
907
|
+
bundle.features = group.features;
|
|
908
|
+
bundle.revision = group.revision;
|
|
909
|
+
bundle.requiredIconIds = requiredIconIds;
|
|
910
|
+
if (this.reconcileGroupChannels(handle, bundle, groupPaintChannels(group.features, handle.style))) bundlesNeedingOrder.add(group.id);
|
|
911
|
+
if (contentChanged || resourcesChanged) this.updateGroupBundleSource(bundle);
|
|
912
|
+
}
|
|
913
|
+
handle.groupOrder = groups.map((group) => group.id);
|
|
914
|
+
this.orderGroupBundles(handle, previousGroupOrder, handle.groupOrder, bundlesNeedingOrder);
|
|
915
|
+
}
|
|
916
|
+
createGroupBundle(layerId, handle) {
|
|
917
|
+
const sourceId = `${layerId}-group-${++handle.nextGroupBundleId}-src`;
|
|
918
|
+
this.map.addSource(sourceId, {
|
|
919
|
+
type: "geojson",
|
|
920
|
+
data: emptyFC(),
|
|
921
|
+
promoteId: "_fid"
|
|
922
|
+
});
|
|
923
|
+
return {
|
|
924
|
+
sourceId,
|
|
925
|
+
layerIds: /* @__PURE__ */ new Map(),
|
|
926
|
+
features: [],
|
|
927
|
+
sourceFeatures: [],
|
|
928
|
+
sourceInitialized: false,
|
|
929
|
+
featureSignature: "[]",
|
|
930
|
+
requiredIconIds: /* @__PURE__ */ new Set()
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
destroyGroupBundle(bundle) {
|
|
934
|
+
for (const layerId of bundle.layerIds.values()) if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
935
|
+
if (this.map.getSource(bundle.sourceId)) this.map.removeSource(bundle.sourceId);
|
|
936
|
+
}
|
|
937
|
+
clearGroupBundles(handle) {
|
|
938
|
+
for (const bundle of handle.groupBundles.values()) this.destroyGroupBundle(bundle);
|
|
939
|
+
handle.groupBundles.clear();
|
|
940
|
+
handle.groupOrder = [];
|
|
941
|
+
}
|
|
942
|
+
requiredIconIds(group) {
|
|
943
|
+
const referenced = new Set(group.features.flatMap((feature) => {
|
|
944
|
+
const iconId = feature.properties?.iconId;
|
|
945
|
+
return typeof iconId === "string" ? [iconId] : [];
|
|
946
|
+
}));
|
|
947
|
+
return new Set(group.iconResources.map((resource) => resource.id).filter((resourceId) => referenced.has(resourceId)));
|
|
948
|
+
}
|
|
949
|
+
reconcileGroupChannels(handle, bundle, desiredChannels) {
|
|
950
|
+
let changed = false;
|
|
951
|
+
for (const channel of PAINT_CHANNELS) {
|
|
952
|
+
const layerId = bundle.layerIds.get(channel);
|
|
953
|
+
if (layerId && !desiredChannels.has(channel)) {
|
|
954
|
+
if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
955
|
+
bundle.layerIds.delete(channel);
|
|
956
|
+
changed = true;
|
|
957
|
+
} else if (!layerId && desiredChannels.has(channel)) {
|
|
958
|
+
const nextLayerId = `${bundle.sourceId}-${channel}`;
|
|
959
|
+
this.map.addLayer(this.buildGroupLayer(nextLayerId, bundle.sourceId, channel, handle.style), handle.mlLayerIds[0]);
|
|
960
|
+
bundle.layerIds.set(channel, nextLayerId);
|
|
961
|
+
changed = true;
|
|
381
962
|
}
|
|
382
|
-
}
|
|
383
|
-
|
|
963
|
+
}
|
|
964
|
+
return changed;
|
|
965
|
+
}
|
|
966
|
+
buildGroupLayer(id, sourceId, channel, style) {
|
|
967
|
+
switch (channel) {
|
|
968
|
+
case "fill": return {
|
|
969
|
+
id,
|
|
970
|
+
type: "fill",
|
|
971
|
+
source: sourceId,
|
|
972
|
+
filter: buildFillFilter(false),
|
|
973
|
+
paint: buildFillPaint(style)
|
|
974
|
+
};
|
|
975
|
+
case "fill-pattern": return {
|
|
976
|
+
id,
|
|
977
|
+
type: "fill",
|
|
978
|
+
source: sourceId,
|
|
979
|
+
filter: buildFillFilter(true),
|
|
980
|
+
paint: FILL_PATTERN_PAINT
|
|
981
|
+
};
|
|
982
|
+
case "line": return {
|
|
983
|
+
id,
|
|
984
|
+
type: "line",
|
|
985
|
+
source: sourceId,
|
|
986
|
+
filter: [
|
|
987
|
+
"any",
|
|
988
|
+
[
|
|
989
|
+
"==",
|
|
990
|
+
"$type",
|
|
991
|
+
"LineString"
|
|
992
|
+
],
|
|
993
|
+
[
|
|
994
|
+
"==",
|
|
995
|
+
"$type",
|
|
996
|
+
"Polygon"
|
|
997
|
+
]
|
|
998
|
+
],
|
|
999
|
+
paint: buildLinePaint(style),
|
|
1000
|
+
layout: {
|
|
1001
|
+
"line-cap": "round",
|
|
1002
|
+
"line-join": "round"
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
case "circle": return {
|
|
1006
|
+
id,
|
|
1007
|
+
type: "circle",
|
|
1008
|
+
source: sourceId,
|
|
1009
|
+
filter: buildCirclePointFilter(style),
|
|
1010
|
+
paint: buildCirclePaint(style)
|
|
1011
|
+
};
|
|
1012
|
+
case "icon": return buildIconLayer(id, sourceId);
|
|
1013
|
+
case "text":
|
|
1014
|
+
if (!style?.text) throw new Error("Cannot create a MapLibre text channel without text style");
|
|
1015
|
+
return buildSymbolLayer(id, sourceId, style.text);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
updateGroupBundleSource(bundle) {
|
|
1019
|
+
const source = this.map.getSource(bundle.sourceId);
|
|
1020
|
+
if (!source) return;
|
|
1021
|
+
const features = [...bundle.requiredIconIds].every((id) => this.map.hasImage(id)) ? bundle.features.map((feature) => this.promoteFeature(feature)) : [];
|
|
1022
|
+
const result = bundle.sourceInitialized && typeof source.updateData === "function" ? source.updateData(buildGeoJSONSourceDiff(bundle.sourceFeatures, features), true) : source.setData({
|
|
384
1023
|
type: "FeatureCollection",
|
|
385
1024
|
features
|
|
386
|
-
});
|
|
1025
|
+
}, true);
|
|
1026
|
+
bundle.sourceFeatures = features;
|
|
1027
|
+
bundle.sourceInitialized = true;
|
|
1028
|
+
if (isPromiseLike(result)) Promise.resolve(result).catch(() => {});
|
|
1029
|
+
}
|
|
1030
|
+
orderGroupBundles(handle, previousGroupIds, desiredGroupIds, forcedGroupIds) {
|
|
1031
|
+
let beforeId = handle.mlLayerIds[0];
|
|
1032
|
+
if (!beforeId) return;
|
|
1033
|
+
const stableGroupIds = sameOrder(previousGroupIds, desiredGroupIds) ? null : longestStableGroupIds(previousGroupIds, desiredGroupIds);
|
|
1034
|
+
for (let index = desiredGroupIds.length - 1; index >= 0; index--) {
|
|
1035
|
+
const groupId = desiredGroupIds[index];
|
|
1036
|
+
const bundle = handle.groupBundles.get(groupId);
|
|
1037
|
+
if (!bundle) continue;
|
|
1038
|
+
if (forcedGroupIds.has(groupId) || stableGroupIds !== null && !stableGroupIds.has(groupId)) for (const channel of PAINT_CHANNELS) {
|
|
1039
|
+
const layerId = bundle.layerIds.get(channel);
|
|
1040
|
+
if (layerId && this.map.getLayer(layerId)) this.map.moveLayer(layerId, beforeId);
|
|
1041
|
+
}
|
|
1042
|
+
beforeId = firstBundleLayerId(bundle) ?? beforeId;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
nativeLayerIds(handle) {
|
|
1046
|
+
return [...handle.mlLayerIds, ...[...handle.groupBundles.values()].flatMap((bundle) => [...bundle.layerIds.values()])];
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
|
|
1050
|
+
* and picks can key on the caller's feature id) and, for a hatch-patterned
|
|
1051
|
+
* Polygon, `_fillPatternImageId` (registering the tile image on demand). At
|
|
1052
|
+
* most one object spread — a feature needing neither passes through
|
|
1053
|
+
* untouched.
|
|
1054
|
+
*/
|
|
1055
|
+
promoteFeature(feature) {
|
|
1056
|
+
const extra = {};
|
|
1057
|
+
if (feature.id !== void 0) extra._fid = feature.id;
|
|
1058
|
+
const imageId = this.resolveFillPatternImage(feature);
|
|
1059
|
+
if (imageId !== void 0) extra[FILL_PATTERN_IMAGE_ID_PROPERTY] = imageId;
|
|
1060
|
+
if (Object.keys(extra).length === 0) return feature;
|
|
1061
|
+
return {
|
|
1062
|
+
...feature,
|
|
1063
|
+
properties: {
|
|
1064
|
+
...feature.properties ?? {},
|
|
1065
|
+
...extra
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Resolve a hatch-patterned Polygon's `style.fillPattern`/`fillColor` to its
|
|
1071
|
+
* hatch-tile image id, registering the image on demand. Returns `undefined`
|
|
1072
|
+
* for features without a hatch pattern, so `fillPatternImageId` stays the
|
|
1073
|
+
* single source of truth for the id baked into `_fillPatternImageId`.
|
|
1074
|
+
*/
|
|
1075
|
+
resolveFillPatternImage(feature) {
|
|
1076
|
+
const props = feature.properties;
|
|
1077
|
+
const pattern = props?.style?.fillPattern;
|
|
1078
|
+
if (!isHatchPattern(pattern)) return void 0;
|
|
1079
|
+
const color = props?.fillColor ?? props?.style?.fillColor ?? "rgba(0,0,0,0)";
|
|
1080
|
+
const imageId = fillPatternImageId(color, pattern);
|
|
1081
|
+
this.ensureFillPatternImage(imageId, color, pattern);
|
|
1082
|
+
return imageId;
|
|
1083
|
+
}
|
|
1084
|
+
/** Register a hatch-tile image once per (color, pattern) pair, keyed by `fillPatternImageId`. */
|
|
1085
|
+
ensureFillPatternImage(imageId, color, pattern) {
|
|
1086
|
+
if (this.map.hasImage(imageId)) return;
|
|
1087
|
+
const image = makeFillPatternImage(color, pattern, 2);
|
|
1088
|
+
this.map.addImage(imageId, image, { pixelRatio: 2 });
|
|
387
1089
|
}
|
|
388
1090
|
applyIconResources(resources) {
|
|
389
1091
|
if (typeof Image === "undefined") return;
|
|
@@ -395,6 +1097,7 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
395
1097
|
if (!this.pendingIconImages.has(resource.id)) return;
|
|
396
1098
|
this.pendingIconImages.delete(resource.id);
|
|
397
1099
|
if (!this.map.hasImage(resource.id)) this.map.addImage(resource.id, image);
|
|
1100
|
+
for (const handle of this.layers.values()) for (const bundle of handle.groupBundles.values()) if (bundle.requiredIconIds.has(resource.id)) this.updateGroupBundleSource(bundle);
|
|
398
1101
|
};
|
|
399
1102
|
image.onerror = () => this.pendingIconImages.delete(resource.id);
|
|
400
1103
|
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(centeredSvg(resource))}`;
|
|
@@ -420,17 +1123,19 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
420
1123
|
}
|
|
421
1124
|
attachViewChange(handler) {
|
|
422
1125
|
const fn = () => handler();
|
|
423
|
-
this.map.on(
|
|
424
|
-
this.map.on("rotate", fn);
|
|
425
|
-
this.map.on("pitch", fn);
|
|
426
|
-
this.map.on("moveend", fn);
|
|
1126
|
+
for (const event of this.viewChangeEvents()) this.map.on(event, fn);
|
|
427
1127
|
return { fn };
|
|
428
1128
|
}
|
|
429
1129
|
detachViewChange(token) {
|
|
430
|
-
this.map.off(
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.
|
|
1130
|
+
for (const event of this.viewChangeEvents()) this.map.off(event, token.fn);
|
|
1131
|
+
}
|
|
1132
|
+
viewChangeEvents() {
|
|
1133
|
+
return this.viewChangeMode === "settle" ? ["moveend"] : [
|
|
1134
|
+
"zoom",
|
|
1135
|
+
"rotate",
|
|
1136
|
+
"pitch",
|
|
1137
|
+
"moveend"
|
|
1138
|
+
];
|
|
434
1139
|
}
|
|
435
1140
|
createEditPointerDriver() {
|
|
436
1141
|
const map = this.map;
|
|
@@ -550,40 +1255,75 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
550
1255
|
setLayerStyle(layerId, style) {
|
|
551
1256
|
const handle = this.layers.get(layerId);
|
|
552
1257
|
if (!handle) return;
|
|
553
|
-
|
|
1258
|
+
const resolvedStyle = {
|
|
1259
|
+
...handle.style,
|
|
1260
|
+
...style,
|
|
1261
|
+
...style.text ? { text: {
|
|
1262
|
+
...handle.style?.text,
|
|
1263
|
+
...style.text
|
|
1264
|
+
} } : {}
|
|
1265
|
+
};
|
|
1266
|
+
handle.style = resolvedStyle;
|
|
554
1267
|
for (const mlId of handle.mlLayerIds) {
|
|
555
|
-
const
|
|
556
|
-
if (
|
|
557
|
-
const type = layer.type;
|
|
558
|
-
if (type === "fill") this.map.setPaintProperty(mlId, "fill-color", buildFillPaint(style)["fill-color"]);
|
|
559
|
-
else if (type === "line") {
|
|
560
|
-
if (style.strokeColor !== void 0 || style.strokeWidth !== void 0) {
|
|
561
|
-
const paint = buildLinePaint(style);
|
|
562
|
-
if (style.strokeColor !== void 0) this.map.setPaintProperty(mlId, "line-color", paint["line-color"]);
|
|
563
|
-
if (style.strokeWidth !== void 0) this.map.setPaintProperty(mlId, "line-width", paint["line-width"]);
|
|
564
|
-
}
|
|
565
|
-
if (style.strokeDash) this.map.setPaintProperty(mlId, "line-dasharray", style.strokeDash);
|
|
566
|
-
} else if (type === "circle") {
|
|
567
|
-
const paint = buildCirclePaint(style);
|
|
568
|
-
this.map.setPaintProperty(mlId, "circle-radius", paint["circle-radius"]);
|
|
569
|
-
this.map.setPaintProperty(mlId, "circle-color", paint["circle-color"]);
|
|
570
|
-
this.map.setPaintProperty(mlId, "circle-stroke-color", paint["circle-stroke-color"]);
|
|
571
|
-
this.map.setPaintProperty(mlId, "circle-stroke-width", paint["circle-stroke-width"]);
|
|
572
|
-
this.map.setFilter(mlId, buildCirclePointFilter(style));
|
|
573
|
-
} else if (type === "symbol" && style.text) {
|
|
574
|
-
if (style.text.text) this.map.setLayoutProperty(mlId, "text-field", style.text.text);
|
|
575
|
-
else if (style.text.labelProperty) this.map.setLayoutProperty(mlId, "text-field", ["get", style.text.labelProperty]);
|
|
576
|
-
if (style.text.fillColor) this.map.setPaintProperty(mlId, "text-color", style.text.fillColor);
|
|
577
|
-
if (style.text.strokeColor) this.map.setPaintProperty(mlId, "text-halo-color", style.text.strokeColor);
|
|
578
|
-
if (style.text.strokeWidth !== void 0) this.map.setPaintProperty(mlId, "text-halo-width", style.text.strokeWidth);
|
|
579
|
-
}
|
|
1268
|
+
const channel = this.legacyPaintChannel(mlId);
|
|
1269
|
+
if (channel) this.applyChannelStyle(mlId, channel, style, resolvedStyle);
|
|
580
1270
|
}
|
|
1271
|
+
const bundlesNeedingOrder = /* @__PURE__ */ new Set();
|
|
1272
|
+
for (const [groupId, bundle] of handle.groupBundles) if (this.reconcileGroupChannels(handle, bundle, groupPaintChannels(bundle.features, resolvedStyle))) bundlesNeedingOrder.add(groupId);
|
|
1273
|
+
this.orderGroupBundles(handle, handle.groupOrder, handle.groupOrder, bundlesNeedingOrder);
|
|
1274
|
+
for (const bundle of handle.groupBundles.values()) for (const [channel, mlId] of bundle.layerIds) this.applyChannelStyle(mlId, channel, style, resolvedStyle);
|
|
581
1275
|
if (style.text && !handle.mlLayerIds.some((id) => id.endsWith("-symbol"))) {
|
|
582
1276
|
const symbolId = `${layerId}-symbol`;
|
|
583
|
-
this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId,
|
|
1277
|
+
this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId, resolvedStyle.text));
|
|
584
1278
|
handle.mlLayerIds.push(symbolId);
|
|
585
1279
|
}
|
|
586
1280
|
}
|
|
1281
|
+
legacyPaintChannel(layerId) {
|
|
1282
|
+
if (layerId.endsWith("-fill-pattern")) return "fill-pattern";
|
|
1283
|
+
if (layerId.endsWith("-fill")) return "fill";
|
|
1284
|
+
if (layerId.endsWith("-line")) return "line";
|
|
1285
|
+
if (layerId.endsWith("-circle")) return "circle";
|
|
1286
|
+
if (layerId.endsWith("-icon")) return "icon";
|
|
1287
|
+
if (layerId.endsWith("-symbol")) return "text";
|
|
1288
|
+
}
|
|
1289
|
+
applyChannelStyle(layerId, channel, styleUpdate, resolvedStyle) {
|
|
1290
|
+
if (!this.map.getLayer(layerId)) return;
|
|
1291
|
+
switch (channel) {
|
|
1292
|
+
case "fill":
|
|
1293
|
+
if (styleUpdate.fillColor === void 0) break;
|
|
1294
|
+
this.map.setPaintProperty(layerId, "fill-color", buildFillPaint(resolvedStyle)["fill-color"]);
|
|
1295
|
+
break;
|
|
1296
|
+
case "fill-pattern": break;
|
|
1297
|
+
case "line": {
|
|
1298
|
+
const paint = buildLinePaint(resolvedStyle);
|
|
1299
|
+
if (styleUpdate.strokeColor !== void 0) this.map.setPaintProperty(layerId, "line-color", paint["line-color"]);
|
|
1300
|
+
if (styleUpdate.strokeWidth !== void 0) this.map.setPaintProperty(layerId, "line-width", paint["line-width"]);
|
|
1301
|
+
if (styleUpdate.strokeDash !== void 0) this.map.setPaintProperty(layerId, "line-dasharray", paint["line-dasharray"]);
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
case "circle": {
|
|
1305
|
+
if (!(styleUpdate.pointRadius !== void 0 || styleUpdate.pointFillColor !== void 0 || styleUpdate.fillColor !== void 0 || styleUpdate.strokeColor !== void 0 || styleUpdate.strokeWidth !== void 0 || styleUpdate.text !== void 0)) break;
|
|
1306
|
+
const paint = buildCirclePaint(resolvedStyle);
|
|
1307
|
+
this.map.setPaintProperty(layerId, "circle-radius", paint["circle-radius"]);
|
|
1308
|
+
this.map.setPaintProperty(layerId, "circle-color", paint["circle-color"]);
|
|
1309
|
+
this.map.setPaintProperty(layerId, "circle-stroke-color", paint["circle-stroke-color"]);
|
|
1310
|
+
this.map.setPaintProperty(layerId, "circle-stroke-width", paint["circle-stroke-width"]);
|
|
1311
|
+
this.map.setFilter(layerId, buildCirclePointFilter(resolvedStyle));
|
|
1312
|
+
break;
|
|
1313
|
+
}
|
|
1314
|
+
case "icon": break;
|
|
1315
|
+
case "text":
|
|
1316
|
+
if (!styleUpdate.text || !resolvedStyle.text) break;
|
|
1317
|
+
if (styleUpdate.text.text || styleUpdate.text.labelProperty) this.map.setLayoutProperty(layerId, "text-field", buildTextFieldExpression(resolvedStyle.text));
|
|
1318
|
+
if (styleUpdate.text.fillColor) this.map.setPaintProperty(layerId, "text-color", resolvedStyle.text.fillColor);
|
|
1319
|
+
if (styleUpdate.text.strokeColor) this.map.setPaintProperty(layerId, "text-halo-color", buildTextHaloColorExpression(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
|
+
}
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
587
1327
|
toLonLat(coordinate) {
|
|
588
1328
|
return unproject(coordinate[0], coordinate[1]);
|
|
589
1329
|
}
|
|
@@ -594,6 +1334,10 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
594
1334
|
const point = this.map.project([this.toContinuousLng(position[0]), position[1]]);
|
|
595
1335
|
return [point.x, point.y];
|
|
596
1336
|
}
|
|
1337
|
+
getCoordinateFromPixel(pixel) {
|
|
1338
|
+
const coordinate = this.map.unproject(pixel);
|
|
1339
|
+
return [this.toContinuousLng(coordinate.lng), coordinate.lat];
|
|
1340
|
+
}
|
|
597
1341
|
getViewportSize() {
|
|
598
1342
|
const container = this.map.getContainer();
|
|
599
1343
|
return {
|
|
@@ -639,20 +1383,48 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
639
1383
|
const tol = pickHitTolerance(originalEvent);
|
|
640
1384
|
const { x, y } = e.point;
|
|
641
1385
|
const bbox = [[x - tol, y - tol], [x + tol, y + tol]];
|
|
642
|
-
const
|
|
643
|
-
if (features.length === 0) return;
|
|
644
|
-
const f = features[0];
|
|
645
|
-
const geojson = {
|
|
1386
|
+
const relevantFeatures = mergeNativeAndCachedPickFeatures(this.map.queryRenderedFeatures(bbox, { layers: this.nativeLayerIds(handle) }).map((feature) => ({
|
|
646
1387
|
type: "Feature",
|
|
647
|
-
id:
|
|
648
|
-
geometry:
|
|
649
|
-
properties:
|
|
650
|
-
};
|
|
651
|
-
const
|
|
1388
|
+
id: feature.id,
|
|
1389
|
+
geometry: feature.geometry,
|
|
1390
|
+
properties: feature.properties
|
|
1391
|
+
})), this.featureCache.get(layerId) ?? []);
|
|
1392
|
+
const size = this.getViewportSize();
|
|
1393
|
+
const candidate = evaluatePickCandidates(relevantFeatures.map(({ feature, nativeHit, visualOrder }) => {
|
|
1394
|
+
const iconId = feature.properties?.iconId;
|
|
1395
|
+
const icon = typeof iconId === "string" ? this.getIconResource(iconId) : void 0;
|
|
1396
|
+
const featureGroupOrder = this.getFeatureGroupOrder(layerId, feature.id);
|
|
1397
|
+
return {
|
|
1398
|
+
feature,
|
|
1399
|
+
visualOrder,
|
|
1400
|
+
nativeHit,
|
|
1401
|
+
...featureGroupOrder !== void 0 ? { featureGroupOrder } : {},
|
|
1402
|
+
...icon ? { icon } : {},
|
|
1403
|
+
layerStyle: {
|
|
1404
|
+
...handle.style,
|
|
1405
|
+
strokeColor: handle.style?.strokeColor ?? DEFAULT_LINE_COLOR,
|
|
1406
|
+
strokeWidth: handle.style?.strokeWidth ?? DEFAULT_LINE_WIDTH
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
}), {
|
|
1410
|
+
pixel: [x, y],
|
|
1411
|
+
pointerType: pickPointerType(originalEvent),
|
|
1412
|
+
tolerance: tol,
|
|
1413
|
+
toPixel: (position) => this.getPixelFromCoordinate(position),
|
|
1414
|
+
viewport: {
|
|
1415
|
+
minX: 0,
|
|
1416
|
+
minY: 0,
|
|
1417
|
+
maxX: size.width,
|
|
1418
|
+
maxY: size.height
|
|
1419
|
+
},
|
|
1420
|
+
bearing: this.getBearing()
|
|
1421
|
+
});
|
|
1422
|
+
if (!candidate) return;
|
|
1423
|
+
const cmId = candidate.graphicId;
|
|
652
1424
|
if (cmId == null) return;
|
|
653
1425
|
handler({
|
|
654
1426
|
id: cmId,
|
|
655
|
-
feature:
|
|
1427
|
+
feature: candidate.feature,
|
|
656
1428
|
pixel: [x, y],
|
|
657
1429
|
coordinate: [this.toContinuousLng(e.lngLat.lng), e.lngLat.lat],
|
|
658
1430
|
originalEvent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-draw-adapter-maplibre",
|
|
3
|
-
"version": "0.2.0
|
|
3
|
+
"version": "0.2.0",
|
|
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,17 +37,18 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@orbat-mapper/control-measures": "^0.2.0
|
|
41
|
-
"@orbat-mapper/tactical-draw": "^0.2.0
|
|
40
|
+
"@orbat-mapper/control-measures": "^0.2.0",
|
|
41
|
+
"@orbat-mapper/tactical-draw": "^0.2.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"maplibre-gl": ">=
|
|
44
|
+
"maplibre-gl": ">=5.8.0 <6.0.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@arethetypeswrong/core": "^0.18.3",
|
|
48
|
+
"@maplibre/maplibre-gl-style-spec": "^24.10.0",
|
|
48
49
|
"@types/geojson": "^7946.0.16",
|
|
49
50
|
"jsdom": "^27.4.0",
|
|
50
|
-
"maplibre-gl": "^5.
|
|
51
|
+
"maplibre-gl": "^5.24.0",
|
|
51
52
|
"publint": "^0.3.21",
|
|
52
53
|
"tsdown": "^0.22.2",
|
|
53
54
|
"typescript": "~5.9.3",
|