Reference
The complete public API. For task-oriented examples, see the recipes.
Constructor and options
const td = new TacticalDraw(adapter, {
generateId: () => crypto.randomUUID(),
graphicsStyle: { color: "#111827", strokeWidth: 2 },
interactionStyle: {
guide: { strokeColor: "#64748b", strokeWidth: 1, strokeDash: [8, 4] },
vertexHandle: { fillColor: "#ffffff", strokeColor: "#2563eb", strokeWidth: 2 },
midpointHandle: { fillColor: "#fbbf24", strokeColor: "#2563eb", strokeWidth: 2 },
},
});| Option | Default | Description |
|---|---|---|
layers | created automatically | Optional application layer IDs for graphics, preview, guide, and handles. Supplied IDs survive destroy(). |
graphicsStyle | { color: "#000000", strokeWidth: 2 } | Lowest-priority default style. Monocolor model: color paints stroke and fill; strokeColor / fillColor are per-channel overrides. |
interactionStyle | built-ins | Unified affordance style. Slots: guide, vertexHandle, midpointHandle, scaleHandle, rotateHandle, boxOutline, plus boxPadding (px, default 0) to inflate the fitted transform box. Per-call overrides merge on top. |
generateId | td-1, td-2, and so on | Used when draw() commits a control measure whose draft had no id. |
Layers
TacticalDraw uses four layer slots. It creates any slot you omit and removes that layer during destroy().
| Layer | Holds |
|---|---|
graphics | Authoritative rendered measures from render(). |
preview | Generated preview geometry during draw/edit. |
guide | Rubber-band polyline and optional area-closing segment. |
handles | Edit handles for reshape (vertex/midpoint) and transform (box outline, scale corners, rotate grip). |
Do not use layerIds
td.layerIds is internal and may change. Select committed graphics with onGraphicPick(), and use ownsInteractionAt to check whether TacticalDraw owns a pixel. Do not query internal layers directly.
Data shapes
TacticalDraw uses the public data types from @orbat-mapper/control-measures. Your application owns the stored ControlMeasure[]; TacticalDraw creates, edits, and renders entries from it.
ControlMeasure<K>
import type { Position } from "geojson";
interface ControlMeasure<K extends ControlMeasureKind = ControlMeasureKind> {
id: string; // stable, unique; prefixes rendered feature ids
kind: K; // selects metadata, rules, option typing, generator
controlPoints: Position[]; // lon/lat order; gestures round to 6 decimals
options?: OptionsByKind[K]; // per-kind generator options
style?: ControlMeasureStyle; // participates in the render cascade
properties?: Record<string, unknown>; // your metadata (not copied onto preview features)
schemaVersion?: 1;
}ControlMeasureStyle
interface ControlMeasureStyle {
color?: string; // Monocolor input. Resolves into stroke and fill.
strokeColor?: string; // per-channel override
strokeWidth?: number;
strokeDash?: number[];
fillColor?: string; // per-channel override
fillPattern?:
| "solid"
| "hatch"
| "reverse-hatch"
| "cross-hatch"
| "horizontal"
| "vertical"
| "dots"; // interior of filled parts
opacity?: number; // graphic opacity multiplier, 0 through 1
textHalo?: boolean; // contrasting halo behind label text
}Before an adapter draws GeoJSON, TacticalDraw resolves the layer style, graphicsStyle, measure style, and generator hints. See Styling for the priority sequence.
opacity is multiplied into each resolved color's alpha. It affects display only; selection and hit-testing still use the color's original alpha. See Graphic opacity.
textHalo adds a contrasting halo only to features with a text field. The renderer chooses the color and writes textHalo and textHaloColor at feature top level for adapters to read. See Text halo.
GraphicSnapshot<G>
interface GraphicSnapshot<G extends Graphic = Graphic> {
graphic: G; // Deep copy. You can retain it.
render: G extends PointSymbol ? PointSymbolRender : ControlMeasureRender;
}TacticalDraw copies graphic with structuredClone, so it remains valid after later session changes. Rendered feature IDs follow ${cmId}:${part}:${index}; recover the parent ID with tryControlMeasureIdFromFeature().
TransformBox
interface TransformBox {
center: PixelCoordinate; // box center, pixel space
halfWidth: number; // half-width along the box's local x-axis (pre-rotation), px
halfHeight: number; // half-height along the box's local y-axis (pre-rotation), px
angle: number; // radians, screen space, positive = clockwise on screen
}This exported type describes the oriented selection box, including its four corner scale handles and rotation handle, without exposing TacticalDraw's internal placement or hit-test geometry.
InteractionHit
interface InteractionHit {
layer: "handles" | "preview" | "graphics"; // topmost match wins
feature: Feature; // The selected feature. Do not change it.
measureId?: string; // set when the feature belongs to a control measure
}td.ownsInteractionAt(pixel, options?) returns this type or null. See Host click handling.
Sessions
Receive a session through onSession or read it from td.activeSession while an interaction is running. The property is null while idle and after the session settles.
DrawSession
| Member | Description |
|---|---|
controlPoints | Live snapshot of committed draw points. |
canCommit | true when the point count is inside the kind's valid range. |
minControlPoints / maxControlPoints | Point-count bounds for the active draw. |
commit() | Commits if possible, returns true; otherwise false. |
abort() | Rejects the draw promise with reason "session". |
onChange(listener) | Subscribes to committed point changes. It does not report rubber-band pointer movements. |
onTransientChange(listener) | Subscribes to live geometry during the draw. It supplies a TransientChangeEvent for each rubber-band pointer movement: the working points, the pointIndex of the live point, and a "start" / "move" / "end" phase. The "end" phase occurs one time. |
onCommit(handler) | Subscribes to the committed snapshot. The handler receives the same object that resolves the draw() promise. It runs synchronously one time. It does not run after an abort. It returns an unsubscribe function. A subscription after settlement has no effect. |
onSettled(handler) | Subscribes to session settlement. It runs one time after commit or abort and supplies a SettleReason. It runs after onCommit and after TacticalDraw clears td.activeSession. A subscription after settlement runs asynchronously one time. |
EditSession
| Member | Description |
|---|---|
measure | Immutable edit-start input. |
controlPoints / options / style | Live working-state snapshots. |
dirty | true after a meaningful mutation (tolerance-based for geometry). |
history | Session-local undo/redo for completed gestures and authored programmatic mutations. Exposes observable state, undo(), redo(), and subscribe(). Both stacks clear when the session settles. |
modes | Active edit modes. |
sizeAnchor | Current size anchor. The "ground" value converts geometry and label pixel sizes to meters at commit. The "screen" value keeps pixel sizes. It applies only to control measures that contain a pixel size. |
canResetGroundSizes | true when the ground-anchored measure has resettable geometry or label sizes and the adapter has a usable current resolution. |
close() | Commits: returns the GraphicSnapshot<ControlMeasure> of the current working state (the same reference the edit promise resolves with) and resolves the promise; undefined after the session has settled. |
abort() | Rejects with reason "session". |
setModes(modes) | Changes active modes. Queued during a drag (latest wins). "delete" collapses to delete-only. |
setOptions(partial) | Shallow-merges options, re-renders preview, marks dirty, emits onChange. |
setStyle(partial) | Shallow-merges style, re-renders preview, marks dirty, emits onChange. |
setSizeAnchor(anchor) | Flips the size anchor, re-renders the emitted snapshot, marks dirty, emits onChange. No-op for measures with no pixel size; the in-flight preview stays screen-locked until commit. |
resetGroundSizes() | Restores all supported sizes to their default on-screen appearance at the current zoom, stores them in meters, and records one undoable change. No-op when canResetGroundSizes is false. |
onChange(listener) | Subscribes to completed gesture changes. |
onTransientChange(listener) | Subscribes to live geometry during a drag. It supplies a TransientChangeEvent for each pointer movement of a vertex, midpoint, or transform-box gesture. pointIndex is null when all points move together. The "end" phase occurs one time, on commit and on cancel, before onChange. |
onCommit(handler) | Subscribes to the committed snapshot. The handler receives the same object that close() returns and that resolves the edit() promise. It runs synchronously one time. It does not run after an abort. It returns an unsubscribe function. A subscription after settlement has no effect. |
onSettled(handler) | Subscribes to session settlement. It runs one time after commit or abort and supplies a SettleReason. It runs after onCommit and after TacticalDraw clears td.activeSession. A subscription after settlement runs asynchronously one time. |
EditChangeEvent
Listeners receive measure, previous, and session. The first two are GraphicSnapshot<ControlMeasure> values. Call reject(), close(), or abort() from the listener. Geometry changes, setOptions(), setStyle(), setSizeAnchor(), and resetGroundSizes() emit events; setModes() does not.
TransformSession
TransformSession is the group counterpart to EditSession, available through editMany or td.activeTransformSession. It applies scale, rotation, and translation to all members, but cannot reshape a member or change one member's options and style. Change membership with setGraphics() and key returned snapshots by snapshot.graphic.id, not array index.
| Member | Description |
|---|---|
measures | Live view of the current members' join-time inputs (a fresh array per read). Grows/shrinks with setGraphics; key snapshots by measure.id, not by index. |
controlPointsFor(id) | Live working control points for the member with that measure id, or undefined if no member carries it. |
dirty | true when any member's working points differ from its edit-start input beyond tolerance. |
history | Session-local undo/redo for completed group transforms. A setGraphics() call establishes a new baseline and clears both stacks; membership changes are not undoable. |
sizeAnchor | Live size anchor, applied group-wide. |
setSizeAnchor(anchor) | Switches the group's size anchor; takes effect on the next snapshot and on close(). |
setGraphics(measures) | Replaces the member set in place and resets session history to the resulting working state. Retained members carry their working state forward; removed members are committed and their exit snapshots returned (and delivered to onCommit); added members join at their input (lifted off the graphics layer if on it). Returns the removed members' GraphicSnapshot<ControlMeasure>[] (empty when none removed), or undefined after the session has settled. setGraphics([]) delegates to close(). Duplicate ids throw TypeError. |
close() | Commits the members in the set at close time. Returns their GraphicSnapshot<ControlMeasure>[]. The editMany promise and onCommit handler receive the same array. Returns undefined after settlement. |
abort() | Rejects with reason "session". |
onChange(listener) | Subscribes to completed group scale, rotation, and translation gestures. It does not report rubber-band pointer movements. |
onCommit(handler) | Subscribes to committed batches of exit snapshots. It reports members that setGraphics() removes and members that are present at close time. It runs synchronously one time for each batch. It does not run after an abort or rejection. It returns an unsubscribe function. A subscription after settlement has no effect. |
onSettled(handler) | Subscribes to session settlement. It runs one time and supplies a SettleReason. It reports "close", an abort reason, or "error". It runs after the close-batch onCommit and after TacticalDraw clears td.activeTransformSession. A subscription after settlement runs asynchronously one time. |
TransformChangeEvent
Listeners receive measures, previous, and session. At emission time, both arrays follow the order of session.graphics. Call reject(), close(), or abort() from the listener, and key members by ID because indexes may change.
Removed members and render()
setGraphics() and close() do not put removed members back on the graphics layer. Save the returned snapshots, then call td.render(). onCommit receives the same arrays as those methods and the editMany() promise. Use onSettled when you also need to observe aborts and the final settlement reason.
Errors and abort reasons
| Error | When |
|---|---|
TacticalDrawAbortError | Normal interaction cancellation. Extends DOMException (name: "AbortError") with a closed reason. Test with isTacticalDrawAbortError(e). |
TacticalDrawDestroyedError | Synchronous throw when a public method is called after destroy(). |
Error | render() throws on duplicate measure ids. |
TypeError | edit() throws synchronously on an unknown mode; editMany() throws on an empty or duplicate-id measure array (before any preemption); TransformSession.setGraphics() and syncTransformGraphics() throw on a duplicate-id array. |
Abort reasons are "escape", "signal", "preempted", "destroyed", "session", and "removed". Session onSettled handlers also receive "close" for a commit and "error" if snapshot creation fails during commit.
try {
await td.draw({ kind: "ambush" }, { signal: abort.signal });
} catch (error) {
if (isTacticalDrawAbortError(error)) {
console.info("draw cancelled:", error.reason);
} else {
throw error;
}
}Adapter surface
Applications normally use a ready-made engine package. To support another engine, extend BaseMapAdapter for shared layer IDs, caching, event tokens, reconciliation, and cleanup, then implement the remaining MapAdapter methods.
| Capability | Methods |
|---|---|
| Vector layers | addVectorLayer, removeLayer, setLayerStyle, setLayerFeatureGroups, getLayerFeatures |
| Map events | on/off for click, pointermove, dblclick |
| Pick events | onPick(layerId, handler, opts?); control-measure hits dispatch the measure id and non-measure features are ignored |
| Projection & view | toLonLat, fromLonLat, getPixelFromCoordinate, getViewportSize, getResolution, getZoom, onViewChange, offViewChange |
| Interaction controls | panByPixels, setCursor, setDoubleClickZoomEnabled, createEditPointerDriver |
| Lifecycle | destroy() |
Feature groups
setLayerFeatureGroups(layerId, groups) replaces a layer with an ordered list of groups. Each group contains one graphic's features and icon resources:
interface FeatureGroup {
id: string;
features: readonly Feature[];
iconResources: readonly IconResource[];
revision?: number; // monotonic content stamp, when the producer supplies one
}The list is the layer's complete desired state, ordered bottom to top. Adapters must preserve it for both rendering and picking.
revision is an optional content stamp. Matching IDs and revisions guarantee identical features and icons, allowing the adapter to skip comparison and upload. A missing revision means “unknown”; the adapter must compare content and must not treat two missing values as equal.
TacticalDraw caches rendered groups by graphic object identity. A new render gets a new stamp; a cache hit keeps the previous one. View changes and point-symbol generation changes clear the cache.
Supply a new object for a changed graphic
Pass render() a new object whenever a graphic changes. Mutating an existing object and reusing its reference leaves the previous cached render on screen.
Pick-before-click ordering
Dispatch onPick and "click" handlers from the same engine event, with pick handlers first. If the engine exposes separate native events, map the ABI "click" to the event used by onPick. Otherwise click-away handling may close an edit before the next graphic is picked, breaking close-then-pick and Shift-click selection. OpenLayers therefore maps ABI "click" to singleclick.
Adapter-author helpers
hitTestFeature(pixel, features, tol, toPixel): Does pixel-space hit tests for all geometry types.pickHitTolerance(originalEvent): Returns 4 px for a mouse or pen and 12 px for touch.TOUCH_HIT_TOLERANCE_PXis the handle constant.tryControlMeasureIdFromFeature(feature): Gets the control-measure ID from a rendered feature ID. Returnsnullif the feature is not part of a control measure.coerceHandleStyle(raw): Reads the handle style fromproperties.style.PointerCallbackHub: Supplies a subscriber registry for anEditPointerDriver.bindAbortable(signal, attach): Connects resource removal to anAbortSignaland a dispose function.nearestWorldCopyLng(lng, refLng)andnearestWorldCopyPosition(position, refLng): Keep a longitude or position continuous with a reference longitude across the antimeridian, allowing adapters to project onto the world copy in the current view.combineWithHostSignal(hostSignal?): Combines the TacticalDraw internal abort controller with an optional hostAbortSignal, returning aCombinedAbort(signal,abort(reason, cause?),dispose()).
Known limitations
Current limitations and their workarounds:
| Edge | Today & workaround |
|---|---|
| Seed semantics for fixed-rule kinds | Draft controlPoints are documented as variable-length seed; fixed-rule kinds have subtler user-vs-canonical-point semantics. |