@orbat-mapper/tactical-draw-adapter-maplibre 0.2.0-alpha.2 → 0.2.0-alpha.20
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 +60 -5
- package/dist/index.mjs +886 -109
- package/package.json +6 -5
package/dist/index.d.mts
CHANGED
|
@@ -1,34 +1,88 @@
|
|
|
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
|
+
featureSignature: string;
|
|
21
|
+
requiredIconIds: ReadonlySet<string>;
|
|
22
|
+
}
|
|
12
23
|
type MapLibreEventToken = (e: any) => void;
|
|
13
24
|
interface MapLibreViewToken {
|
|
14
25
|
fn: () => void;
|
|
15
26
|
}
|
|
27
|
+
interface MapLibreAdapterOptions {
|
|
28
|
+
/**
|
|
29
|
+
* How view changes propagate to view-state refits. "continuous" (default)
|
|
30
|
+
* refits on every frame of an animated zoom/rotate/pitch; "settle" defers
|
|
31
|
+
* the refit until the animation ends.
|
|
32
|
+
*/
|
|
33
|
+
viewChangeMode?: "continuous" | "settle";
|
|
34
|
+
}
|
|
16
35
|
declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLibreEventToken, MapLibreViewToken> {
|
|
17
36
|
private map;
|
|
18
|
-
|
|
37
|
+
private readonly viewChangeMode;
|
|
38
|
+
private readonly pendingIconImages;
|
|
39
|
+
readonly scalesGroundPointSymbolsNatively = true;
|
|
40
|
+
constructor(map: maplibregl.Map, options?: MapLibreAdapterOptions);
|
|
19
41
|
private toContinuousLng;
|
|
20
42
|
protected createLayer(layerId: LayerId, style?: StyleOptions): MapLibreLayerHandle;
|
|
21
43
|
protected destroyLayer(handle: MapLibreLayerHandle): void;
|
|
22
|
-
protected
|
|
44
|
+
protected applyFeatureGroups(layerId: LayerId, groups: readonly FeatureGroup[]): void;
|
|
45
|
+
private createGroupBundle;
|
|
46
|
+
private destroyGroupBundle;
|
|
47
|
+
private clearGroupBundles;
|
|
48
|
+
private requiredIconIds;
|
|
49
|
+
private reconcileGroupChannels;
|
|
50
|
+
private buildGroupLayer;
|
|
51
|
+
private updateGroupBundleSource;
|
|
52
|
+
private orderGroupBundles;
|
|
53
|
+
private nativeLayerIds;
|
|
54
|
+
/**
|
|
55
|
+
* Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
|
|
56
|
+
* and picks can key on the caller's feature id) and, for a hatch-patterned
|
|
57
|
+
* Polygon, `_fillPatternImageId` (registering the tile image on demand). At
|
|
58
|
+
* most one object spread — a feature needing neither passes through
|
|
59
|
+
* untouched.
|
|
60
|
+
*/
|
|
61
|
+
private promoteFeature;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a hatch-patterned Polygon's `style.fillPattern`/`fillColor` to its
|
|
64
|
+
* hatch-tile image id, registering the image on demand. Returns `undefined`
|
|
65
|
+
* for features without a hatch pattern, so `fillPatternImageId` stays the
|
|
66
|
+
* single source of truth for the id baked into `_fillPatternImageId`.
|
|
67
|
+
*/
|
|
68
|
+
private resolveFillPatternImage;
|
|
69
|
+
/** Register a hatch-tile image once per (color, pattern) pair, keyed by `fillPatternImageId`. */
|
|
70
|
+
private ensureFillPatternImage;
|
|
71
|
+
protected applyIconResources(resources: readonly IconResource[]): void;
|
|
72
|
+
protected removeIconResource(id: string): void;
|
|
23
73
|
protected attachNativeEvent(type: "click" | "pointermove" | "dblclick", onEvent: (lonLat: Position, pixel: PixelCoordinate, originalEvent: unknown) => void): NativeEventEntry<MapLibreEventToken>;
|
|
24
74
|
protected detachNativeEvent(entry: NativeEventEntry<MapLibreEventToken>): void;
|
|
25
75
|
protected attachViewChange(handler: () => void): MapLibreViewToken;
|
|
26
76
|
protected detachViewChange(token: MapLibreViewToken): void;
|
|
77
|
+
private viewChangeEvents;
|
|
27
78
|
createEditPointerDriver(): EditPointerDriver;
|
|
28
79
|
setLayerStyle(layerId: LayerId, style: StyleOptions): void;
|
|
80
|
+
private legacyPaintChannel;
|
|
81
|
+
private applyChannelStyle;
|
|
29
82
|
toLonLat(coordinate: number[]): Position;
|
|
30
83
|
fromLonLat(position: Position): number[];
|
|
31
84
|
getPixelFromCoordinate(position: Position): PixelCoordinate | null;
|
|
85
|
+
getCoordinateFromPixel(pixel: PixelCoordinate): Position | null;
|
|
32
86
|
getViewportSize(): {
|
|
33
87
|
width: number;
|
|
34
88
|
height: number;
|
|
@@ -40,8 +94,9 @@ declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLib
|
|
|
40
94
|
setDoubleClickZoomEnabled(enabled: boolean): void;
|
|
41
95
|
getResolution(): number | undefined;
|
|
42
96
|
getZoom(): number | undefined;
|
|
97
|
+
getBearing(): number;
|
|
43
98
|
onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
|
|
44
99
|
setGlobeMode(enabled: boolean): void;
|
|
45
100
|
}
|
|
46
101
|
//#endregion
|
|
47
|
-
export { MapLibreAdapter };
|
|
102
|
+
export { MapLibreAdapter, type MapLibreAdapterOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
import { project, unproject } from "@orbat-mapper/control-measures";
|
|
2
|
-
import { BaseMapAdapter, 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
|
+
/**
|
|
26
|
+
* Find the largest subset of bundles whose relative order already matches the
|
|
27
|
+
* desired stack. Reorders then move only the remaining bundles.
|
|
28
|
+
*/
|
|
29
|
+
function longestStableGroupIds(previousGroupIds, desiredGroupIds) {
|
|
30
|
+
const previousIndices = new Map(previousGroupIds.map((groupId, index) => [groupId, index]));
|
|
31
|
+
const sequence = desiredGroupIds.flatMap((groupId) => {
|
|
32
|
+
const previousIndex = previousIndices.get(groupId);
|
|
33
|
+
return previousIndex === void 0 ? [] : [{
|
|
34
|
+
groupId,
|
|
35
|
+
previousIndex
|
|
36
|
+
}];
|
|
37
|
+
});
|
|
38
|
+
const tails = [];
|
|
39
|
+
const tailSequenceIndices = [];
|
|
40
|
+
const predecessors = new Array(sequence.length).fill(-1);
|
|
41
|
+
for (let sequenceIndex = 0; sequenceIndex < sequence.length; sequenceIndex++) {
|
|
42
|
+
const previousIndex = sequence[sequenceIndex].previousIndex;
|
|
43
|
+
let low = 0;
|
|
44
|
+
let high = tails.length;
|
|
45
|
+
while (low < high) {
|
|
46
|
+
const middle = low + high >>> 1;
|
|
47
|
+
if (tails[middle] < previousIndex) low = middle + 1;
|
|
48
|
+
else high = middle;
|
|
49
|
+
}
|
|
50
|
+
if (low > 0) predecessors[sequenceIndex] = tailSequenceIndices[low - 1];
|
|
51
|
+
tails[low] = previousIndex;
|
|
52
|
+
tailSequenceIndices[low] = sequenceIndex;
|
|
53
|
+
}
|
|
54
|
+
const stableGroupIds = /* @__PURE__ */ new Set();
|
|
55
|
+
let sequenceIndex = tailSequenceIndices.at(-1);
|
|
56
|
+
while (sequenceIndex !== void 0 && sequenceIndex >= 0) {
|
|
57
|
+
stableGroupIds.add(sequence[sequenceIndex].groupId);
|
|
58
|
+
sequenceIndex = predecessors[sequenceIndex];
|
|
59
|
+
}
|
|
60
|
+
return stableGroupIds;
|
|
61
|
+
}
|
|
62
|
+
function isPromiseLike(value) {
|
|
63
|
+
return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
|
|
64
|
+
}
|
|
4
65
|
function emptyFC() {
|
|
5
66
|
return {
|
|
6
67
|
type: "FeatureCollection",
|
|
@@ -52,51 +113,218 @@ function buildCirclePointFilter(style) {
|
|
|
52
113
|
"$type",
|
|
53
114
|
"Point"
|
|
54
115
|
],
|
|
116
|
+
["!has", "iconId"],
|
|
55
117
|
["!has", style.text.labelProperty]
|
|
56
118
|
] : [
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
119
|
+
"all",
|
|
120
|
+
[
|
|
121
|
+
"==",
|
|
122
|
+
"$type",
|
|
123
|
+
"Point"
|
|
124
|
+
],
|
|
125
|
+
["!has", "iconId"]
|
|
60
126
|
];
|
|
61
127
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
128
|
+
function buildIconLayer(id, source) {
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
type: "symbol",
|
|
132
|
+
source,
|
|
133
|
+
filter: [
|
|
134
|
+
"all",
|
|
135
|
+
[
|
|
136
|
+
"==",
|
|
137
|
+
"$type",
|
|
138
|
+
"Point"
|
|
139
|
+
],
|
|
140
|
+
["has", "iconId"]
|
|
141
|
+
],
|
|
142
|
+
layout: {
|
|
143
|
+
"icon-image": ["get", "iconId"],
|
|
144
|
+
"icon-size": buildIconSizeExpression(),
|
|
145
|
+
"icon-rotate": [
|
|
146
|
+
"*",
|
|
147
|
+
[
|
|
148
|
+
"coalesce",
|
|
149
|
+
["get", "iconRotation"],
|
|
150
|
+
0
|
|
151
|
+
],
|
|
152
|
+
180 / Math.PI
|
|
153
|
+
],
|
|
154
|
+
"icon-rotation-alignment": "viewport",
|
|
155
|
+
"icon-pitch-alignment": "viewport",
|
|
156
|
+
"icon-allow-overlap": true,
|
|
157
|
+
"icon-ignore-placement": true,
|
|
158
|
+
"symbol-z-order": "source"
|
|
159
|
+
},
|
|
160
|
+
paint: { "icon-opacity": [
|
|
161
|
+
"coalesce",
|
|
162
|
+
["get", "iconOpacity"],
|
|
163
|
+
1
|
|
164
|
+
] }
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function centeredSvg(resource) {
|
|
168
|
+
const halfWidth = Math.max(resource.anchor[0], resource.width - resource.anchor[0]);
|
|
169
|
+
const halfHeight = Math.max(resource.anchor[1], resource.height - resource.anchor[1]);
|
|
170
|
+
const width = halfWidth * 2;
|
|
171
|
+
const height = halfHeight * 2;
|
|
172
|
+
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>`;
|
|
173
|
+
}
|
|
174
|
+
/** Web Mercator world circumference in meters (2π × 6378137). */
|
|
175
|
+
const WEB_MERCATOR_CIRCUMFERENCE = 40075016.686;
|
|
176
|
+
/**
|
|
177
|
+
* Per-feature text size (ADR-0028): a ground-anchored label carries
|
|
178
|
+
* `textSizeMeters` and renders at `textSizeMeters / resolution`, clamped to
|
|
179
|
+
* the layer's `textSizeMinPixels`/`textSizeMaxPixels` band; a screen-anchored
|
|
180
|
+
* one carries `textSizePixels` and renders at that constant; neither renders
|
|
181
|
+
* the 14px engine default. Layout properties only take zoom as an
|
|
182
|
+
* `interpolate`/`step` input, so the ground conversion is evaluated at every
|
|
183
|
+
* integer zoom stop (resolution = world circumference / 512·2^zoom pixels,
|
|
184
|
+
* matching `getResolution`); the linear ramp between stops approximates the
|
|
185
|
+
* exponential exactly closely enough at 1-zoom granularity.
|
|
186
|
+
*/
|
|
187
|
+
function buildTextSizeExpression(text) {
|
|
188
|
+
const minPx = text.textSizeMinPixels ?? DEFAULT_TEXT_SIZE_MIN_PIXELS;
|
|
189
|
+
const maxPx = text.textSizeMaxPixels ?? DEFAULT_TEXT_SIZE_MAX_PIXELS;
|
|
190
|
+
const screenOrDefault = [
|
|
64
191
|
"coalesce",
|
|
65
192
|
["get", "textSizePixels"],
|
|
66
193
|
14
|
|
67
194
|
];
|
|
68
|
-
const
|
|
195
|
+
const sizeAtZoom = (zoom) => {
|
|
196
|
+
const groundPx = [
|
|
197
|
+
"*",
|
|
198
|
+
["get", "textSizeMeters"],
|
|
199
|
+
Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
|
|
200
|
+
];
|
|
201
|
+
return [
|
|
202
|
+
"case",
|
|
203
|
+
["has", "textSizeMeters"],
|
|
204
|
+
[
|
|
205
|
+
"case",
|
|
206
|
+
["has", "textMaxSizePixels"],
|
|
207
|
+
[
|
|
208
|
+
"min",
|
|
209
|
+
["get", "textMaxSizePixels"],
|
|
210
|
+
groundPx
|
|
211
|
+
],
|
|
212
|
+
[
|
|
213
|
+
"min",
|
|
214
|
+
maxPx,
|
|
215
|
+
[
|
|
216
|
+
"max",
|
|
217
|
+
minPx,
|
|
218
|
+
groundPx
|
|
219
|
+
]
|
|
220
|
+
]
|
|
221
|
+
],
|
|
222
|
+
screenOrDefault
|
|
223
|
+
];
|
|
224
|
+
};
|
|
225
|
+
return [
|
|
226
|
+
"interpolate",
|
|
227
|
+
["linear"],
|
|
228
|
+
["zoom"],
|
|
229
|
+
...Array.from({ length: 25 }, (_, zoom) => [zoom, sizeAtZoom(zoom)]).flat()
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Companion to `buildTextSizeExpression` (ADR-0037): drives `text-opacity` to
|
|
234
|
+
* 0 once a ground-anchored Text graphic's derived on-screen size exceeds its
|
|
235
|
+
* `textMaxSizePixels` threshold, 1 otherwise (including every feature without
|
|
236
|
+
* a threshold, e.g. amplifier labels). Layout properties like `text-size`
|
|
237
|
+
* cannot represent "hidden", so hiding is a paint-property concern; `step`
|
|
238
|
+
* (rather than `interpolate`) is enough since opacity only ever needs to
|
|
239
|
+
* flip at a stop, evaluated at the same per-zoom granularity as the size
|
|
240
|
+
* expression.
|
|
241
|
+
*/
|
|
242
|
+
const TEXT_OPACITY_EXPRESSION = (() => {
|
|
243
|
+
const opacityAtZoom = (zoom) => {
|
|
244
|
+
return [
|
|
245
|
+
"case",
|
|
246
|
+
[
|
|
247
|
+
"all",
|
|
248
|
+
["has", "textSizeMeters"],
|
|
249
|
+
["has", "textMaxSizePixels"]
|
|
250
|
+
],
|
|
251
|
+
[
|
|
252
|
+
"case",
|
|
253
|
+
[
|
|
254
|
+
">",
|
|
255
|
+
[
|
|
256
|
+
"*",
|
|
257
|
+
["get", "textSizeMeters"],
|
|
258
|
+
Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
|
|
259
|
+
],
|
|
260
|
+
["get", "textMaxSizePixels"]
|
|
261
|
+
],
|
|
262
|
+
0,
|
|
263
|
+
1
|
|
264
|
+
],
|
|
265
|
+
1
|
|
266
|
+
];
|
|
267
|
+
};
|
|
268
|
+
const stops = Array.from({ length: 25 }, (_, zoom) => zoom);
|
|
269
|
+
return [
|
|
270
|
+
"step",
|
|
271
|
+
["zoom"],
|
|
272
|
+
opacityAtZoom(0),
|
|
273
|
+
...stops.slice(1).flatMap((zoom) => [zoom, opacityAtZoom(zoom)])
|
|
274
|
+
];
|
|
275
|
+
})();
|
|
276
|
+
/**
|
|
277
|
+
* Per-feature icon size, same zoom-stop scheme as `buildTextSizeExpression`:
|
|
278
|
+
* a ground-sized point symbol carries `iconSizeMeters`/`iconOctagonSize` (and
|
|
279
|
+
* optional `iconMinPixels`/`iconMaxPixels`), so the meters→pixels clamp is
|
|
280
|
+
* evaluated inside the style layer at every integer zoom stop and the symbol
|
|
281
|
+
* scales smoothly through animated zooms. Anything else falls back to the
|
|
282
|
+
* host-computed `iconScale` snapshot.
|
|
283
|
+
*/
|
|
284
|
+
function buildIconSizeExpression() {
|
|
285
|
+
const minExpr = [
|
|
69
286
|
"coalesce",
|
|
70
|
-
["get", "
|
|
287
|
+
["get", "iconMinPixels"],
|
|
71
288
|
0
|
|
72
289
|
];
|
|
73
|
-
const
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
290
|
+
const maxExpr = [
|
|
291
|
+
"coalesce",
|
|
292
|
+
["get", "iconMaxPixels"],
|
|
293
|
+
1e9
|
|
294
|
+
];
|
|
295
|
+
const scaleAtZoom = (zoom) => {
|
|
296
|
+
return [
|
|
77
297
|
"case",
|
|
78
|
-
["has", "
|
|
298
|
+
["has", "iconSizeMeters"],
|
|
79
299
|
[
|
|
80
|
-
"
|
|
81
|
-
size,
|
|
300
|
+
"/",
|
|
82
301
|
[
|
|
83
|
-
"
|
|
84
|
-
|
|
302
|
+
"max",
|
|
303
|
+
minExpr,
|
|
85
304
|
[
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
|
|
305
|
+
"min",
|
|
306
|
+
maxExpr,
|
|
307
|
+
[
|
|
308
|
+
"*",
|
|
309
|
+
["get", "iconSizeMeters"],
|
|
310
|
+
Math.pow(2, zoom + 9) / WEB_MERCATOR_CIRCUMFERENCE
|
|
311
|
+
]
|
|
89
312
|
]
|
|
90
|
-
]
|
|
313
|
+
],
|
|
314
|
+
["get", "iconOctagonSize"]
|
|
91
315
|
],
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
316
|
+
[
|
|
317
|
+
"coalesce",
|
|
318
|
+
["get", "iconScale"],
|
|
319
|
+
1
|
|
320
|
+
]
|
|
321
|
+
];
|
|
322
|
+
};
|
|
95
323
|
return [
|
|
96
324
|
"interpolate",
|
|
97
325
|
["linear"],
|
|
98
326
|
["zoom"],
|
|
99
|
-
...Array.from({ length: 25 }, (_, zoom) => [zoom,
|
|
327
|
+
...Array.from({ length: 25 }, (_, zoom) => [zoom, scaleAtZoom(zoom)]).flat()
|
|
100
328
|
];
|
|
101
329
|
}
|
|
102
330
|
/**
|
|
@@ -104,6 +332,94 @@ function buildTextSizeExpression() {
|
|
|
104
332
|
* `properties.style.fillColor` overlay. Mirrors `buildCirclePaint`.
|
|
105
333
|
*/
|
|
106
334
|
/**
|
|
335
|
+
* Per-feature `text-field` value: the layer's literal text, or the feature's
|
|
336
|
+
* `labelProperty` value, uppercased when the feature's `textStyle` is
|
|
337
|
+
* `"caps"`. Factored out of `buildSymbolLayer` so `setLayerStyle`'s
|
|
338
|
+
* incremental `text-field` update (below) can reuse it instead of
|
|
339
|
+
* regressing to a caps-unaware plain `["get", ...]`.
|
|
340
|
+
*/
|
|
341
|
+
function buildTextFieldExpression(text) {
|
|
342
|
+
const rawTextField = text.labelProperty ? ["get", text.labelProperty] : text.text ?? "";
|
|
343
|
+
return text.labelProperty ? [
|
|
344
|
+
"case",
|
|
345
|
+
[
|
|
346
|
+
"==",
|
|
347
|
+
["get", "textStyle"],
|
|
348
|
+
"caps"
|
|
349
|
+
],
|
|
350
|
+
["upcase", rawTextField],
|
|
351
|
+
rawTextField
|
|
352
|
+
] : rawTextField;
|
|
353
|
+
}
|
|
354
|
+
const HAS_TEXT_HALO = [
|
|
355
|
+
"boolean",
|
|
356
|
+
["get", "textHalo"],
|
|
357
|
+
false
|
|
358
|
+
];
|
|
359
|
+
const HAS_TEXT_BACKGROUND = [
|
|
360
|
+
"boolean",
|
|
361
|
+
["get", "textBackground"],
|
|
362
|
+
false
|
|
363
|
+
];
|
|
364
|
+
/**
|
|
365
|
+
* Per-feature `text-halo-color` value: `textHalo` wins over both the layer's
|
|
366
|
+
* `text.strokeColor` and the `textBackground` halo, matching the OL/Leaflet
|
|
367
|
+
* adapters' precedence. Factored out so `setLayerStyle`'s incremental
|
|
368
|
+
* `text-halo-color` update (below) can reuse it instead of dropping the
|
|
369
|
+
* `textHalo` case.
|
|
370
|
+
*/
|
|
371
|
+
function buildTextHaloColorExpression(text) {
|
|
372
|
+
const fallback = text.strokeColor ? text.strokeColor : [
|
|
373
|
+
"case",
|
|
374
|
+
HAS_TEXT_BACKGROUND,
|
|
375
|
+
"#ffffff",
|
|
376
|
+
"rgba(255,255,255,0)"
|
|
377
|
+
];
|
|
378
|
+
return [
|
|
379
|
+
"case",
|
|
380
|
+
HAS_TEXT_HALO,
|
|
381
|
+
[
|
|
382
|
+
"coalesce",
|
|
383
|
+
["get", "textHaloColor"],
|
|
384
|
+
TEXT_HALO_COLOR
|
|
385
|
+
],
|
|
386
|
+
fallback
|
|
387
|
+
];
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Per-feature `text-halo-width` value, mirroring
|
|
391
|
+
* `buildTextHaloColorExpression`'s precedence. MapLibre's `text-halo-width`
|
|
392
|
+
* already extends outward from the glyph edge, matching
|
|
393
|
+
* `resolveTextHaloWidth`'s own outward-depth semantics, so the `textHalo`
|
|
394
|
+
* case uses its formula directly (unlike OpenLayers's straddling canvas
|
|
395
|
+
* stroke, which must double it).
|
|
396
|
+
*/
|
|
397
|
+
function buildTextHaloWidthExpression(text) {
|
|
398
|
+
return [
|
|
399
|
+
"case",
|
|
400
|
+
HAS_TEXT_HALO,
|
|
401
|
+
[
|
|
402
|
+
"max",
|
|
403
|
+
1.5,
|
|
404
|
+
[
|
|
405
|
+
"/",
|
|
406
|
+
[
|
|
407
|
+
"coalesce",
|
|
408
|
+
["get", "textSizePixels"],
|
|
409
|
+
24
|
|
410
|
+
],
|
|
411
|
+
8
|
|
412
|
+
]
|
|
413
|
+
],
|
|
414
|
+
text.strokeColor ? text.strokeWidth ?? 1 : [
|
|
415
|
+
"case",
|
|
416
|
+
HAS_TEXT_BACKGROUND,
|
|
417
|
+
3,
|
|
418
|
+
0
|
|
419
|
+
]
|
|
420
|
+
];
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
107
423
|
* Build the layout and paint objects for a symbol (text) layer.
|
|
108
424
|
* Centralises the rotation convention so `createLayer` and `setLayerStyle`
|
|
109
425
|
* stay in sync. Rotation follows the OpenLayers convention: `π − r` radians
|
|
@@ -136,25 +452,75 @@ function buildSymbolLayer(symbolId, sourceId, text) {
|
|
|
136
452
|
],
|
|
137
453
|
"#000"
|
|
138
454
|
];
|
|
455
|
+
const textAnchor = text.labelProperty ? [
|
|
456
|
+
"case",
|
|
457
|
+
[
|
|
458
|
+
"==",
|
|
459
|
+
["get", "textAnchor"],
|
|
460
|
+
"start"
|
|
461
|
+
],
|
|
462
|
+
"left",
|
|
463
|
+
[
|
|
464
|
+
"==",
|
|
465
|
+
["get", "textAnchor"],
|
|
466
|
+
"end"
|
|
467
|
+
],
|
|
468
|
+
"right",
|
|
469
|
+
"center"
|
|
470
|
+
] : "center";
|
|
471
|
+
const textJustify = text.labelProperty ? [
|
|
472
|
+
"case",
|
|
473
|
+
[
|
|
474
|
+
"==",
|
|
475
|
+
["get", "textJustify"],
|
|
476
|
+
"left"
|
|
477
|
+
],
|
|
478
|
+
"left",
|
|
479
|
+
[
|
|
480
|
+
"==",
|
|
481
|
+
["get", "textJustify"],
|
|
482
|
+
"right"
|
|
483
|
+
],
|
|
484
|
+
"right",
|
|
485
|
+
"center"
|
|
486
|
+
] : "center";
|
|
139
487
|
return {
|
|
140
488
|
id: symbolId,
|
|
141
489
|
type: "symbol",
|
|
142
490
|
source: sourceId,
|
|
143
491
|
layout: {
|
|
144
|
-
"text-field": text
|
|
145
|
-
"text-font":
|
|
146
|
-
|
|
492
|
+
"text-field": buildTextFieldExpression(text),
|
|
493
|
+
"text-font": text.labelProperty ? [
|
|
494
|
+
"case",
|
|
495
|
+
[
|
|
496
|
+
"==",
|
|
497
|
+
["get", "textStyle"],
|
|
498
|
+
"italic"
|
|
499
|
+
],
|
|
500
|
+
["literal", ["Open Sans Italic"]],
|
|
501
|
+
[
|
|
502
|
+
"==",
|
|
503
|
+
["get", "textStyle"],
|
|
504
|
+
"light"
|
|
505
|
+
],
|
|
506
|
+
["literal", ["Open Sans Light"]],
|
|
507
|
+
["literal", ["Open Sans Regular"]]
|
|
508
|
+
] : ["Open Sans Regular"],
|
|
509
|
+
"text-size": buildTextSizeExpression(text),
|
|
147
510
|
"text-rotate": textRotate,
|
|
511
|
+
"text-anchor": textAnchor,
|
|
512
|
+
"text-justify": textJustify,
|
|
513
|
+
"text-max-width": 9999,
|
|
148
514
|
"text-rotation-alignment": "map",
|
|
149
515
|
"text-pitch-alignment": "map",
|
|
150
|
-
"text-allow-overlap": true
|
|
516
|
+
"text-allow-overlap": true,
|
|
517
|
+
"symbol-z-order": "source"
|
|
151
518
|
},
|
|
152
519
|
paint: {
|
|
153
520
|
"text-color": textColor,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
} : {}
|
|
521
|
+
"text-halo-color": buildTextHaloColorExpression(text),
|
|
522
|
+
"text-halo-width": buildTextHaloWidthExpression(text),
|
|
523
|
+
"text-opacity": TEXT_OPACITY_EXPRESSION
|
|
158
524
|
}
|
|
159
525
|
};
|
|
160
526
|
}
|
|
@@ -176,14 +542,79 @@ function buildFillPaint(style) {
|
|
|
176
542
|
"fill-opacity": 1
|
|
177
543
|
};
|
|
178
544
|
}
|
|
545
|
+
/**
|
|
546
|
+
* `properties.style.fillPattern` read as a GL expression, defaulting to
|
|
547
|
+
* `"solid"`. `fillPattern` is a per-feature-only field (ADR-0007) — unlike
|
|
548
|
+
* `fillColor`/`strokeColor` it has no layer-level `StyleOptions` default, so
|
|
549
|
+
* this expression takes no `style` argument.
|
|
550
|
+
*/
|
|
551
|
+
function fillPatternField() {
|
|
552
|
+
return [
|
|
553
|
+
"coalesce",
|
|
554
|
+
[
|
|
555
|
+
"get",
|
|
556
|
+
"fillPattern",
|
|
557
|
+
[
|
|
558
|
+
"coalesce",
|
|
559
|
+
["get", "style"],
|
|
560
|
+
["literal", {}]
|
|
561
|
+
]
|
|
562
|
+
],
|
|
563
|
+
"solid"
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Filter for one of the two mutually-exclusive Polygon fill layers, keyed on
|
|
568
|
+
* whether the feature's resolved `fillPattern` is a hatch pattern. The solid
|
|
569
|
+
* `-fill` layer takes `hatched: false` (every Polygon except hatched ones);
|
|
570
|
+
* the `-fill-pattern` layer takes `hatched: true`. Split because MapLibre's
|
|
571
|
+
* `fill-color` and `fill-pattern` paints cannot coexist on one fill layer.
|
|
572
|
+
*/
|
|
573
|
+
function buildFillFilter(hatched) {
|
|
574
|
+
const isHatched = [
|
|
575
|
+
"in",
|
|
576
|
+
fillPatternField(),
|
|
577
|
+
["literal", [...HATCH_PATTERNS]]
|
|
578
|
+
];
|
|
579
|
+
return [
|
|
580
|
+
"all",
|
|
581
|
+
[
|
|
582
|
+
"==",
|
|
583
|
+
["geometry-type"],
|
|
584
|
+
"Polygon"
|
|
585
|
+
],
|
|
586
|
+
hatched ? isHatched : ["!", isHatched]
|
|
587
|
+
];
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Paint for the hatch-pattern fill layer. The image id is baked per-feature
|
|
591
|
+
* into `properties._fillPatternImageId` (see `promoteFeature`), so the paint
|
|
592
|
+
* is a plain `["get", ...]` rather than re-deriving the id as a GL expression,
|
|
593
|
+
* keeping `fillPatternImageId` the single source of truth for the id string.
|
|
594
|
+
*/
|
|
595
|
+
const FILL_PATTERN_PAINT = {
|
|
596
|
+
"fill-pattern": ["get", FILL_PATTERN_IMAGE_ID_PROPERTY],
|
|
597
|
+
"fill-opacity": 1
|
|
598
|
+
};
|
|
599
|
+
const SOLID_LINE_DASH = [1, 0];
|
|
179
600
|
function buildLinePaint(style) {
|
|
180
|
-
const colorDefault = style?.strokeColor ??
|
|
181
|
-
const widthDefault = style?.strokeWidth ??
|
|
601
|
+
const colorDefault = style?.strokeColor ?? DEFAULT_LINE_COLOR;
|
|
602
|
+
const widthDefault = style?.strokeWidth ?? DEFAULT_LINE_WIDTH;
|
|
603
|
+
const dashDefault = style?.strokeDash?.length ? [...style.strokeDash] : SOLID_LINE_DASH;
|
|
182
604
|
const styleObj = [
|
|
183
605
|
"coalesce",
|
|
184
606
|
["get", "style"],
|
|
185
607
|
["literal", {}]
|
|
186
608
|
];
|
|
609
|
+
const dashExpr = [
|
|
610
|
+
"coalesce",
|
|
611
|
+
[
|
|
612
|
+
"get",
|
|
613
|
+
"strokeDash",
|
|
614
|
+
styleObj
|
|
615
|
+
],
|
|
616
|
+
["literal", dashDefault]
|
|
617
|
+
];
|
|
187
618
|
return {
|
|
188
619
|
"line-color": [
|
|
189
620
|
"coalesce",
|
|
@@ -203,14 +634,63 @@ function buildLinePaint(style) {
|
|
|
203
634
|
],
|
|
204
635
|
widthDefault
|
|
205
636
|
],
|
|
206
|
-
|
|
637
|
+
"line-dasharray": [
|
|
638
|
+
"case",
|
|
639
|
+
[
|
|
640
|
+
">",
|
|
641
|
+
["length", dashExpr],
|
|
642
|
+
0
|
|
643
|
+
],
|
|
644
|
+
dashExpr,
|
|
645
|
+
["literal", SOLID_LINE_DASH]
|
|
646
|
+
]
|
|
207
647
|
};
|
|
208
648
|
}
|
|
649
|
+
function geometryIncludes(feature, geometryTypes) {
|
|
650
|
+
const geometry = feature.geometry;
|
|
651
|
+
if (!geometry) return false;
|
|
652
|
+
if (geometry.type === "GeometryCollection") return geometry.geometries.some((candidate) => geometryTypes.has(candidate.type));
|
|
653
|
+
return geometryTypes.has(geometry.type);
|
|
654
|
+
}
|
|
655
|
+
const POLYGON_GEOMETRIES = new Set(["Polygon", "MultiPolygon"]);
|
|
656
|
+
const LINE_GEOMETRIES = new Set([
|
|
657
|
+
"LineString",
|
|
658
|
+
"MultiLineString",
|
|
659
|
+
"Polygon",
|
|
660
|
+
"MultiPolygon"
|
|
661
|
+
]);
|
|
662
|
+
const POINT_GEOMETRIES = new Set(["Point", "MultiPoint"]);
|
|
663
|
+
function featureStyle(feature) {
|
|
664
|
+
const style = feature.properties?.style;
|
|
665
|
+
return style && typeof style === "object" ? style : {};
|
|
666
|
+
}
|
|
667
|
+
function groupPaintChannels(features, style) {
|
|
668
|
+
const channels = /* @__PURE__ */ new Set();
|
|
669
|
+
for (const feature of features) {
|
|
670
|
+
if (geometryIncludes(feature, POLYGON_GEOMETRIES)) {
|
|
671
|
+
const fillPattern = featureStyle(feature).fillPattern;
|
|
672
|
+
if (featureStyle(feature).fillColor !== void 0 || style?.fillColor !== void 0 || isHatchPattern(fillPattern)) channels.add(isHatchPattern(fillPattern) ? "fill-pattern" : "fill");
|
|
673
|
+
}
|
|
674
|
+
if (geometryIncludes(feature, LINE_GEOMETRIES)) channels.add("line");
|
|
675
|
+
if (!geometryIncludes(feature, POINT_GEOMETRIES)) continue;
|
|
676
|
+
const properties = feature.properties ?? {};
|
|
677
|
+
const hasIcon = properties.iconId != null;
|
|
678
|
+
const hasText = style?.text !== void 0 && (style.text.labelProperty ? properties[style.text.labelProperty] != null : style.text.text !== void 0);
|
|
679
|
+
if (hasIcon) channels.add("icon");
|
|
680
|
+
if (hasText) channels.add("text");
|
|
681
|
+
if (!hasIcon && !hasText) channels.add("circle");
|
|
682
|
+
}
|
|
683
|
+
return channels;
|
|
684
|
+
}
|
|
209
685
|
var MapLibreAdapter = class extends BaseMapAdapter {
|
|
210
686
|
map;
|
|
211
|
-
|
|
687
|
+
viewChangeMode;
|
|
688
|
+
pendingIconImages = /* @__PURE__ */ new Set();
|
|
689
|
+
scalesGroundPointSymbolsNatively = true;
|
|
690
|
+
constructor(map, options = {}) {
|
|
212
691
|
super();
|
|
213
692
|
this.map = map;
|
|
693
|
+
this.viewChangeMode = options.viewChangeMode ?? "continuous";
|
|
214
694
|
}
|
|
215
695
|
toContinuousLng(lng) {
|
|
216
696
|
return nearestWorldCopyLng(lng, this.map.getCenter().lng);
|
|
@@ -228,14 +708,19 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
228
708
|
id: fillId,
|
|
229
709
|
type: "fill",
|
|
230
710
|
source: sourceId,
|
|
231
|
-
filter:
|
|
232
|
-
"==",
|
|
233
|
-
"$type",
|
|
234
|
-
"Polygon"
|
|
235
|
-
],
|
|
711
|
+
filter: buildFillFilter(false),
|
|
236
712
|
paint: buildFillPaint(style)
|
|
237
713
|
});
|
|
238
714
|
mlLayerIds.push(fillId);
|
|
715
|
+
const fillPatternId = `${layerId}-fill-pattern`;
|
|
716
|
+
this.map.addLayer({
|
|
717
|
+
id: fillPatternId,
|
|
718
|
+
type: "fill",
|
|
719
|
+
source: sourceId,
|
|
720
|
+
filter: buildFillFilter(true),
|
|
721
|
+
paint: FILL_PATTERN_PAINT
|
|
722
|
+
});
|
|
723
|
+
mlLayerIds.push(fillPatternId);
|
|
239
724
|
const lineId = `${layerId}-line`;
|
|
240
725
|
this.map.addLayer({
|
|
241
726
|
id: lineId,
|
|
@@ -270,6 +755,9 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
270
755
|
paint: buildCirclePaint(style)
|
|
271
756
|
});
|
|
272
757
|
mlLayerIds.push(circleId);
|
|
758
|
+
const iconId = `${layerId}-icon`;
|
|
759
|
+
this.map.addLayer(buildIconLayer(iconId, sourceId));
|
|
760
|
+
mlLayerIds.push(iconId);
|
|
273
761
|
if (style?.text) {
|
|
274
762
|
const symbolId = `${layerId}-symbol`;
|
|
275
763
|
this.map.addLayer(buildSymbolLayer(symbolId, sourceId, style.text));
|
|
@@ -278,29 +766,238 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
278
766
|
return {
|
|
279
767
|
sourceId,
|
|
280
768
|
mlLayerIds,
|
|
769
|
+
groupBundles: /* @__PURE__ */ new Map(),
|
|
770
|
+
groupOrder: [],
|
|
771
|
+
nextGroupBundleId: 0,
|
|
281
772
|
style
|
|
282
773
|
};
|
|
283
774
|
}
|
|
284
775
|
destroyLayer(handle) {
|
|
776
|
+
this.clearGroupBundles(handle);
|
|
285
777
|
for (const mlId of handle.mlLayerIds) if (this.map.getLayer(mlId)) this.map.removeLayer(mlId);
|
|
286
778
|
if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
|
|
287
779
|
}
|
|
288
|
-
|
|
780
|
+
applyFeatureGroups(layerId, groups) {
|
|
289
781
|
const handle = this.layers.get(layerId);
|
|
290
782
|
if (!handle) return;
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
783
|
+
const desiredGroupIds = new Set(groups.map((group) => group.id));
|
|
784
|
+
const previousGroupOrder = handle.groupOrder.filter((groupId) => desiredGroupIds.has(groupId));
|
|
785
|
+
const bundlesNeedingOrder = /* @__PURE__ */ new Set();
|
|
786
|
+
for (const [groupId, bundle] of handle.groupBundles) {
|
|
787
|
+
if (desiredGroupIds.has(groupId)) continue;
|
|
788
|
+
this.destroyGroupBundle(bundle);
|
|
789
|
+
handle.groupBundles.delete(groupId);
|
|
790
|
+
}
|
|
791
|
+
for (const group of groups) {
|
|
792
|
+
let bundle = handle.groupBundles.get(group.id);
|
|
793
|
+
const isNewBundle = bundle === void 0;
|
|
794
|
+
if (!bundle) {
|
|
795
|
+
bundle = this.createGroupBundle(layerId, handle);
|
|
796
|
+
handle.groupBundles.set(group.id, bundle);
|
|
797
|
+
bundlesNeedingOrder.add(group.id);
|
|
798
|
+
}
|
|
799
|
+
const featureSignature = JSON.stringify(group.features);
|
|
800
|
+
const contentChanged = isNewBundle || bundle.featureSignature !== featureSignature;
|
|
801
|
+
const requiredIconIds = this.requiredIconIds(group);
|
|
802
|
+
const resourcesChanged = bundle.requiredIconIds.size !== requiredIconIds.size || [...bundle.requiredIconIds].some((id) => !requiredIconIds.has(id));
|
|
803
|
+
bundle.features = group.features;
|
|
804
|
+
bundle.featureSignature = featureSignature;
|
|
805
|
+
bundle.requiredIconIds = requiredIconIds;
|
|
806
|
+
if (this.reconcileGroupChannels(handle, bundle, groupPaintChannels(group.features, handle.style))) bundlesNeedingOrder.add(group.id);
|
|
807
|
+
if (contentChanged || resourcesChanged) this.updateGroupBundleSource(bundle);
|
|
808
|
+
}
|
|
809
|
+
handle.groupOrder = groups.map((group) => group.id);
|
|
810
|
+
this.orderGroupBundles(handle, previousGroupOrder, handle.groupOrder, bundlesNeedingOrder);
|
|
811
|
+
}
|
|
812
|
+
createGroupBundle(layerId, handle) {
|
|
813
|
+
const sourceId = `${layerId}-group-${++handle.nextGroupBundleId}-src`;
|
|
814
|
+
this.map.addSource(sourceId, {
|
|
815
|
+
type: "geojson",
|
|
816
|
+
data: emptyFC(),
|
|
817
|
+
promoteId: "_fid"
|
|
818
|
+
});
|
|
819
|
+
return {
|
|
820
|
+
sourceId,
|
|
821
|
+
layerIds: /* @__PURE__ */ new Map(),
|
|
822
|
+
features: [],
|
|
823
|
+
featureSignature: "[]",
|
|
824
|
+
requiredIconIds: /* @__PURE__ */ new Set()
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
destroyGroupBundle(bundle) {
|
|
828
|
+
for (const layerId of bundle.layerIds.values()) if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
829
|
+
if (this.map.getSource(bundle.sourceId)) this.map.removeSource(bundle.sourceId);
|
|
830
|
+
}
|
|
831
|
+
clearGroupBundles(handle) {
|
|
832
|
+
for (const bundle of handle.groupBundles.values()) this.destroyGroupBundle(bundle);
|
|
833
|
+
handle.groupBundles.clear();
|
|
834
|
+
handle.groupOrder = [];
|
|
835
|
+
}
|
|
836
|
+
requiredIconIds(group) {
|
|
837
|
+
const referenced = new Set(group.features.flatMap((feature) => {
|
|
838
|
+
const iconId = feature.properties?.iconId;
|
|
839
|
+
return typeof iconId === "string" ? [iconId] : [];
|
|
840
|
+
}));
|
|
841
|
+
return new Set(group.iconResources.map((resource) => resource.id).filter((resourceId) => referenced.has(resourceId)));
|
|
842
|
+
}
|
|
843
|
+
reconcileGroupChannels(handle, bundle, desiredChannels) {
|
|
844
|
+
let changed = false;
|
|
845
|
+
for (const channel of PAINT_CHANNELS) {
|
|
846
|
+
const layerId = bundle.layerIds.get(channel);
|
|
847
|
+
if (layerId && !desiredChannels.has(channel)) {
|
|
848
|
+
if (this.map.getLayer(layerId)) this.map.removeLayer(layerId);
|
|
849
|
+
bundle.layerIds.delete(channel);
|
|
850
|
+
changed = true;
|
|
851
|
+
} else if (!layerId && desiredChannels.has(channel)) {
|
|
852
|
+
const nextLayerId = `${bundle.sourceId}-${channel}`;
|
|
853
|
+
this.map.addLayer(this.buildGroupLayer(nextLayerId, bundle.sourceId, channel, handle.style), handle.mlLayerIds[0]);
|
|
854
|
+
bundle.layerIds.set(channel, nextLayerId);
|
|
855
|
+
changed = true;
|
|
298
856
|
}
|
|
299
|
-
}
|
|
300
|
-
|
|
857
|
+
}
|
|
858
|
+
return changed;
|
|
859
|
+
}
|
|
860
|
+
buildGroupLayer(id, sourceId, channel, style) {
|
|
861
|
+
switch (channel) {
|
|
862
|
+
case "fill": return {
|
|
863
|
+
id,
|
|
864
|
+
type: "fill",
|
|
865
|
+
source: sourceId,
|
|
866
|
+
filter: buildFillFilter(false),
|
|
867
|
+
paint: buildFillPaint(style)
|
|
868
|
+
};
|
|
869
|
+
case "fill-pattern": return {
|
|
870
|
+
id,
|
|
871
|
+
type: "fill",
|
|
872
|
+
source: sourceId,
|
|
873
|
+
filter: buildFillFilter(true),
|
|
874
|
+
paint: FILL_PATTERN_PAINT
|
|
875
|
+
};
|
|
876
|
+
case "line": return {
|
|
877
|
+
id,
|
|
878
|
+
type: "line",
|
|
879
|
+
source: sourceId,
|
|
880
|
+
filter: [
|
|
881
|
+
"any",
|
|
882
|
+
[
|
|
883
|
+
"==",
|
|
884
|
+
"$type",
|
|
885
|
+
"LineString"
|
|
886
|
+
],
|
|
887
|
+
[
|
|
888
|
+
"==",
|
|
889
|
+
"$type",
|
|
890
|
+
"Polygon"
|
|
891
|
+
]
|
|
892
|
+
],
|
|
893
|
+
paint: buildLinePaint(style),
|
|
894
|
+
layout: {
|
|
895
|
+
"line-cap": "round",
|
|
896
|
+
"line-join": "round"
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
case "circle": return {
|
|
900
|
+
id,
|
|
901
|
+
type: "circle",
|
|
902
|
+
source: sourceId,
|
|
903
|
+
filter: buildCirclePointFilter(style),
|
|
904
|
+
paint: buildCirclePaint(style)
|
|
905
|
+
};
|
|
906
|
+
case "icon": return buildIconLayer(id, sourceId);
|
|
907
|
+
case "text":
|
|
908
|
+
if (!style?.text) throw new Error("Cannot create a MapLibre text channel without text style");
|
|
909
|
+
return buildSymbolLayer(id, sourceId, style.text);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
updateGroupBundleSource(bundle) {
|
|
913
|
+
const source = this.map.getSource(bundle.sourceId);
|
|
914
|
+
if (!source) return;
|
|
915
|
+
const features = [...bundle.requiredIconIds].every((id) => this.map.hasImage(id)) ? bundle.features.map((feature) => this.promoteFeature(feature)) : [];
|
|
916
|
+
const result = source.setData({
|
|
301
917
|
type: "FeatureCollection",
|
|
302
918
|
features
|
|
303
|
-
});
|
|
919
|
+
}, true);
|
|
920
|
+
if (isPromiseLike(result)) Promise.resolve(result).catch(() => {});
|
|
921
|
+
}
|
|
922
|
+
orderGroupBundles(handle, previousGroupIds, desiredGroupIds, forcedGroupIds) {
|
|
923
|
+
let beforeId = handle.mlLayerIds[0];
|
|
924
|
+
if (!beforeId) return;
|
|
925
|
+
const stableGroupIds = longestStableGroupIds(previousGroupIds, desiredGroupIds);
|
|
926
|
+
for (let index = desiredGroupIds.length - 1; index >= 0; index--) {
|
|
927
|
+
const groupId = desiredGroupIds[index];
|
|
928
|
+
const bundle = handle.groupBundles.get(groupId);
|
|
929
|
+
if (!bundle) continue;
|
|
930
|
+
if (!stableGroupIds.has(groupId) || forcedGroupIds.has(groupId)) for (const channel of PAINT_CHANNELS) {
|
|
931
|
+
const layerId = bundle.layerIds.get(channel);
|
|
932
|
+
if (layerId && this.map.getLayer(layerId)) this.map.moveLayer(layerId, beforeId);
|
|
933
|
+
}
|
|
934
|
+
beforeId = firstBundleLayerId(bundle) ?? beforeId;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
nativeLayerIds(handle) {
|
|
938
|
+
return [...handle.mlLayerIds, ...[...handle.groupBundles.values()].flatMap((bundle) => [...bundle.layerIds.values()])];
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Promote a source feature for MapLibre: stamp `_fid` (so data-driven paint
|
|
942
|
+
* and picks can key on the caller's feature id) and, for a hatch-patterned
|
|
943
|
+
* Polygon, `_fillPatternImageId` (registering the tile image on demand). At
|
|
944
|
+
* most one object spread — a feature needing neither passes through
|
|
945
|
+
* untouched.
|
|
946
|
+
*/
|
|
947
|
+
promoteFeature(feature) {
|
|
948
|
+
const extra = {};
|
|
949
|
+
if (feature.id !== void 0) extra._fid = feature.id;
|
|
950
|
+
const imageId = this.resolveFillPatternImage(feature);
|
|
951
|
+
if (imageId !== void 0) extra[FILL_PATTERN_IMAGE_ID_PROPERTY] = imageId;
|
|
952
|
+
if (Object.keys(extra).length === 0) return feature;
|
|
953
|
+
return {
|
|
954
|
+
...feature,
|
|
955
|
+
properties: {
|
|
956
|
+
...feature.properties ?? {},
|
|
957
|
+
...extra
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Resolve a hatch-patterned Polygon's `style.fillPattern`/`fillColor` to its
|
|
963
|
+
* hatch-tile image id, registering the image on demand. Returns `undefined`
|
|
964
|
+
* for features without a hatch pattern, so `fillPatternImageId` stays the
|
|
965
|
+
* single source of truth for the id baked into `_fillPatternImageId`.
|
|
966
|
+
*/
|
|
967
|
+
resolveFillPatternImage(feature) {
|
|
968
|
+
const props = feature.properties;
|
|
969
|
+
const pattern = props?.style?.fillPattern;
|
|
970
|
+
if (!isHatchPattern(pattern)) return void 0;
|
|
971
|
+
const color = props?.fillColor ?? props?.style?.fillColor ?? "rgba(0,0,0,0)";
|
|
972
|
+
const imageId = fillPatternImageId(color, pattern);
|
|
973
|
+
this.ensureFillPatternImage(imageId, color, pattern);
|
|
974
|
+
return imageId;
|
|
975
|
+
}
|
|
976
|
+
/** Register a hatch-tile image once per (color, pattern) pair, keyed by `fillPatternImageId`. */
|
|
977
|
+
ensureFillPatternImage(imageId, color, pattern) {
|
|
978
|
+
if (this.map.hasImage(imageId)) return;
|
|
979
|
+
const image = makeFillPatternImage(color, pattern, 2);
|
|
980
|
+
this.map.addImage(imageId, image, { pixelRatio: 2 });
|
|
981
|
+
}
|
|
982
|
+
applyIconResources(resources) {
|
|
983
|
+
if (typeof Image === "undefined") return;
|
|
984
|
+
for (const resource of resources) {
|
|
985
|
+
if (this.map.hasImage(resource.id) || this.pendingIconImages.has(resource.id)) continue;
|
|
986
|
+
this.pendingIconImages.add(resource.id);
|
|
987
|
+
const image = new Image();
|
|
988
|
+
image.onload = () => {
|
|
989
|
+
if (!this.pendingIconImages.has(resource.id)) return;
|
|
990
|
+
this.pendingIconImages.delete(resource.id);
|
|
991
|
+
if (!this.map.hasImage(resource.id)) this.map.addImage(resource.id, image);
|
|
992
|
+
for (const handle of this.layers.values()) for (const bundle of handle.groupBundles.values()) if (bundle.requiredIconIds.has(resource.id)) this.updateGroupBundleSource(bundle);
|
|
993
|
+
};
|
|
994
|
+
image.onerror = () => this.pendingIconImages.delete(resource.id);
|
|
995
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(centeredSvg(resource))}`;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
removeIconResource(id) {
|
|
999
|
+
this.pendingIconImages.delete(id);
|
|
1000
|
+
if (this.map.hasImage(id)) this.map.removeImage(id);
|
|
304
1001
|
}
|
|
305
1002
|
attachNativeEvent(type, onEvent) {
|
|
306
1003
|
const mlEvent = type === "pointermove" ? "mousemove" : type === "dblclick" ? "dblclick" : "click";
|
|
@@ -318,17 +1015,19 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
318
1015
|
}
|
|
319
1016
|
attachViewChange(handler) {
|
|
320
1017
|
const fn = () => handler();
|
|
321
|
-
this.map.on(
|
|
322
|
-
this.map.on("rotate", fn);
|
|
323
|
-
this.map.on("pitch", fn);
|
|
324
|
-
this.map.on("moveend", fn);
|
|
1018
|
+
for (const event of this.viewChangeEvents()) this.map.on(event, fn);
|
|
325
1019
|
return { fn };
|
|
326
1020
|
}
|
|
327
1021
|
detachViewChange(token) {
|
|
328
|
-
this.map.off(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.
|
|
1022
|
+
for (const event of this.viewChangeEvents()) this.map.off(event, token.fn);
|
|
1023
|
+
}
|
|
1024
|
+
viewChangeEvents() {
|
|
1025
|
+
return this.viewChangeMode === "settle" ? ["moveend"] : [
|
|
1026
|
+
"zoom",
|
|
1027
|
+
"rotate",
|
|
1028
|
+
"pitch",
|
|
1029
|
+
"moveend"
|
|
1030
|
+
];
|
|
332
1031
|
}
|
|
333
1032
|
createEditPointerDriver() {
|
|
334
1033
|
const map = this.map;
|
|
@@ -338,7 +1037,10 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
338
1037
|
let disposed = false;
|
|
339
1038
|
let touchDragActive = false;
|
|
340
1039
|
let multiTouchCancelled = false;
|
|
341
|
-
const
|
|
1040
|
+
const touchInfo = (e) => ({
|
|
1041
|
+
hitTolerance: TOUCH_HIT_TOLERANCE_PX,
|
|
1042
|
+
native: e.originalEvent
|
|
1043
|
+
});
|
|
342
1044
|
const preventMapEventDefaults = (e) => {
|
|
343
1045
|
e.preventDefault();
|
|
344
1046
|
e.originalEvent.preventDefault?.();
|
|
@@ -346,22 +1048,32 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
346
1048
|
};
|
|
347
1049
|
const onDown = (e) => {
|
|
348
1050
|
const px = [e.point.x, e.point.y];
|
|
349
|
-
const info = {
|
|
1051
|
+
const info = {
|
|
1052
|
+
altKey: e.originalEvent.altKey,
|
|
1053
|
+
shiftKey: e.originalEvent.shiftKey,
|
|
1054
|
+
native: e.originalEvent
|
|
1055
|
+
};
|
|
350
1056
|
hub.fireDown(px, info);
|
|
351
1057
|
};
|
|
352
1058
|
const onMove = (e) => {
|
|
353
1059
|
const px = [e.point.x, e.point.y];
|
|
354
|
-
hub.fireMove(px
|
|
1060
|
+
hub.fireMove(px, {
|
|
1061
|
+
shiftKey: e.originalEvent.shiftKey,
|
|
1062
|
+
native: e.originalEvent
|
|
1063
|
+
});
|
|
355
1064
|
};
|
|
356
1065
|
const onUp = (e) => {
|
|
357
1066
|
const px = [e.point.x, e.point.y];
|
|
358
|
-
hub.fireUp(px
|
|
1067
|
+
hub.fireUp(px, {
|
|
1068
|
+
shiftKey: e.originalEvent.shiftKey,
|
|
1069
|
+
native: e.originalEvent
|
|
1070
|
+
});
|
|
359
1071
|
};
|
|
360
1072
|
const onTouchStart = (e) => {
|
|
361
1073
|
if (e.points.length !== 1) return;
|
|
362
1074
|
const before = dragPanWasEnabled;
|
|
363
1075
|
const px = [e.point.x, e.point.y];
|
|
364
|
-
hub.fireDown(px,
|
|
1076
|
+
hub.fireDown(px, touchInfo(e));
|
|
365
1077
|
touchDragActive = dragPanWasEnabled !== before;
|
|
366
1078
|
if (touchDragActive) preventMapEventDefaults(e);
|
|
367
1079
|
};
|
|
@@ -370,20 +1082,20 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
370
1082
|
const px = [e.point.x, e.point.y];
|
|
371
1083
|
if (touchCount > 1 && touchDragActive && !multiTouchCancelled) {
|
|
372
1084
|
multiTouchCancelled = true;
|
|
373
|
-
hub.fireUp(px,
|
|
1085
|
+
hub.fireUp(px, touchInfo(e));
|
|
374
1086
|
touchDragActive = false;
|
|
375
1087
|
preventMapEventDefaults(e);
|
|
376
1088
|
return;
|
|
377
1089
|
}
|
|
378
1090
|
if (touchCount > 1) return;
|
|
379
1091
|
if (touchCount === 1) multiTouchCancelled = false;
|
|
380
|
-
hub.fireMove(px,
|
|
1092
|
+
hub.fireMove(px, touchInfo(e));
|
|
381
1093
|
if (touchDragActive) preventMapEventDefaults(e);
|
|
382
1094
|
};
|
|
383
1095
|
const onTouchEnd = (e) => {
|
|
384
1096
|
multiTouchCancelled = false;
|
|
385
1097
|
const px = [e.point.x, e.point.y];
|
|
386
|
-
hub.fireUp(px,
|
|
1098
|
+
hub.fireUp(px, touchInfo(e));
|
|
387
1099
|
touchDragActive = false;
|
|
388
1100
|
};
|
|
389
1101
|
map.on("mousedown", onDown);
|
|
@@ -435,40 +1147,72 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
435
1147
|
setLayerStyle(layerId, style) {
|
|
436
1148
|
const handle = this.layers.get(layerId);
|
|
437
1149
|
if (!handle) return;
|
|
438
|
-
|
|
1150
|
+
const resolvedStyle = {
|
|
1151
|
+
...handle.style,
|
|
1152
|
+
...style,
|
|
1153
|
+
...style.text ? { text: {
|
|
1154
|
+
...handle.style?.text,
|
|
1155
|
+
...style.text
|
|
1156
|
+
} } : {}
|
|
1157
|
+
};
|
|
1158
|
+
handle.style = resolvedStyle;
|
|
439
1159
|
for (const mlId of handle.mlLayerIds) {
|
|
440
|
-
const
|
|
441
|
-
if (
|
|
442
|
-
const type = layer.type;
|
|
443
|
-
if (type === "fill") this.map.setPaintProperty(mlId, "fill-color", buildFillPaint(style)["fill-color"]);
|
|
444
|
-
else if (type === "line") {
|
|
445
|
-
if (style.strokeColor !== void 0 || style.strokeWidth !== void 0) {
|
|
446
|
-
const paint = buildLinePaint(style);
|
|
447
|
-
if (style.strokeColor !== void 0) this.map.setPaintProperty(mlId, "line-color", paint["line-color"]);
|
|
448
|
-
if (style.strokeWidth !== void 0) this.map.setPaintProperty(mlId, "line-width", paint["line-width"]);
|
|
449
|
-
}
|
|
450
|
-
if (style.strokeDash) this.map.setPaintProperty(mlId, "line-dasharray", style.strokeDash);
|
|
451
|
-
} else if (type === "circle") {
|
|
452
|
-
const paint = buildCirclePaint(style);
|
|
453
|
-
this.map.setPaintProperty(mlId, "circle-radius", paint["circle-radius"]);
|
|
454
|
-
this.map.setPaintProperty(mlId, "circle-color", paint["circle-color"]);
|
|
455
|
-
this.map.setPaintProperty(mlId, "circle-stroke-color", paint["circle-stroke-color"]);
|
|
456
|
-
this.map.setPaintProperty(mlId, "circle-stroke-width", paint["circle-stroke-width"]);
|
|
457
|
-
this.map.setFilter(mlId, buildCirclePointFilter(style));
|
|
458
|
-
} else if (type === "symbol" && style.text) {
|
|
459
|
-
if (style.text.text) this.map.setLayoutProperty(mlId, "text-field", style.text.text);
|
|
460
|
-
else if (style.text.labelProperty) this.map.setLayoutProperty(mlId, "text-field", ["get", style.text.labelProperty]);
|
|
461
|
-
if (style.text.fillColor) this.map.setPaintProperty(mlId, "text-color", style.text.fillColor);
|
|
462
|
-
if (style.text.strokeColor) this.map.setPaintProperty(mlId, "text-halo-color", style.text.strokeColor);
|
|
463
|
-
if (style.text.strokeWidth !== void 0) this.map.setPaintProperty(mlId, "text-halo-width", style.text.strokeWidth);
|
|
464
|
-
}
|
|
1160
|
+
const channel = this.legacyPaintChannel(mlId);
|
|
1161
|
+
if (channel) this.applyChannelStyle(mlId, channel, style, resolvedStyle);
|
|
465
1162
|
}
|
|
1163
|
+
const bundlesNeedingOrder = /* @__PURE__ */ new Set();
|
|
1164
|
+
for (const [groupId, bundle] of handle.groupBundles) if (this.reconcileGroupChannels(handle, bundle, groupPaintChannels(bundle.features, resolvedStyle))) bundlesNeedingOrder.add(groupId);
|
|
1165
|
+
this.orderGroupBundles(handle, handle.groupOrder, handle.groupOrder, bundlesNeedingOrder);
|
|
1166
|
+
for (const bundle of handle.groupBundles.values()) for (const [channel, mlId] of bundle.layerIds) this.applyChannelStyle(mlId, channel, style, resolvedStyle);
|
|
466
1167
|
if (style.text && !handle.mlLayerIds.some((id) => id.endsWith("-symbol"))) {
|
|
467
1168
|
const symbolId = `${layerId}-symbol`;
|
|
468
|
-
this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId,
|
|
1169
|
+
this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId, resolvedStyle.text));
|
|
469
1170
|
handle.mlLayerIds.push(symbolId);
|
|
470
1171
|
}
|
|
471
1172
|
}
|
|
1173
|
+
legacyPaintChannel(layerId) {
|
|
1174
|
+
if (layerId.endsWith("-fill-pattern")) return "fill-pattern";
|
|
1175
|
+
if (layerId.endsWith("-fill")) return "fill";
|
|
1176
|
+
if (layerId.endsWith("-line")) return "line";
|
|
1177
|
+
if (layerId.endsWith("-circle")) return "circle";
|
|
1178
|
+
if (layerId.endsWith("-icon")) return "icon";
|
|
1179
|
+
if (layerId.endsWith("-symbol")) return "text";
|
|
1180
|
+
}
|
|
1181
|
+
applyChannelStyle(layerId, channel, styleUpdate, resolvedStyle) {
|
|
1182
|
+
if (!this.map.getLayer(layerId)) return;
|
|
1183
|
+
switch (channel) {
|
|
1184
|
+
case "fill":
|
|
1185
|
+
if (styleUpdate.fillColor === void 0) break;
|
|
1186
|
+
this.map.setPaintProperty(layerId, "fill-color", buildFillPaint(resolvedStyle)["fill-color"]);
|
|
1187
|
+
break;
|
|
1188
|
+
case "fill-pattern": break;
|
|
1189
|
+
case "line": {
|
|
1190
|
+
const paint = buildLinePaint(resolvedStyle);
|
|
1191
|
+
if (styleUpdate.strokeColor !== void 0) this.map.setPaintProperty(layerId, "line-color", paint["line-color"]);
|
|
1192
|
+
if (styleUpdate.strokeWidth !== void 0) this.map.setPaintProperty(layerId, "line-width", paint["line-width"]);
|
|
1193
|
+
if (styleUpdate.strokeDash !== void 0) this.map.setPaintProperty(layerId, "line-dasharray", paint["line-dasharray"]);
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
case "circle": {
|
|
1197
|
+
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;
|
|
1198
|
+
const paint = buildCirclePaint(resolvedStyle);
|
|
1199
|
+
this.map.setPaintProperty(layerId, "circle-radius", paint["circle-radius"]);
|
|
1200
|
+
this.map.setPaintProperty(layerId, "circle-color", paint["circle-color"]);
|
|
1201
|
+
this.map.setPaintProperty(layerId, "circle-stroke-color", paint["circle-stroke-color"]);
|
|
1202
|
+
this.map.setPaintProperty(layerId, "circle-stroke-width", paint["circle-stroke-width"]);
|
|
1203
|
+
this.map.setFilter(layerId, buildCirclePointFilter(resolvedStyle));
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
case "icon": break;
|
|
1207
|
+
case "text":
|
|
1208
|
+
if (!styleUpdate.text || !resolvedStyle.text) break;
|
|
1209
|
+
if (styleUpdate.text.text || styleUpdate.text.labelProperty) this.map.setLayoutProperty(layerId, "text-field", buildTextFieldExpression(resolvedStyle.text));
|
|
1210
|
+
if (styleUpdate.text.fillColor) this.map.setPaintProperty(layerId, "text-color", resolvedStyle.text.fillColor);
|
|
1211
|
+
if (styleUpdate.text.strokeColor) this.map.setPaintProperty(layerId, "text-halo-color", buildTextHaloColorExpression(resolvedStyle.text));
|
|
1212
|
+
if (styleUpdate.text.strokeWidth !== void 0) this.map.setPaintProperty(layerId, "text-halo-width", buildTextHaloWidthExpression(resolvedStyle.text));
|
|
1213
|
+
break;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
472
1216
|
toLonLat(coordinate) {
|
|
473
1217
|
return unproject(coordinate[0], coordinate[1]);
|
|
474
1218
|
}
|
|
@@ -479,6 +1223,10 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
479
1223
|
const point = this.map.project([this.toContinuousLng(position[0]), position[1]]);
|
|
480
1224
|
return [point.x, point.y];
|
|
481
1225
|
}
|
|
1226
|
+
getCoordinateFromPixel(pixel) {
|
|
1227
|
+
const coordinate = this.map.unproject(pixel);
|
|
1228
|
+
return [this.toContinuousLng(coordinate.lng), coordinate.lat];
|
|
1229
|
+
}
|
|
482
1230
|
getViewportSize() {
|
|
483
1231
|
const container = this.map.getContainer();
|
|
484
1232
|
return {
|
|
@@ -508,13 +1256,14 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
508
1256
|
else this.map.doubleClickZoom.disable();
|
|
509
1257
|
}
|
|
510
1258
|
getResolution() {
|
|
511
|
-
|
|
512
|
-
const center = this.map.getCenter();
|
|
513
|
-
return 40075016.686 * Math.cos(center.lat * Math.PI / 180) / Math.pow(2, zoom + 8);
|
|
1259
|
+
return WEB_MERCATOR_CIRCUMFERENCE / Math.pow(2, this.map.getZoom() + 9);
|
|
514
1260
|
}
|
|
515
1261
|
getZoom() {
|
|
516
1262
|
return this.map.getZoom();
|
|
517
1263
|
}
|
|
1264
|
+
getBearing() {
|
|
1265
|
+
return this.map.getBearing() * (Math.PI / 180);
|
|
1266
|
+
}
|
|
518
1267
|
onPick(layerId, handler, opts) {
|
|
519
1268
|
const onMlClick = (e) => {
|
|
520
1269
|
const handle = this.layers.get(layerId);
|
|
@@ -523,20 +1272,48 @@ var MapLibreAdapter = class extends BaseMapAdapter {
|
|
|
523
1272
|
const tol = pickHitTolerance(originalEvent);
|
|
524
1273
|
const { x, y } = e.point;
|
|
525
1274
|
const bbox = [[x - tol, y - tol], [x + tol, y + tol]];
|
|
526
|
-
const
|
|
527
|
-
if (features.length === 0) return;
|
|
528
|
-
const f = features[0];
|
|
529
|
-
const geojson = {
|
|
1275
|
+
const relevantFeatures = mergeNativeAndCachedPickFeatures(this.map.queryRenderedFeatures(bbox, { layers: this.nativeLayerIds(handle) }).map((feature) => ({
|
|
530
1276
|
type: "Feature",
|
|
531
|
-
id:
|
|
532
|
-
geometry:
|
|
533
|
-
properties:
|
|
534
|
-
};
|
|
535
|
-
const
|
|
1277
|
+
id: feature.id,
|
|
1278
|
+
geometry: feature.geometry,
|
|
1279
|
+
properties: feature.properties
|
|
1280
|
+
})), this.featureCache.get(layerId) ?? []);
|
|
1281
|
+
const size = this.getViewportSize();
|
|
1282
|
+
const candidate = evaluatePickCandidates(relevantFeatures.map(({ feature, nativeHit, visualOrder }) => {
|
|
1283
|
+
const iconId = feature.properties?.iconId;
|
|
1284
|
+
const icon = typeof iconId === "string" ? this.getIconResource(iconId) : void 0;
|
|
1285
|
+
const featureGroupOrder = this.getFeatureGroupOrder(layerId, feature.id);
|
|
1286
|
+
return {
|
|
1287
|
+
feature,
|
|
1288
|
+
visualOrder,
|
|
1289
|
+
nativeHit,
|
|
1290
|
+
...featureGroupOrder !== void 0 ? { featureGroupOrder } : {},
|
|
1291
|
+
...icon ? { icon } : {},
|
|
1292
|
+
layerStyle: {
|
|
1293
|
+
...handle.style,
|
|
1294
|
+
strokeColor: handle.style?.strokeColor ?? DEFAULT_LINE_COLOR,
|
|
1295
|
+
strokeWidth: handle.style?.strokeWidth ?? DEFAULT_LINE_WIDTH
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
}), {
|
|
1299
|
+
pixel: [x, y],
|
|
1300
|
+
pointerType: pickPointerType(originalEvent),
|
|
1301
|
+
tolerance: tol,
|
|
1302
|
+
toPixel: (position) => this.getPixelFromCoordinate(position),
|
|
1303
|
+
viewport: {
|
|
1304
|
+
minX: 0,
|
|
1305
|
+
minY: 0,
|
|
1306
|
+
maxX: size.width,
|
|
1307
|
+
maxY: size.height
|
|
1308
|
+
},
|
|
1309
|
+
bearing: this.getBearing()
|
|
1310
|
+
});
|
|
1311
|
+
if (!candidate) return;
|
|
1312
|
+
const cmId = candidate.graphicId;
|
|
536
1313
|
if (cmId == null) return;
|
|
537
1314
|
handler({
|
|
538
1315
|
id: cmId,
|
|
539
|
-
feature:
|
|
1316
|
+
feature: candidate.feature,
|
|
540
1317
|
pixel: [x, y],
|
|
541
1318
|
coordinate: [this.toContinuousLng(e.lngLat.lng), e.lngLat.lat],
|
|
542
1319
|
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-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.20",
|
|
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-alpha.
|
|
41
|
-
"@orbat-mapper/tactical-draw": "^0.2.0-alpha.
|
|
40
|
+
"@orbat-mapper/control-measures": "^0.2.0-alpha.25",
|
|
41
|
+
"@orbat-mapper/tactical-draw": "^0.2.0-alpha.35"
|
|
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",
|