@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 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
@@ -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
+ }