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

Recipes ​

Common patterns for connecting TacticalDraw to application UI and state.

Variable-length draw with Done / Cancel buttons ​

For a variable-length kind, wire Done to the active session's commit() and Cancel to td.cancel(). Use onSession to keep the Done button in sync with canCommit.

ts
import { ignoreAbort } from "@orbat-mapper/tactical-draw";

const pending = td.draw(
  { kind: "fortified-area" },
  {
    onSession(s) {
      doneButton.disabled = !s.canCommit;
      s.onChange(() => {
        doneButton.disabled = !s.canCommit; // inside the kind's valid range?
      });
    },
  },
);

doneButton.onclick = () => {
  const session = td.activeSession;
  if (session && "canCommit" in session) session.commit();
};
cancelButton.onclick = () => td.cancel();

void pending
  .then(({ graphic }) => {
    measures.push(graphic);
    td.render(measures);
  })
  .catch(ignoreAbort);

Show a live radius while the user draws a circle ​

onTransientChange reports working geometry for each pointer movement. Use haversineDistance to calculate the great-circle radius in meters. The circle generator itself builds the ring in projected Web Mercator meters, so ground distance is smaller than projected distance at high latitudes.

Place the readout with getPixelFromCoordinate and hide it when phase is "end", which occurs on both commit and cancel.

ts
import { haversineDistance } from "@orbat-mapper/control-measures";

await td.draw(
  { kind: "circle" },
  {
    onSession(session) {
      session.onTransientChange((event) => {
        if (event.phase === "end" || event.points.length < 2) {
          hideReadout();
          return;
        }
        const meters = haversineDistance(event.points[0], event.points[1]);
        const pixel = adapter.getPixelFromCoordinate(event.points[event.pointIndex ?? 1]);
        if (pixel) showReadout(pixel, meters);
      });
    },
  },
);

The same subscription on an EditSession follows the handle being dragged.

Validate edits and reject invalid changes ​

onChange runs after each completed gesture. Call event.reject() to restore the previous state, close() to accept and finish, or abort() to discard the session. TacticalDraw applies the result after all listeners have run.

ts
await td.edit(measure, {
  onSession(session) {
    session.onChange((event) => {
      if (!isInsideAllowedArea(event.graphic.graphic.controlPoints)) {
        event.reject(); // bounce the gesture, keep editing
      }
    });
  },
});

Drive style & options from UI controls ​

During an edit, session.setStyle() and session.setOptions() shallow-merge changes into the working copy, rerender the preview, mark the session dirty, and emit onChange.

ts
import { ignoreAbort } from "@orbat-mapper/tactical-draw";

const pending = td.edit(measure);

strokeWidthInput.oninput = () => {
  const session = td.activeSession;
  if (session && "setStyle" in session) {
    session.setStyle({ strokeWidth: Number(strokeWidthInput.value) });
  }
};

smoothToggle.onchange = () => {
  const session = td.activeSession;
  if (session && "setOptions" in session) {
    session.setOptions({ smooth: smoothToggle.checked });
  }
};

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

void pending
  .then(({ graphic: next }) => {
    const i = measures.findIndex((m) => m.id === next.id);
    if (i !== -1) measures.splice(i, 1, next);
    td.render(measures);
  })
  .catch(ignoreAbort);