๐Ÿšง These docs are a work in progress โ€” content is incomplete and may change without notice.
Skip to content

Reference โ€‹

The complete public API. For task-oriented examples, see the recipes.

Constructor and options โ€‹

ts
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 },
  },
});
OptionDefaultDescription
layerscreated automaticallyOptional application layer IDs for graphics, highlight, preview, guide, and handles. Supplied IDs survive destroy().
graphicsStyle{ strokeWidth: 2 }Lowest-priority default style for every control measure. Monocolor model: color paints stroke and fill; strokeColor / fillColor are per-channel overrides. Without a color, each kind's doctrinal default applies.
symbolColorsdoctrinalDoctrinal default colors, such as { obstacle: MIL_STD_GREEN.dark }. See Doctrinal color defaults.
interactionStylebuilt-insUnified affordance style. Slots: guide, vertexHandle, midpointHandle, optionHandle, unsetOptionHandle, geometryHandle, labelHandle, scaleHandle, rotateHandle, boxOutline, highlightOutline, plus boxPadding (px, default 0). Per-call overrides merge on top.
generateIdtd-1, td-2, and so onUsed when draw() commits a graphic whose draft had no id.
snappingoffSnapping configuration. See Snapping.
pointSymbolsnoneRenderer capability for point symbols. See Optional point symbols.
imagesnoneResource resolver for Images. See Host-backed Images.
offsetDragModifierfalseKey that turns a point-symbol body drag into an offset drag: "alt", "shift", "ctrl", "meta", or false.
editModifierssee EditKeys for the edit gestures: modifierDrag (Ctrl or โŒ˜), deleteOrReset (Alt), constrain (Shift), extendPartSelection (off). See Modifier keys.

Layers โ€‹

TacticalDraw uses five layer slots, listed bottom to top. It creates any slot you omit and removes that layer during destroy().

LayerHolds
graphicsAuthoritative rendered graphics from render().
highlightPassive outlines from setHighlightedGraphics().
previewGenerated preview geometry during draw/edit.
guideRubber-band polyline and optional area-closing segment.
handlesEdit 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.

Methods โ€‹

MethodDescription
render(graphics)Reconciles the graphics layer with your list. Throws on duplicate IDs.
draw(draft, options?)Starts a draw. See Draw.
edit(graphic, options?)Starts an edit of one graphic. See Edit.
editMany(graphics, options?)Starts a group transform. See Multi-select.
syncTransformGraphics(graphics, options?)Updates or starts a group transform to match a selection.
cancel(reason?)Aborts the active interaction. Returns whether one was active.
onGraphicPick(handler, options?)Subscribes to clicks on committed graphics. Returns an unsubscribe function.
onPick(layerId, handler, options?)Subscribes to clicks on features in one of your own layers.
ownsInteractionAt(pixel, options?)Returns an InteractionHit if TacticalDraw owns the pixel, otherwise null.
consumedPointerCycle()Returns whether the active edit kept the current click. See Host click handling.
setHighlightedGraphics(ids)Replaces the set of passively outlined graphics. See Selection highlights.
setSnappingOptions(options?)Replaces the snapping configuration. See Snapping.
setOffsetDragModifier(modifier)Changes the point-symbol offset-drag key, including for an active edit.
setEditModifiers(modifiers)Changes the edit gesture keys, including for an active edit. See Modifier keys.
getEditModifiers()Returns the edit gesture keys with every role filled in.
setPinPresentation(defaults) and relatedConfigure MapLibre billboard pins. See Billboard pins.
destroy()Aborts any interaction and removes owned layers and listeners. Idempotent.
PropertyDescription
activeSessionThe live DrawSession, EditSession, or GraphicEditSession, or null.
activeTransformSessionThe live TransformSession, or null.
pinsThe billboard-pin settings store. See Save settings.

Data shapes โ€‹

TacticalDraw uses the public data types from @orbat-mapper/control-measures. Your application owns the stored Graphic[]; TacticalDraw creates, edits, and renders entries from it. A Graphic is a ControlMeasure, a PointSymbol, or an ImageGraphic.

ControlMeasure<K> โ€‹

ts
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)
  textAmplifiers?: TextAmplifiers; // doctrinal text fields, e.g. { T: "BLUE" }
  amplifierPlacements?: AmplifierPlacements; // user-adjusted label positions/rotations
  schemaVersion?: 1;
}

