TacticalDraw ​
@orbat-mapper/tactical-draw adds interactive drawing and editing to @orbat-mapper/control-measures. Its API is independent of frameworks and map engines, and it applies the correct draw rule for each control-measure kind.
Create it with a map adapter, then call render(), draw(), edit(), or the other methods below. You only install the adapter and engine your app uses.
The Quick start walks through a complete MapLibre setup.
Adapters
Choose one engine package: @orbat-mapper/tactical-draw-adapter-openlayers, -maplibre, or -leaflet. Each extends BaseMapAdapter. For another engine, implement the MapAdapter interface.
Operating principles ​
Four rules shape the API:
- Your app owns the data. Keep the authoritative
Graphic[]and pass it torender(). TacticalDraw never mutates the array. - TacticalDraw owns active interactions. While a
draw()oredit()promise is pending, it manages the preview, guide, handles, pointer, abort behavior, and relevant map events. - Only one interaction can be active. Starting a new draw or edit aborts the previous one with a
TacticalDrawAbortErrorthat has the reason"preempted". - A kind selects the behavior. Pass a kind such as
"block"; TacticalDraw finds its metadata, option type, draw rule, and generator automatically.
The verbs at a glance ​
| Verb | What it does |
|---|---|
render(graphics) | Reconcile the graphics layer against your authoritative list. |
draw(draft, options?) | Create a control measure interactively; resolves on commit. |
edit(measure, options?) | Reshape or transform (scale/rotate/move) a control measure; resolves on close. |
editMany(graphics, options?) | Transform several graphics at once as a group; resolves on close. |
syncTransformGraphics(measures, options?) | Synchronize a selection with a group transform. Start a transform if necessary. |
onGraphicPick(handler) | Subscribe to clicks on committed measures. |
cancel() | Abort the live draw or edit. |
Await an interaction, save snapshot.graphic to your list, then call render(). Cancellations reject the promise; handle them with isTacticalDrawAbortError or ignoreAbort.
Render the list ​
render(measures) reconciles the map with your authoritative list. Call it after any list change. IDs must be unique; duplicates throw before the layer is updated.
td.render(measures);During an active interaction, render() uses these rules:
- During a draw, only the graphics layer is touched; the preview is left intact.
- During an edit, the edited measure is excluded from graphics (the preview owns its working copy).
- If the edited measure disappears from the next list, the edit promise rejects with reason
"removed". - If it stays but its style changes, the edit preview picks up the new style.
Pixel-sized symbols, zoom, and baking
Pixel-sized geometry and labels are screen-anchored during drawing and editing, so they retain a constant on-screen size as the view changes. On commit, they bake to meters by default using the finishing zoom. Pass sizeAnchor: "screen" to draw() or edit() to keep them zoom-aware.
Meter-sized geometry does not rerender on zoom. Adapters scale ground-anchored label text from its meter height and clamp it to the layer's textSizeMinPixels/textSizeMaxPixels range (8–24 px by default). Gesture coordinates are rounded to six decimals, or roughly 0.11 m at the equator.
Cancel and remove resources ​
cancel() aborts the current draw or edit with reason "session" and returns whether anything was active. Call destroy() before removing the map.
cancelButton.onclick = () => td.cancel();
const unsubscribe = td.onGraphicPick(onPick);
function dispose() {
unsubscribe(); // idempotent
td.destroy(); // aborts with reason "destroyed" and removes owned layers
adapter.destroy();
}After destroy()
destroy() is idempotent. Afterward, public methods such as cancel() throw TacticalDrawDestroyedError, while activeSession remains null. TacticalDraw removes layers it created but leaves host-supplied layers intact.