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