See Labels and text amplifiers for the last two fields.

ControlMeasureStyle โ€‹

ts
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> โ€‹

ts
interface GraphicSnapshot<G extends Graphic = Graphic> {
  graphic: G; // Deep copy. You can retain it.
  render: G extends PointSymbol
    ? PointSymbolRender
    : G extends ImageGraphic
      ? ImageRender
      : 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 โ€‹

ts
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 โ€‹

ts
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 โ€‹

MemberDescription
controlPointsLive snapshot of committed draw points.
canCommittrue when the point count is inside the kind's valid range.
minControlPoints / maxControlPointsPoint-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 โ€‹

The session for edit() on a control measure.

MemberDescription
graphicImmutable edit-start input.
workingGraphicLive working copy, as a snapshot.
controlPoints / options / styleLive working-state snapshots.
textAmplifiers / amplifierPlacementsLive working text fields and label placements.
dirtytrue after a meaningful mutation (tolerance-based for geometry).
historySession-local undo/redo for completed gestures and programmatic mutations. Exposes observable state (canUndo, canRedo), undo(), redo(), and subscribe(). Both stacks clear when the session settles.
modesActive edit modes.
detailHandlesWhether reshape mode shows detail handles, such as the width grips on variable-width arrows.
selectedPartThe primary selected part of a graphic made of parts, such as the index of the Flow Arrow's selected flow. undefined for other graphics.
selectedPartsEvery selected part, selectedPart included, ascending. Reshape mode shows the grips of each. Empty for graphics without parts.
sizeAnchorCurrent 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.
canResetGroundSizestrue 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.
setDetailHandles(visible)Shows or hides the detail handles. View state: no change event, no undo step.
selectPart(part, { extend? })Selects a part alone, or with extend toggles it in or out of selectedParts. View state: no change event, no undo step. No-op for a part the graphic doesn't have.
selectParts(parts, primary?)Replaces selectedParts; primary (or else the last listed part) becomes selectedPart. View state, like selectPart().
onPartSelect(listener)Calls listener(part, parts) once per change to the selection, from the pointer, selectPart(), selectParts(), or the reconcile after an edit or undo.
setOptions(partial)Shallow-merges options.
setStyle(partial)Shallow-merges style.
setTextAmplifiers(partial)Merges text amplifiers. undefined or an empty string removes a field.
setAmplifierPlacements(partial)Merges label placements field by field. undefined removes an entry.
setSizeAnchor(anchor, options?)Changes the size anchor, optionally merging a size-option patch in the same undo step. 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.
updateGraphic(updater)Replaces the working graphic with updater(current) in one step. id and kind are preserved.
onChange(listener)Subscribes to completed changes: gestures, undo/redo, and programmatic mutations.
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.

All mutating methods rerender the preview, mark the session dirty, emit onChange, and record one undo step. They do nothing after the session has settled.

EditChangeEvent

Listeners receive graphic (the state after the change), previous, and session. The first two are GraphicSnapshot<ControlMeasure> values. source says what caused the change: "gesture", "undo", "redo", or "programmatic". Call reject(), close(), or abort() from the listener. Every mutation emits an event; setModes(), setDetailHandles(), selectPart() and selectParts() do not.

GraphicEditSession โ€‹

The session for edit() on a point symbol or an Image. It shares the lifecycle of EditSession but has no control points, options, or text methods.

MemberDescription
graphic / workingGraphicEdit-start input and live working copy.
dirty / history / modesAs on EditSession.
offsetPoint symbols only: offset commands (setPlacement, setConnector, align, reset). See Offset placement.
setModes(modes)Changes active modes.
updateGraphic(updater)Replaces the working graphic in one undoable step.
close() / abort()Commit or abort, as on EditSession.
onChange / onCommit / onSettledAs on EditSession.

To tell sessions apart in a shared handler, check for a member: only EditSession has setOptions, and only DrawSession has canCommit.

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.

MemberDescription
graphicsLive view of the current members' join-time inputs (a fresh array per read). Grows/shrinks with setGraphics; key snapshots by graphic.id, not by index.
workingGraphicsLive working copies of the current members, in the same order as graphics.
controlPointsFor(id)Live working control points for the member with that graphic id, or undefined if no member carries it.
dirtytrue when any member's working points differ from its edit-start input beyond tolerance.
historySession-local undo/redo for completed group transforms. A setGraphics() call establishes a new baseline and clears both stacks; membership changes are not undoable.
sizeAnchorLive size anchor, applied group-wide.
setSizeAnchor(anchor)Switches the group's size anchor; takes effect on the next snapshot and on close().
setGraphics(graphics)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[] (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[]. 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 graphics, previous, source, 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 โ€‹

ErrorWhen
TacticalDrawAbortErrorNormal interaction cancellation. Extends DOMException (name: "AbortError") with a closed reason. Test with isTacticalDrawAbortError(e).
TacticalDrawDestroyedErrorSynchronous throw when a public method is called after destroy().
PointSymbolCapabilityRequiredErrorDrawing or editing a point symbol without a pointSymbols capability.
ImageCapabilityRequiredErrorDrawing or editing an Image without an images capability.
Errorrender() throws on duplicate graphic ids.
TypeErroredit() 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.

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

CapabilityMethods
Vector layersaddVectorLayer, removeLayer, setLayerStyle, setLayerFeatureGroups, getLayerFeatures
Map eventson/off for click, pointermove, dblclick
Pick eventsonPick(layerId, handler, opts?); control-measure hits dispatch the measure id and non-measure features are ignored
Projection & viewtoLonLat, fromLonLat, getPixelFromCoordinate, getCoordinateFromPixel (optional), getViewportSize, getResolution, getZoom, getBearing, onViewChange, offViewChange
Interaction controlspanByPixels, setCursor, setDoubleClickZoomEnabled, createEditPointerDriver
Billboard pinsOptional: getPinPresentationCapability, onPinPresentationStatusChange, getPinFootprint, pickPin, movePinFromPixel
Lifecycledestroy()

getResolution() returns Web Mercator meters per CSS pixel at the view center, without a latitude correction. Generators build pixel-sized geometry in the same units.

Feature groups โ€‹

setLayerFeatureGroups(layerId, groups) replaces a layer with an ordered list of groups. Each group contains one graphic's features and icon resources:

ts
interface FeatureGroup {
  id: string;
  features: readonly Feature[];
  iconResources: readonly IconResource[];
  imageOverlays?: readonly ImageOverlayResource[]; // four-corner rasters (Images)
  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_PX is the handle constant.
  • tryControlMeasureIdFromFeature(feature): Gets the control-measure ID from a rendered feature ID. Returns null if the feature is not part of a control measure.
  • coerceHandleStyle(raw): Reads the handle style from properties.style.
  • PointerCallbackHub: Supplies a subscriber registry for an EditPointerDriver.
  • bindAbortable(signal, attach): Connects resource removal to an AbortSignal and a dispose function.
  • nearestWorldCopyLng(lng, refLng) and nearestWorldCopyPosition(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 host AbortSignal, returning a CombinedAbort (signal, abort(reason, cause?), dispose()).

Conformance tests โ€‹

@orbat-mapper/tactical-draw/testing exports the Vitest suites that the built-in adapters pass. Call them from a spec file with a name and a factory that wraps your adapter:

ts
import {
  runFeatureGroupConformance,
  runGroupedPickConformance,
} from "@orbat-mapper/tactical-draw/testing";

runFeatureGroupConformance("MyEngineAdapter", () => ({
  adapter,
  layerId,
  // Engine probes: feature ids in paint order, icon/style checks, mutation countsโ€ฆ
  engineFeatureIds: () => readEngineFeatureIds(),
  // โ€ฆsee FeatureGroupConformanceSubject for the full list.
}));

runGroupedPickConformance("MyEngineAdapter", () => ({ adapter, layerId, pickAt }));

runFeatureGroupConformance checks paint order, reconciliation, and revision handling for setLayerFeatureGroups. runGroupedPickConformance checks that picking follows the same order even when the engine's native hit order disagrees. The subject types are exported alongside the suites.

Known limitations โ€‹

Current limitations and their workarounds:

EdgeToday & workaround
Seed semantics for fixed-rule kindsDraft controlPoints are documented as variable-length seed; fixed-rule kinds have subtler user-vs-canonical-point semantics.