@orbat-mapper/tactical-draw-adapter-maplibre 0.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.mjs +548 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orbat Mapper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @orbat-mapper/tactical-draw-adapter-maplibre
|
|
2
|
+
|
|
3
|
+
The [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) engine adapter for
|
|
4
|
+
the `@orbat-mapper/tactical-draw` toolkit. It implements the `MapAdapter` contract
|
|
5
|
+
from `@orbat-mapper/tactical-draw` on top of MapLibre, so the engine-agnostic
|
|
6
|
+
draw/edit controllers — sessions, draw rules, gestures, handle rendering — run
|
|
7
|
+
unchanged against a MapLibre map.
|
|
8
|
+
|
|
9
|
+
`maplibre-gl` is declared as a **peer dependency**, so you bring your own single
|
|
10
|
+
copy and never install an engine you don't use.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# npm
|
|
16
|
+
npm install @orbat-mapper/tactical-draw-adapter-maplibre @orbat-mapper/tactical-draw maplibre-gl
|
|
17
|
+
|
|
18
|
+
# pnpm
|
|
19
|
+
pnpm add @orbat-mapper/tactical-draw-adapter-maplibre @orbat-mapper/tactical-draw maplibre-gl
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import maplibregl from "maplibre-gl";
|
|
26
|
+
import { TacticalDraw } from "@orbat-mapper/tactical-draw";
|
|
27
|
+
import { MapLibreAdapter } from "@orbat-mapper/tactical-draw-adapter-maplibre";
|
|
28
|
+
|
|
29
|
+
const map = new maplibregl.Map({
|
|
30
|
+
/* … */
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const adapter = new MapLibreAdapter(map);
|
|
34
|
+
const draw = new TacticalDraw(adapter);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Hand the adapter to `TacticalDraw` and drive draw/edit exactly as with any other
|
|
38
|
+
engine adapter in the family.
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BaseMapAdapter, EditPointerDriver, FeatureOps, LayerId, MapCursor, NativeEventEntry, PickHandler, PickOptions, PixelCoordinate, StyleOptions } from "@orbat-mapper/tactical-draw";
|
|
2
|
+
import maplibregl from "maplibre-gl";
|
|
3
|
+
import { Position } from "geojson";
|
|
4
|
+
|
|
5
|
+
//#region src/ml-adapter.d.ts
|
|
6
|
+
interface MapLibreLayerHandle {
|
|
7
|
+
sourceId: string;
|
|
8
|
+
/** MapLibre layer ids created for this logical layer (fill, line, circle, symbol) */
|
|
9
|
+
mlLayerIds: string[];
|
|
10
|
+
style?: StyleOptions;
|
|
11
|
+
}
|
|
12
|
+
type MapLibreEventToken = (e: any) => void;
|
|
13
|
+
interface MapLibreViewToken {
|
|
14
|
+
fn: () => void;
|
|
15
|
+
}
|
|
16
|
+
declare class MapLibreAdapter extends BaseMapAdapter<MapLibreLayerHandle, MapLibreEventToken, MapLibreViewToken> {
|
|
17
|
+
private map;
|
|
18
|
+
constructor(map: maplibregl.Map);
|
|
19
|
+
protected createLayer(layerId: LayerId, style?: StyleOptions): MapLibreLayerHandle;
|
|
20
|
+
protected destroyLayer(handle: MapLibreLayerHandle): void;
|
|
21
|
+
protected applyFeatureOps(layerId: LayerId, ops: FeatureOps): void;
|
|
22
|
+
protected attachNativeEvent(type: "click" | "pointermove" | "dblclick", onEvent: (lonLat: Position, pixel: PixelCoordinate, originalEvent: unknown) => void): NativeEventEntry<MapLibreEventToken>;
|
|
23
|
+
protected detachNativeEvent(entry: NativeEventEntry<MapLibreEventToken>): void;
|
|
24
|
+
protected attachViewChange(handler: () => void): MapLibreViewToken;
|
|
25
|
+
protected detachViewChange(token: MapLibreViewToken): void;
|
|
26
|
+
createEditPointerDriver(): EditPointerDriver;
|
|
27
|
+
setLayerStyle(layerId: LayerId, style: StyleOptions): void;
|
|
28
|
+
toLonLat(coordinate: number[]): Position;
|
|
29
|
+
fromLonLat(position: Position): number[];
|
|
30
|
+
getPixelFromCoordinate(position: Position): PixelCoordinate | null;
|
|
31
|
+
getViewportSize(): {
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
};
|
|
35
|
+
panByPixels(delta: PixelCoordinate, options?: {
|
|
36
|
+
durationMs?: number;
|
|
37
|
+
}): void;
|
|
38
|
+
setCursor(cursor: MapCursor): void;
|
|
39
|
+
setDoubleClickZoomEnabled(enabled: boolean): void;
|
|
40
|
+
getResolution(): number | undefined;
|
|
41
|
+
getZoom(): number | undefined;
|
|
42
|
+
onPick(layerId: LayerId, handler: PickHandler, opts?: PickOptions): () => void;
|
|
43
|
+
setGlobeMode(enabled: boolean): void;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { MapLibreAdapter };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import { project, unproject } from "@orbat-mapper/control-measures";
|
|
2
|
+
import { BaseMapAdapter, PointerCallbackHub, TOUCH_HIT_TOLERANCE_PX, bindAbortable, pickHitTolerance, tryControlMeasureIdFromFeature } from "@orbat-mapper/tactical-draw";
|
|
3
|
+
//#region src/ml-adapter.ts
|
|
4
|
+
function emptyFC() {
|
|
5
|
+
return {
|
|
6
|
+
type: "FeatureCollection",
|
|
7
|
+
features: []
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build the paint expression for a circle layer with per-feature
|
|
12
|
+
* `properties.style` overlay (ADR-0006). Each property prefers a value
|
|
13
|
+
* pulled from `properties.style.<field>`, falling back to the layer default.
|
|
14
|
+
*/
|
|
15
|
+
function buildCirclePaint(style) {
|
|
16
|
+
const radiusDefault = style?.pointRadius ?? 5;
|
|
17
|
+
const colorDefault = style?.pointFillColor ?? style?.fillColor ?? "white";
|
|
18
|
+
const strokeColorDefault = style?.strokeColor ?? "black";
|
|
19
|
+
const strokeWidthDefault = style?.strokeWidth ?? 1;
|
|
20
|
+
const styleObj = [
|
|
21
|
+
"coalesce",
|
|
22
|
+
["get", "style"],
|
|
23
|
+
["literal", {}]
|
|
24
|
+
];
|
|
25
|
+
const pick = (field, fallback) => [
|
|
26
|
+
"coalesce",
|
|
27
|
+
[
|
|
28
|
+
"get",
|
|
29
|
+
field,
|
|
30
|
+
styleObj
|
|
31
|
+
],
|
|
32
|
+
fallback
|
|
33
|
+
];
|
|
34
|
+
return {
|
|
35
|
+
"circle-radius": pick("pointRadius", radiusDefault),
|
|
36
|
+
"circle-color": pick("fillColor", colorDefault),
|
|
37
|
+
"circle-stroke-color": pick("strokeColor", strokeColorDefault),
|
|
38
|
+
"circle-stroke-width": pick("strokeWidth", strokeWidthDefault)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Filter for the circle (Point) layer. Point features carrying label text are
|
|
43
|
+
* drawn by the symbol/text layer only, so exclude them here — otherwise the
|
|
44
|
+
* label would be ringed by a marker dot. Matches the OpenLayers adapter, which
|
|
45
|
+
* draws a dot for a Point only when a point radius is set.
|
|
46
|
+
*/
|
|
47
|
+
function buildCirclePointFilter(style) {
|
|
48
|
+
return style?.text?.labelProperty ? [
|
|
49
|
+
"all",
|
|
50
|
+
[
|
|
51
|
+
"==",
|
|
52
|
+
"$type",
|
|
53
|
+
"Point"
|
|
54
|
+
],
|
|
55
|
+
["!has", style.text.labelProperty]
|
|
56
|
+
] : [
|
|
57
|
+
"==",
|
|
58
|
+
"$type",
|
|
59
|
+
"Point"
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function buildTextSizeExpression() {
|
|
63
|
+
const size = [
|
|
64
|
+
"coalesce",
|
|
65
|
+
["get", "textSizePixels"],
|
|
66
|
+
14
|
|
67
|
+
];
|
|
68
|
+
const referenceZoom = [
|
|
69
|
+
"coalesce",
|
|
70
|
+
["get", "textSizeZoom"],
|
|
71
|
+
0
|
|
72
|
+
];
|
|
73
|
+
const sizeAtZoom = (zoom) => [
|
|
74
|
+
"min",
|
|
75
|
+
254,
|
|
76
|
+
[
|
|
77
|
+
"case",
|
|
78
|
+
["has", "textSizeZoom"],
|
|
79
|
+
[
|
|
80
|
+
"*",
|
|
81
|
+
size,
|
|
82
|
+
[
|
|
83
|
+
"^",
|
|
84
|
+
2,
|
|
85
|
+
[
|
|
86
|
+
"-",
|
|
87
|
+
zoom,
|
|
88
|
+
referenceZoom
|
|
89
|
+
]
|
|
90
|
+
]
|
|
91
|
+
],
|
|
92
|
+
size
|
|
93
|
+
]
|
|
94
|
+
];
|
|
95
|
+
return [
|
|
96
|
+
"interpolate",
|
|
97
|
+
["linear"],
|
|
98
|
+
["zoom"],
|
|
99
|
+
...Array.from({ length: 25 }, (_, zoom) => [zoom, sizeAtZoom(zoom)]).flat()
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build the paint expression for a fill layer with per-feature
|
|
104
|
+
* `properties.style.fillColor` overlay. Mirrors `buildCirclePaint`.
|
|
105
|
+
*/
|
|
106
|
+
/**
|
|
107
|
+
* Build the layout and paint objects for a symbol (text) layer.
|
|
108
|
+
* Centralises the rotation convention so `createLayer` and `setLayerStyle`
|
|
109
|
+
* stay in sync. Rotation follows the OpenLayers convention: `π − r` radians
|
|
110
|
+
* (converted to degrees for MapLibre).
|
|
111
|
+
*/
|
|
112
|
+
function buildSymbolLayer(symbolId, sourceId, text) {
|
|
113
|
+
const textRotate = text.labelProperty ? [
|
|
114
|
+
"-",
|
|
115
|
+
180,
|
|
116
|
+
[
|
|
117
|
+
"*",
|
|
118
|
+
[
|
|
119
|
+
"coalesce",
|
|
120
|
+
["get", "rotation"],
|
|
121
|
+
0
|
|
122
|
+
],
|
|
123
|
+
180 / Math.PI
|
|
124
|
+
]
|
|
125
|
+
] : text.rotation ? (Math.PI - text.rotation) * 180 / Math.PI : 0;
|
|
126
|
+
const textColor = text.fillColor ? text.fillColor : [
|
|
127
|
+
"coalesce",
|
|
128
|
+
[
|
|
129
|
+
"get",
|
|
130
|
+
"strokeColor",
|
|
131
|
+
[
|
|
132
|
+
"coalesce",
|
|
133
|
+
["get", "style"],
|
|
134
|
+
["literal", {}]
|
|
135
|
+
]
|
|
136
|
+
],
|
|
137
|
+
"#000"
|
|
138
|
+
];
|
|
139
|
+
return {
|
|
140
|
+
id: symbolId,
|
|
141
|
+
type: "symbol",
|
|
142
|
+
source: sourceId,
|
|
143
|
+
layout: {
|
|
144
|
+
"text-field": text.labelProperty ? ["get", text.labelProperty] : text.text ?? "",
|
|
145
|
+
"text-font": ["Open Sans Regular"],
|
|
146
|
+
"text-size": buildTextSizeExpression(),
|
|
147
|
+
"text-rotate": textRotate,
|
|
148
|
+
"text-rotation-alignment": "map",
|
|
149
|
+
"text-pitch-alignment": "map",
|
|
150
|
+
"text-allow-overlap": true
|
|
151
|
+
},
|
|
152
|
+
paint: {
|
|
153
|
+
"text-color": textColor,
|
|
154
|
+
...text.strokeColor ? {
|
|
155
|
+
"text-halo-color": text.strokeColor,
|
|
156
|
+
"text-halo-width": text.strokeWidth ?? 1
|
|
157
|
+
} : {}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function buildFillPaint(style) {
|
|
162
|
+
return {
|
|
163
|
+
"fill-color": [
|
|
164
|
+
"coalesce",
|
|
165
|
+
[
|
|
166
|
+
"get",
|
|
167
|
+
"fillColor",
|
|
168
|
+
[
|
|
169
|
+
"coalesce",
|
|
170
|
+
["get", "style"],
|
|
171
|
+
["literal", {}]
|
|
172
|
+
]
|
|
173
|
+
],
|
|
174
|
+
style?.fillColor ?? "rgba(0,0,0,0)"
|
|
175
|
+
],
|
|
176
|
+
"fill-opacity": 1
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function buildLinePaint(style) {
|
|
180
|
+
const colorDefault = style?.strokeColor ?? "#000";
|
|
181
|
+
const widthDefault = style?.strokeWidth ?? 2;
|
|
182
|
+
const styleObj = [
|
|
183
|
+
"coalesce",
|
|
184
|
+
["get", "style"],
|
|
185
|
+
["literal", {}]
|
|
186
|
+
];
|
|
187
|
+
return {
|
|
188
|
+
"line-color": [
|
|
189
|
+
"coalesce",
|
|
190
|
+
[
|
|
191
|
+
"get",
|
|
192
|
+
"strokeColor",
|
|
193
|
+
styleObj
|
|
194
|
+
],
|
|
195
|
+
colorDefault
|
|
196
|
+
],
|
|
197
|
+
"line-width": [
|
|
198
|
+
"coalesce",
|
|
199
|
+
[
|
|
200
|
+
"get",
|
|
201
|
+
"strokeWidth",
|
|
202
|
+
styleObj
|
|
203
|
+
],
|
|
204
|
+
widthDefault
|
|
205
|
+
],
|
|
206
|
+
...style?.strokeDash ? { "line-dasharray": style.strokeDash } : {}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
var MapLibreAdapter = class extends BaseMapAdapter {
|
|
210
|
+
map;
|
|
211
|
+
constructor(map) {
|
|
212
|
+
super();
|
|
213
|
+
this.map = map;
|
|
214
|
+
}
|
|
215
|
+
createLayer(layerId, style) {
|
|
216
|
+
const sourceId = `${layerId}-src`;
|
|
217
|
+
this.map.addSource(sourceId, {
|
|
218
|
+
type: "geojson",
|
|
219
|
+
data: emptyFC(),
|
|
220
|
+
promoteId: "_fid"
|
|
221
|
+
});
|
|
222
|
+
const mlLayerIds = [];
|
|
223
|
+
const fillId = `${layerId}-fill`;
|
|
224
|
+
this.map.addLayer({
|
|
225
|
+
id: fillId,
|
|
226
|
+
type: "fill",
|
|
227
|
+
source: sourceId,
|
|
228
|
+
filter: [
|
|
229
|
+
"==",
|
|
230
|
+
"$type",
|
|
231
|
+
"Polygon"
|
|
232
|
+
],
|
|
233
|
+
paint: buildFillPaint(style)
|
|
234
|
+
});
|
|
235
|
+
mlLayerIds.push(fillId);
|
|
236
|
+
const lineId = `${layerId}-line`;
|
|
237
|
+
this.map.addLayer({
|
|
238
|
+
id: lineId,
|
|
239
|
+
type: "line",
|
|
240
|
+
source: sourceId,
|
|
241
|
+
filter: [
|
|
242
|
+
"any",
|
|
243
|
+
[
|
|
244
|
+
"==",
|
|
245
|
+
"$type",
|
|
246
|
+
"LineString"
|
|
247
|
+
],
|
|
248
|
+
[
|
|
249
|
+
"==",
|
|
250
|
+
"$type",
|
|
251
|
+
"Polygon"
|
|
252
|
+
]
|
|
253
|
+
],
|
|
254
|
+
paint: buildLinePaint(style),
|
|
255
|
+
layout: {
|
|
256
|
+
"line-cap": "round",
|
|
257
|
+
"line-join": "round"
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
mlLayerIds.push(lineId);
|
|
261
|
+
const circleId = `${layerId}-circle`;
|
|
262
|
+
this.map.addLayer({
|
|
263
|
+
id: circleId,
|
|
264
|
+
type: "circle",
|
|
265
|
+
source: sourceId,
|
|
266
|
+
filter: buildCirclePointFilter(style),
|
|
267
|
+
paint: buildCirclePaint(style)
|
|
268
|
+
});
|
|
269
|
+
mlLayerIds.push(circleId);
|
|
270
|
+
if (style?.text) {
|
|
271
|
+
const symbolId = `${layerId}-symbol`;
|
|
272
|
+
this.map.addLayer(buildSymbolLayer(symbolId, sourceId, style.text));
|
|
273
|
+
mlLayerIds.push(symbolId);
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
sourceId,
|
|
277
|
+
mlLayerIds,
|
|
278
|
+
style
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
destroyLayer(handle) {
|
|
282
|
+
for (const mlId of handle.mlLayerIds) if (this.map.getLayer(mlId)) this.map.removeLayer(mlId);
|
|
283
|
+
if (this.map.getSource(handle.sourceId)) this.map.removeSource(handle.sourceId);
|
|
284
|
+
}
|
|
285
|
+
applyFeatureOps(layerId, ops) {
|
|
286
|
+
const handle = this.layers.get(layerId);
|
|
287
|
+
if (!handle) return;
|
|
288
|
+
const source = this.map.getSource(handle.sourceId);
|
|
289
|
+
if (!source) return;
|
|
290
|
+
const features = ops.full.map((f) => f.id !== void 0 ? {
|
|
291
|
+
...f,
|
|
292
|
+
properties: {
|
|
293
|
+
...f.properties ?? {},
|
|
294
|
+
_fid: f.id
|
|
295
|
+
}
|
|
296
|
+
} : f);
|
|
297
|
+
source.setData({
|
|
298
|
+
type: "FeatureCollection",
|
|
299
|
+
features
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
attachNativeEvent(type, onEvent) {
|
|
303
|
+
const mlEvent = type === "pointermove" ? "mousemove" : type === "dblclick" ? "dblclick" : "click";
|
|
304
|
+
const fn = (e) => {
|
|
305
|
+
onEvent([e.lngLat.lng, e.lngLat.lat], [e.point.x, e.point.y], e);
|
|
306
|
+
};
|
|
307
|
+
this.map.on(mlEvent, fn);
|
|
308
|
+
return {
|
|
309
|
+
type: mlEvent,
|
|
310
|
+
token: fn
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
detachNativeEvent(entry) {
|
|
314
|
+
this.map.off(entry.type, entry.token);
|
|
315
|
+
}
|
|
316
|
+
attachViewChange(handler) {
|
|
317
|
+
const fn = () => handler();
|
|
318
|
+
this.map.on("moveend", fn);
|
|
319
|
+
this.map.on("zoomend", fn);
|
|
320
|
+
return { fn };
|
|
321
|
+
}
|
|
322
|
+
detachViewChange(token) {
|
|
323
|
+
this.map.off("moveend", token.fn);
|
|
324
|
+
this.map.off("zoomend", token.fn);
|
|
325
|
+
}
|
|
326
|
+
createEditPointerDriver() {
|
|
327
|
+
const map = this.map;
|
|
328
|
+
const hub = new PointerCallbackHub();
|
|
329
|
+
let dragPanWasEnabled = false;
|
|
330
|
+
let disposed = false;
|
|
331
|
+
let touchDragActive = false;
|
|
332
|
+
let multiTouchCancelled = false;
|
|
333
|
+
const TOUCH_INFO = { hitTolerance: TOUCH_HIT_TOLERANCE_PX };
|
|
334
|
+
const preventMapEventDefaults = (e) => {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
e.originalEvent.preventDefault?.();
|
|
337
|
+
e.originalEvent.stopPropagation?.();
|
|
338
|
+
};
|
|
339
|
+
const onDown = (e) => {
|
|
340
|
+
const px = [e.point.x, e.point.y];
|
|
341
|
+
const info = { altKey: e.originalEvent.altKey };
|
|
342
|
+
hub.fireDown(px, info);
|
|
343
|
+
};
|
|
344
|
+
const onMove = (e) => {
|
|
345
|
+
const px = [e.point.x, e.point.y];
|
|
346
|
+
hub.fireMove(px);
|
|
347
|
+
};
|
|
348
|
+
const onUp = (e) => {
|
|
349
|
+
const px = [e.point.x, e.point.y];
|
|
350
|
+
hub.fireUp(px);
|
|
351
|
+
};
|
|
352
|
+
const onTouchStart = (e) => {
|
|
353
|
+
if (e.points.length !== 1) return;
|
|
354
|
+
const before = dragPanWasEnabled;
|
|
355
|
+
const px = [e.point.x, e.point.y];
|
|
356
|
+
hub.fireDown(px, TOUCH_INFO);
|
|
357
|
+
touchDragActive = dragPanWasEnabled !== before;
|
|
358
|
+
if (touchDragActive) preventMapEventDefaults(e);
|
|
359
|
+
};
|
|
360
|
+
const onTouchMove = (e) => {
|
|
361
|
+
const touchCount = e.points.length;
|
|
362
|
+
const px = [e.point.x, e.point.y];
|
|
363
|
+
if (touchCount > 1 && touchDragActive && !multiTouchCancelled) {
|
|
364
|
+
multiTouchCancelled = true;
|
|
365
|
+
hub.fireUp(px, TOUCH_INFO);
|
|
366
|
+
touchDragActive = false;
|
|
367
|
+
preventMapEventDefaults(e);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (touchCount > 1) return;
|
|
371
|
+
if (touchCount === 1) multiTouchCancelled = false;
|
|
372
|
+
hub.fireMove(px, TOUCH_INFO);
|
|
373
|
+
if (touchDragActive) preventMapEventDefaults(e);
|
|
374
|
+
};
|
|
375
|
+
const onTouchEnd = (e) => {
|
|
376
|
+
multiTouchCancelled = false;
|
|
377
|
+
const px = [e.point.x, e.point.y];
|
|
378
|
+
hub.fireUp(px, TOUCH_INFO);
|
|
379
|
+
touchDragActive = false;
|
|
380
|
+
};
|
|
381
|
+
map.on("mousedown", onDown);
|
|
382
|
+
map.on("mousemove", onMove);
|
|
383
|
+
map.on("mouseup", onUp);
|
|
384
|
+
map.on("touchstart", onTouchStart);
|
|
385
|
+
map.on("touchmove", onTouchMove);
|
|
386
|
+
map.on("touchend", onTouchEnd);
|
|
387
|
+
map.on("touchcancel", onTouchEnd);
|
|
388
|
+
return {
|
|
389
|
+
onPointerDown: (cb) => hub.onPointerDown(cb),
|
|
390
|
+
onPointerMove: (cb) => hub.onPointerMove(cb),
|
|
391
|
+
onPointerUp: (cb) => hub.onPointerUp(cb),
|
|
392
|
+
setDragPanEnabled(enabled) {
|
|
393
|
+
if (enabled) {
|
|
394
|
+
if (dragPanWasEnabled) map.dragPan.enable();
|
|
395
|
+
dragPanWasEnabled = false;
|
|
396
|
+
} else {
|
|
397
|
+
dragPanWasEnabled = map.dragPan.isEnabled();
|
|
398
|
+
if (dragPanWasEnabled) map.dragPan.disable();
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
toPixel(lonLat) {
|
|
402
|
+
const p = map.project(lonLat);
|
|
403
|
+
return [p.x, p.y];
|
|
404
|
+
},
|
|
405
|
+
fromPixel(pixel) {
|
|
406
|
+
const ll = map.unproject(pixel);
|
|
407
|
+
return [ll.lng, ll.lat];
|
|
408
|
+
},
|
|
409
|
+
dispose() {
|
|
410
|
+
if (disposed) return;
|
|
411
|
+
disposed = true;
|
|
412
|
+
hub.clear();
|
|
413
|
+
map.off("mousedown", onDown);
|
|
414
|
+
map.off("mousemove", onMove);
|
|
415
|
+
map.off("mouseup", onUp);
|
|
416
|
+
map.off("touchstart", onTouchStart);
|
|
417
|
+
map.off("touchmove", onTouchMove);
|
|
418
|
+
map.off("touchend", onTouchEnd);
|
|
419
|
+
map.off("touchcancel", onTouchEnd);
|
|
420
|
+
if (dragPanWasEnabled) {
|
|
421
|
+
map.dragPan.enable();
|
|
422
|
+
dragPanWasEnabled = false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
setLayerStyle(layerId, style) {
|
|
428
|
+
const handle = this.layers.get(layerId);
|
|
429
|
+
if (!handle) return;
|
|
430
|
+
handle.style = style;
|
|
431
|
+
for (const mlId of handle.mlLayerIds) {
|
|
432
|
+
const layer = this.map.getLayer(mlId);
|
|
433
|
+
if (!layer) continue;
|
|
434
|
+
const type = layer.type;
|
|
435
|
+
if (type === "fill") this.map.setPaintProperty(mlId, "fill-color", buildFillPaint(style)["fill-color"]);
|
|
436
|
+
else if (type === "line") {
|
|
437
|
+
if (style.strokeColor !== void 0 || style.strokeWidth !== void 0) {
|
|
438
|
+
const paint = buildLinePaint(style);
|
|
439
|
+
if (style.strokeColor !== void 0) this.map.setPaintProperty(mlId, "line-color", paint["line-color"]);
|
|
440
|
+
if (style.strokeWidth !== void 0) this.map.setPaintProperty(mlId, "line-width", paint["line-width"]);
|
|
441
|
+
}
|
|
442
|
+
if (style.strokeDash) this.map.setPaintProperty(mlId, "line-dasharray", style.strokeDash);
|
|
443
|
+
} else if (type === "circle") {
|
|
444
|
+
const paint = buildCirclePaint(style);
|
|
445
|
+
this.map.setPaintProperty(mlId, "circle-radius", paint["circle-radius"]);
|
|
446
|
+
this.map.setPaintProperty(mlId, "circle-color", paint["circle-color"]);
|
|
447
|
+
this.map.setPaintProperty(mlId, "circle-stroke-color", paint["circle-stroke-color"]);
|
|
448
|
+
this.map.setPaintProperty(mlId, "circle-stroke-width", paint["circle-stroke-width"]);
|
|
449
|
+
this.map.setFilter(mlId, buildCirclePointFilter(style));
|
|
450
|
+
} else if (type === "symbol" && style.text) {
|
|
451
|
+
if (style.text.text) this.map.setLayoutProperty(mlId, "text-field", style.text.text);
|
|
452
|
+
else if (style.text.labelProperty) this.map.setLayoutProperty(mlId, "text-field", ["get", style.text.labelProperty]);
|
|
453
|
+
if (style.text.fillColor) this.map.setPaintProperty(mlId, "text-color", style.text.fillColor);
|
|
454
|
+
if (style.text.strokeColor) this.map.setPaintProperty(mlId, "text-halo-color", style.text.strokeColor);
|
|
455
|
+
if (style.text.strokeWidth !== void 0) this.map.setPaintProperty(mlId, "text-halo-width", style.text.strokeWidth);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (style.text && !handle.mlLayerIds.some((id) => id.endsWith("-symbol"))) {
|
|
459
|
+
const symbolId = `${layerId}-symbol`;
|
|
460
|
+
this.map.addLayer(buildSymbolLayer(symbolId, handle.sourceId, style.text));
|
|
461
|
+
handle.mlLayerIds.push(symbolId);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
toLonLat(coordinate) {
|
|
465
|
+
return unproject(coordinate[0], coordinate[1]);
|
|
466
|
+
}
|
|
467
|
+
fromLonLat(position) {
|
|
468
|
+
return project(position[0], position[1]);
|
|
469
|
+
}
|
|
470
|
+
getPixelFromCoordinate(position) {
|
|
471
|
+
const point = this.map.project(position);
|
|
472
|
+
return [point.x, point.y];
|
|
473
|
+
}
|
|
474
|
+
getViewportSize() {
|
|
475
|
+
const container = this.map.getContainer();
|
|
476
|
+
return {
|
|
477
|
+
width: container.clientWidth,
|
|
478
|
+
height: container.clientHeight
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
panByPixels(delta, options) {
|
|
482
|
+
const [dx, dy] = delta;
|
|
483
|
+
if (Math.abs(dx) < .5 && Math.abs(dy) < .5) return;
|
|
484
|
+
const center = this.map.getCenter();
|
|
485
|
+
this.map.easeTo({
|
|
486
|
+
center,
|
|
487
|
+
offset: [dx, dy],
|
|
488
|
+
duration: options?.durationMs ?? 240,
|
|
489
|
+
essential: true
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
setCursor(cursor) {
|
|
493
|
+
this.map.getContainer().classList.toggle("cm-map-cursor-crosshair", cursor === "crosshair");
|
|
494
|
+
this.map.getCanvas().style.cursor = cursor;
|
|
495
|
+
this.map.getCanvasContainer().style.cursor = cursor;
|
|
496
|
+
this.map.getContainer().style.cursor = cursor;
|
|
497
|
+
}
|
|
498
|
+
setDoubleClickZoomEnabled(enabled) {
|
|
499
|
+
if (enabled) this.map.doubleClickZoom.enable();
|
|
500
|
+
else this.map.doubleClickZoom.disable();
|
|
501
|
+
}
|
|
502
|
+
getResolution() {
|
|
503
|
+
const zoom = this.map.getZoom();
|
|
504
|
+
const center = this.map.getCenter();
|
|
505
|
+
return 40075016.686 * Math.cos(center.lat * Math.PI / 180) / Math.pow(2, zoom + 8);
|
|
506
|
+
}
|
|
507
|
+
getZoom() {
|
|
508
|
+
return this.map.getZoom();
|
|
509
|
+
}
|
|
510
|
+
onPick(layerId, handler, opts) {
|
|
511
|
+
const onMlClick = (e) => {
|
|
512
|
+
const handle = this.layers.get(layerId);
|
|
513
|
+
if (!handle) return;
|
|
514
|
+
const originalEvent = e.originalEvent;
|
|
515
|
+
const tol = pickHitTolerance(originalEvent);
|
|
516
|
+
const { x, y } = e.point;
|
|
517
|
+
const bbox = [[x - tol, y - tol], [x + tol, y + tol]];
|
|
518
|
+
const features = this.map.queryRenderedFeatures(bbox, { layers: handle.mlLayerIds });
|
|
519
|
+
if (features.length === 0) return;
|
|
520
|
+
const f = features[0];
|
|
521
|
+
const geojson = {
|
|
522
|
+
type: "Feature",
|
|
523
|
+
id: f.id,
|
|
524
|
+
geometry: f.geometry,
|
|
525
|
+
properties: f.properties
|
|
526
|
+
};
|
|
527
|
+
const cmId = tryControlMeasureIdFromFeature(geojson);
|
|
528
|
+
if (cmId == null) return;
|
|
529
|
+
handler({
|
|
530
|
+
id: cmId,
|
|
531
|
+
feature: geojson,
|
|
532
|
+
pixel: [x, y],
|
|
533
|
+
coordinate: [e.lngLat.lng, e.lngLat.lat],
|
|
534
|
+
originalEvent
|
|
535
|
+
});
|
|
536
|
+
};
|
|
537
|
+
return bindAbortable(opts?.signal, () => {
|
|
538
|
+
this.map.on("click", onMlClick);
|
|
539
|
+
return () => this.map.off("click", onMlClick);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
setGlobeMode(enabled) {
|
|
543
|
+
if (enabled) this.map.setProjection({ type: "globe" });
|
|
544
|
+
else this.map.setProjection({ type: "mercator" });
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
//#endregion
|
|
548
|
+
export { MapLibreAdapter };
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orbat-mapper/tactical-draw-adapter-maplibre",
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"author": "Orbat Mapper",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/orbat-mapper/tactical-graphics.git",
|
|
10
|
+
"directory": "packages/tactical-draw-adapter-maplibre"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"tactical-graphics",
|
|
14
|
+
"control-measures",
|
|
15
|
+
"mil-std-2525",
|
|
16
|
+
"app-6",
|
|
17
|
+
"gis",
|
|
18
|
+
"mapping",
|
|
19
|
+
"draw",
|
|
20
|
+
"edit",
|
|
21
|
+
"maplibre",
|
|
22
|
+
"maplibre-gl"
|
|
23
|
+
],
|
|
24
|
+
"homepage": "https://github.com/orbat-mapper/tactical-graphics",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./dist/index.mjs",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@orbat-mapper/control-measures": "^0.2.0-alpha.0",
|
|
41
|
+
"@orbat-mapper/tactical-draw": "^0.2.0-alpha.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"maplibre-gl": ">=4.0.0 <6.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@arethetypeswrong/core": "^0.18.3",
|
|
48
|
+
"@types/geojson": "^7946.0.16",
|
|
49
|
+
"jsdom": "^27.4.0",
|
|
50
|
+
"maplibre-gl": "^5.18.0",
|
|
51
|
+
"publint": "^0.3.21",
|
|
52
|
+
"tsdown": "^0.22.2",
|
|
53
|
+
"typescript": "~5.9.3",
|
|
54
|
+
"vite": "^8.0.16",
|
|
55
|
+
"vitest": "^4.1.8"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsdown",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"lint": "eslint . --fix"
|
|
61
|
+
}
|
|
62
|
+
}
|