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

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.

What it can draw ​

TacticalDraw works with graphics. Every graphic has an id and a kind, and all kinds share the same render, draw, edit, pick, and group-transform API:

  • Control measures: every kind in the catalog.
  • Point symbols: MIL-STD-2525 and APP-6 unit symbols, rendered by an optional capability. See Optional point symbols.
  • Images: georeferenced raster overlays supplied by your app. See Host-backed Images.

Operating principles ​

Four rules shape the API:

  1. Your app owns the data. Keep the authoritative Graphic[] and pass it to render(). TacticalDraw never mutates the array.
  2. TacticalDraw owns active interactions. While a draw() or edit() promise is pending, it manages the preview, guide, handles, pointer, abort behavior, and relevant map events.
  3. Only one interaction can be active. Starting a new draw or edit aborts the previous one with a TacticalDrawAbortError that has the reason "preempted".
  4. A kind selects the behavior. Pass a kind such as "block"; TacticalDraw finds its metadata, option type, draw rule, and generator automatically.

The methods at a glance ​

MethodWhat it does
render(graphics)Reconcile the graphics layer against your authoritative list.
draw(draft, options?)Create a graphic interactively; resolves on commit.
edit(graphic, options?)Reshape or transform (scale/rotate/move) one graphic; resolves on close.
editMany(graphics, options?)Transform several graphics at once as a group; resolves on close.
syncTransformGraphics(graphics, options?)Synchronize a selection with a group transform. Start a transform if necessary.
onGraphicPick(handler)Subscribe to clicks on committed graphics.
setHighlightedGraphics(ids)Outline graphics as selected without starting an edit.
setSnappingOptions(options)Turn snapping on or off, or change its targets.
cancel()Abort the live draw or edit.
destroy()Abort any interaction and remove TacticalDraw's layers and listeners.

Await an interaction, save snapshot.graphic to your list, then call render(). Cancellations reject the promise; handle them with isTacticalDrawAbortError or ignoreAbort.

The Reference lists every option, session member, and error.

Render the list ​

render(graphics) reconciles the map with your authoritative list. Call it after any list change. IDs must be unique; duplicates throw before the layer is updated.

ts
td.render(graphics);

The list order is the paint order, bottom to top. TacticalDraw caches each graphic's render by object identity, so pass a new object whenever a graphic changes. A mutated object with the same reference keeps its old render.

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 graphic is excluded from the graphics layer (the preview owns its working copy).
  • If the edited graphic 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 keep 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. See Sizing.

Meter-sized geometry does not rerender on zoom. Adapters scale ground-anchored label text from its meter height within the layer's textSizeMinPixels/textSizeMaxPixels range (4–24 px by default). Text stops growing at the maximum and fades out below the minimum. Change the range with the labelSizeBand constructor option, for example new TacticalDraw(adapter, { labelSizeBand: { textSizeMinPixels: 0 } }) to never hide labels.

Screen-anchored area labels keep their pixel size, so zooming out eventually leaves them no room. The labelFit constructor option decides what happens then:

  • "shrink" (default): the label shrinks with its area, then fades out within labelSizeBand, like a meter-sized label.
  • "hide": the rows that don't fit are left out, lowest priority first: the W1 and W dates, then H, then the name.
  • "overflow": the label always renders at its pixel size.

The graphic being edited always shows every row at its pixel size. 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.

ts
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.