Pixel and meter sizes ​
Some dimensions of a control measure do not come from its control points: a FLOT's scallop radius, a Destroy symbol's size, or a label's height. Each of these can be set in pixels (fixed on screen) or meters (fixed on the ground). This page is for editors that let users view and switch those units. If you only render graphics, you can skip it.
Size pairs ​
Each independent dimension is a size pair: two option keys, one per unit. Read them from metadata.sizePairs:
import { getControlMeasureMetadata } from "@orbat-mapper/control-measures";
getControlMeasureMetadata("flot").sizePairs;
// [
// { id: "radius", label: "Radius", pixels: "radiusPixels", meters: "radius",
// defaultUnit: "m", rendering: "geometry" },
// { id: "label", label: "Label size", pixels: "labelSizePixels", meters: "labelSize",
// defaultUnit: "px", defaultValue: 14, rendering: "text", captureOnDraw: true },
// ]| Field | Meaning |
|---|---|
id | Stable identifier within the kind, such as "radius" or "label". |
label | Display name for a form control. |
pixels | Option key for the pixel value. |
meters | Option key for the meter value. |
defaultUnit | Unit used when neither key is set. |
defaultValue | Shared authoring default, used by captured labels (14 px). |
rendering | "geometry" needs a map resolution to draw pixels; "text" does not. |
fallbackMeters | Meter size used when pixels cannot be resolved (Destroy uses 1000 m). |
captureOnDraw | TacticalDraw stores a default for this dimension when a draw commits. |
Build one control per pair. A kind may have several pairs; Generic C2 Line, for example, has separate echelon and label sizes. Text's maxSizePixels is a screen-only hide threshold, not a pair.
metadata.params may hold input hints (min, max, step) for either key, but not every key has one. Do not rely on params or uniformSizing to find pairs; sizePairs is the complete list.
Read the current size ​
resolveSizePair tells you which value applies and what it renders as:
import { getControlMeasureMetadata, resolveSizePair } from "@orbat-mapper/control-measures";
const kind = "destroy";
const pair = getControlMeasureMetadata(kind).sizePairs![0]!;
const selected = resolveSizePair({
kind,
options: { sizePixels: 80, rotation: 30 }, // the authored options
dimension: pair.id,
constructionMetersPerCssPixel: adapter.getResolution(),
});
if (selected.status === "resolved") {
selected.value; // 80
selected.unit; // "px"
selected.source; // "authored"
selected.rendering; // at 5 m/px: { status: "resolved", unit: "m", value: 400, source: "selected" }
}Pass the authored options
Do not merge getDefaultOptions() into the options first. The helpers need to know which values the user actually set. An explicit pixel value wins over an explicit meter value, but a default pixel value must never override authored meters. When neither key is set, defaultUnit picks the default.
What rendering reports depends on the dimension:
- Geometry in pixels resolves to meters when a usable resolution is supplied. Without one, the explicit or default meter value applies, then
fallbackMeters. - Text sizes can stay in pixels without a resolution. Adapter clamping and hiding happen later and are not reported here.
- An omitted captured label reports the 14 px authoring default, but its
rendering.statusis"renderer-default": the bare generator has no captured size. TacticalDraw stores the default before rendering.
Switch units ​
switchSizePairUnit converts a pair to the other unit and returns a new options record:
import { switchSizePairUnit } from "@orbat-mapper/control-measures";
const switched = switchSizePairUnit({
kind,
options: { sizePixels: 80, rotation: 30 },
dimension: pair.id,
constructionMetersPerCssPixel: 5,
unit: "m",
});
if (switched.status === "converted") {
switched.options; // { sizeMeters: 400, rotation: 30 }
}The result removes the other key, keeps unrelated options, and never mutates the input. It preserves the exact size: it does not clamp to input hints or round to their step. Show converted values as they are, even outside a slider's range.
To apply the result to a TacticalDraw edit session, which merges rather than replaces, clear both keys explicitly:
if (switched.status === "converted") {
session.setOptions({
[pair.pixels]: undefined,
[pair.meters]: undefined,
...switched.options,
});
}Failures ​
Both helpers return a status instead of throwing. On failure the input is unchanged. Only switchSizePairUnit reports unavailable-resolution; resolveSizePair falls back to meters instead.
| Status | Cause |
|---|---|
unavailable-resolution | Cross-unit conversion without a finite, positive resolution. |
invalid-value | The size is not a positive finite number, or conversion over/underflows. |
unknown-dimension | dimension is not one of the kind's pair IDs. |
Converting to the unit already in use needs no resolution; it only removes a competing key.
Resolution units ​
constructionMetersPerCssPixel is Web Mercator projected meters per CSS pixel. This matches stored size options and MapAdapter.getResolution(). It is not true ground distance, which is smaller away from the equator. The helpers never read the map or a stale options.metersPerPixel; always pass the current resolution.
Units versus anchors ​
Switching units is not the same as a reset, or as TacticalDraw's size anchor. A pixel value can still be baked to meters when a draw or edit commits. TacticalDraw owns that lifecycle; these helpers never change it.