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:
- 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 methods at a glance ​
| Method | What 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.
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 withinlabelSizeBand, 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.
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.