🚧 These docs are a work in progress — content is incomplete and may change without notice.
Skip to content

TacticalDraw ​

TacticalDraw ships in @orbat-mapper/tactical-draw — the framework- and engine-agnostic draw/edit core. It wraps @orbat-mapper/control-measures and hides all per-kind draw-rule plumbing behind a single object.

Give it a map adapter and you get seven verbs — render(), draw(), edit(), editMany(), syncTransformGraphics(), onGraphicPick(), and cancel(). Pick an engine package, bring your own copy of that engine, install nothing else.

New here? The Quick start has a live MapLibre demo and minimal code to stand up drawing on your own map.

Adapters

Application hosts install one engine package — @orbat-mapper/tactical-draw-adapter-openlayers, -maplibre, or -leaflet — each extending BaseMapAdapter. Writing your own engine? Implement the MapAdapter contract (see Adapter surface). Supported today: OpenLayers, MapLibre GL, Leaflet, plus bring your own.

Mental model ​

Four ideas explain almost everything the facade does.

  1. You own the data. The host holds the authoritative ControlMeasure[]. TacticalDraw paints that list onto a graphics layer with render(measures). It never mutates your array.
  2. It owns interactions. draw() and edit() return promises. Until one settles, the facade runs preview, guide, handles, pointer, abort, and map-event wiring for you.
  3. One at a time. Starting a new draw or edit preempts the previous one, rejecting it with TacticalDrawAbortError and reason "preempted".
  4. Name a kind, not a rule. You pass a kind like "block". The registry resolves metadata, draw rules, option typing, and the generator. No rule imports in host code.

The verbs at a glance ​

VerbWhat it does
render(measures)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(measures, options?)Transform several measures at once as a group; resolves on close.
syncTransformGraphics(measures, options?)Sync a changing selection to the live group transform, or start one if idle.
onGraphicPick(handler)Subscribe to clicks on committed measures.
cancel()Abort the live draw or edit.

Every interaction follows the same shape: await a facade method, take snapshot.graphic, write it into your list, call render(). Cancellations reject the promise — catch with isTacticalDrawAbortError or ignoreAbort.

render(measures) — paint your list ​

Reconciles the graphics layer against your authoritative list. Call it whenever the list changes. ids must be unique — duplicates throw before anything is drawn.

ts
td.render(measures);

While an interaction is live, render() stays out of the way:

  • 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 & baking

Kinds with pixel-denominated sizes (echelonSizePixels, radiusPixels, toothSizePixels, toothHeightPixels, and the labelSizePixels every text-carrying kind stamps at draw time) are screen-anchored during draw/edit: re-resolved on every layer write and re-rendered on view change to hold a constant on-screen size. On commit they bake to a ground anchor by default — each pixel size resolves to meters at the finishing zoom (label sizes become labelSize meters) and the committed measure becomes static. Pass sizeAnchor: "screen" to draw() / edit() to keep the pixel sizes zoom-aware. Meter-sized measures are never re-rendered on zoom; adapters scale a ground-anchored label's text from its meter height instead, clamped to the layer's textSizeMinPixels/textSizeMaxPixels band (default 8–24 px). Gesture coordinates are rounded to 6 decimals (~0.11 m at the equator).

Cancel & clean up ​

cancel() aborts the live draw or edit with reason "session" and returns whether anything was active. Guard the rejected promise with isTacticalDrawAbortError or swallow it with ignoreAbort. When the map goes away, call destroy().

ts
cancelButton.onclick = () => td.cancel();

const unsubscribe = td.onGraphicPick(onPick);

function dispose() {
  unsubscribe(); // idempotent
  td.destroy(); // aborts with reason "destroyed", removes facade-owned layers
  adapter.destroy();
}

After destroy()

destroy() is safe to call repeatedly. Public methods, including cancel(), throw TacticalDrawDestroyedError once the instance is destroyed. The activeSession getter remains safe and reads null. Host-supplied layer ids are left in place; facade-allocated layers are removed.