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

Generate GeoJSON ​

Pass a control-measure kind, a list of points, and any kind-specific options to @orbat-mapper/control-measures. It returns MIL-STD-2525 and APP-6 graphics as GeoJSON, ready for a browser, Node service, test, or SVG pipeline. No map engine or canvas is required.

Install ​

sh
pnpm add @orbat-mapper/control-measures
sh
npm install @orbat-mapper/control-measures

Render a control measure ​

Call renderControlMeasure(measure, options?) with a ControlMeasure:

  • id is a stable identifier for this instance.
  • kind selects the control-measure type.
  • controlPoints contains [longitude, latitude] pairs.
  • options customizes the selected kind.
ts
import { renderControlMeasure } from "@orbat-mapper/control-measures";

const render = renderControlMeasure({
  id: "ambush-1",
  kind: "ambush",
  controlPoints: [
    [-0.871, 49.369],
    [-0.861, 49.369],
    [-0.866, 49.374],
  ],
});

// `render` is a GeoJSON FeatureCollection ready for a map or other renderer.
render.features.forEach((f) => console.log(f.geometry.type));

Each feature includes its graphic part in properties.part and its final paint values in properties.style. Feature IDs follow ${id}:${part}:${index}, so you can match parts across renders and edits.

How many points does a control measure need? ​

Each kind declares the minimum number of coordinates it needs and, where applicable, a maximum. Too few coordinates produce an empty render by default. You can read these limits from the metadata:

ts
import {
  listControlMeasureMetadata,
  getControlMeasureMetadata,
} from "@orbat-mapper/control-measures";

// Every kind, with geometry, point requirements, options, and symbology.
const all = listControlMeasureMetadata();

const ambush = getControlMeasureMetadata("ambush");
ambush.minCoordinates; // 3
ambush.geometry; // "line"

The catalog presents the same metadata for every supported kind.

Options and defaults ​

Most kinds expose options such as arrowhead size, tooth count, or fill. Start with getDefaultOptions, then override only what you need:

ts
import { getDefaultOptions, renderControlMeasure } from "@orbat-mapper/control-measures";

const options = { ...getDefaultOptions("ambush"), arrowheadLengthRatio: 0.25 };

const render = renderControlMeasure({
  id: "ambush-1",
  kind: "ambush",
  controlPoints,
  options,
});

See the catalog for each kind's option names, types, and defaults. Colors and strokes are covered in Styling.

Validation ​

Invalid input—such as too few points or non-finite coordinates—returns an empty render by default. Set validationMode if you prefer a warning or exception:

ts
renderControlMeasure(measure, { validationMode: "warn" }); // "silent" | "warn" | "throw"

Discover and edit pixel/meter sizes ​

Read metadata.sizePairs to discover independent size dimensions. Each pair has a stable id within its kind, a display label, and explicit pixels and meters option keys. For example, FLOT declares radiusPixels / radius, while Destroy declares sizePixels / sizeMeters. Generic C2 Line declares separate echelon and label dimensions. Text's maxSizePixels is a screen-only hide threshold, so it is not a pair.

Build one control per pair. Optional numeric descriptors in metadata.params provide per-unit input hints, but a counterpart may have no descriptor (Text and Minefield are examples). Pair discovery does not depend on those descriptors or on uniformSizing, which additionally describes whole-symbol portrayal and unit-preserving editing.

Use authored options with the shared helpers; do not merge getDefaultOptions into them first. An explicit pixel value wins over an explicit meter value, but a default pixel value must never override authored meters. When both keys are absent, defaultUnit selects the definition's default for that unit. Captured labels use the shared defaultValue instead: 14 px for authoring.

ts
import {
  getControlMeasureMetadata,
  resolveSizePair,
  switchSizePairUnit,
} from "@orbat-mapper/control-measures";

const kind = "destroy";
const authoredOptions = { sizePixels: 80, rotation: 30 };
const pair = getControlMeasureMetadata(kind).sizePairs![0]!;
const request = {
  kind,
  options: authoredOptions,
  dimension: pair.id,
  constructionMetersPerCssPixel: 5,
} as const;

const selected = resolveSizePair(request);
if (selected.status === "resolved") {
  // 80 px, source "authored"; rendering resolves to 400 projected meters.
  console.log(selected.value, selected.unit, selected.source, selected.rendering);
}

const switched = switchSizePairUnit({ ...request, unit: "m" });
if (switched.status === "converted") {
  // Replace the options record: { sizeMeters: 400, rotation: 30 }.
  // sizePixels is deleted, unrelated values are preserved, input is untouched.
  console.log(switched.options);
}

constructionMetersPerCssPixel is Web Mercator projected meters per CSS pixel, matching existing persisted size options and MapAdapter.getResolution(). It is not the render context's true groundMetersPerCssPixel; do not interchange them at nonzero latitudes. The helper performs no map access and does not read a stale options.metersPerPixel value. The caller supplies the current resolution.

resolveSizePair separates the selected size from the input to portrayal:

  • Geometry in pixels resolves to meters when construction resolution is usable. Without it, the generator's explicit/default meter value or declared fallbackMeters applies. Destroy and Minefield fall back to 1000 m.
  • Text and explicit label sizes can retain pixels without resolution. Adapter clamping/hiding and geometry-specific label-placement fallbacks are separate.
  • Omitted captured labels report the 14 px authoring default, but their rendering.status is renderer-default: bare generators have no captured label size. Tactical-draw materializes that default before rendering.

Cross-unit conversion requires finite positive resolution. Missing, zero, negative, NaN, or infinite resolution produces unavailable-resolution. Non-positive/non-finite/non-numeric selected sizes and arithmetic overflow or underflow to zero produce invalid-value; an unrecognized dimension produces unknown-dimension. Failures leave input untouched. Same-unit conversion normalizes competing keys without requiring resolution.

Conversion preserves the underlying size without clamping to input hints or rounding to their steps. Hosts should display converted values outside those hints without silently resizing them. To merge into an edit session instead of replacing a record, explicitly clear both pair keys before applying the result:

ts
if (switched.status === "converted") {
  session.setOptions({
    [pair.pixels]: undefined,
    [pair.meters]: undefined,
    ...switched.options,
  });
}

Unit switching is distinct from reset and from the draw session's size anchor. Pixel input can still bake to meters at commit. Tactical-draw owns that lifecycle; these pure helpers do not change session policy.