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

Labels and text amplifiers ​

Many control measures carry text: a phase line's designation, a boundary's unit and country codes, or a free-standing Text graphic. You supply that text as text amplifiers, and the renderer places it according to doctrine.

Set text amplifiers ​

Text amplifiers are the free-text fields from MIL-STD-2525E Appendix L, Table L-II. Put them on the control measure's textAmplifiers field:

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

const render = renderControlMeasure({
  id: "pl-blue",
  kind: "phase-line",
  controlPoints: [
    [10.0, 59.0],
    [10.1, 59.01],
  ],
  textAmplifiers: { T: "BLUE" },
});

// Two Point features with `properties.text === "PL BLUE"`, one at each end.

Label features are Point features with a text property. They also carry rotation (radians) and textAnchor, which every TacticalDraw adapter already understands.

A label that sits just above or below a line, such as a Light Line's "LL", a Generic C2 line's H/T and W/W1 rows, or a Boundary's unit designations, is placed on the line (or on its echelon glyph's edge). Two properties offset the text from there, in multiples of the rendered font size:

  • textAnchorGap moves the text away from the point in the direction it grows from textAnchor. 0.3 starts the text just inside the tip.
  • textBaselineShift moves the text along its own up axis. 0.6 raises it clear of the line, -0.6 lowers it.

Apply them in screen space after rotating the text, so the label stays the same distance from the line, and from its end, at every zoom. labelTextOffsetEms(properties) returns both as [right, down] ems in the text's frame, or undefined when the label has no offset. That is MapLibre's text-offset as is. Multiply it by the font size for OpenLayers' offsetX/offsetY, or use it as a translate after the rotate in SVG or CSS. "Above" is the top of the text as it reads, whichever way the line was drawn.

Field codes and aliases ​

Keys may be the doctrinal field code or a milsymbol-style alias. You can mix the two forms:

CodeAliasMeaning
CquantityQuantity
HadditionalInformationAdditional information
NhostileHostile (ENY) marker
TuniqueDesignationUnique designation
VtypeEquipment/type
WdtgDate-time group / O/O
XaltitudeDepthAltitude/depth
YlocationLocation
AMdistanceDistance, range, radius
ANazimuthAzimuth
APtargetNumberTarget number
AScountryCountry code

Where doctrine repeats a field, add a 1 or 2 suffix: T1, AS1, uniqueDesignation1, country1. The same table is exported as TEXT_AMPLIFIER_FIELDS.

The renderer normalizes the record before use:

  • Aliases resolve to their code, so country1 becomes AS1.
  • Values are trimmed, and empty values are dropped.
  • Unknown keys are ignored.
  • If a code and its alias are both present, the code wins, even when its value is empty. Set T: "" to clear text that came from uniqueDesignation.

Call normalizeTextAmplifiers(record) to get the same canonical record, or canonicalTextAmplifierKey("uniqueDesignation1") to resolve a single key ("T1").

Graphic amplifiers such as echelon, direction of movement, and offset location are not text amplifiers. They are geometry, so they live in the kind's options.

Discover which fields a kind accepts ​

Each kind lists its fields in metadata.textAmplifiers. Use it to build an editor form instead of hard-coding fields per kind:

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

for (const field of getControlMeasureMetadata("phase-line").textAmplifiers ?? []) {
  // { key: "T", label: "Unique designation", placeholder: "ECHO", maxLength: 21, ... }
  addTextInput(field.key, field.label, field);
}

maxLength is an editor hint only; the renderer never truncates text. A kind without textAmplifiers has no doctrinal text fields. The catalog lists each kind's fields.

Label size ​

Labels have a size pair, like other independent dimensions: labelSize in meters or labelSizePixels in pixels. Meter-sized labels scale with the map. They stop growing at 24 px and fade out below 4 px by default, so a zoomed-out label never spills outside its area. Pixel-sized labels stay fixed on screen. When an area shrinks too far to hold them, TacticalDraw shrinks them with the area and then fades them out, like meter-sized labels. A label you have moved inside the area does the same where it now sits; one moved outside the area keeps its size. Pass context: { labelFit: "shrink" } (or "hide") to renderControlMeasure for the same behavior. See Sizing and Pixel and meter sizes.

To keep labels readable over imagery, set style.textHalo.

Move and rotate labels ​

amplifierPlacements stores user adjustments to label position and rotation. Each entry overrides the doctrinal placement of one label:

ts
const adjusted = {
  ...measure,
  amplifierPlacements: {
    "label:label:0": { position: [10.02, 59.015], rotation: 0 },
  },
};

Key each entry by the label's properties.labelPlacementKey from the render. A label that shows exactly one amplifier field may use that field's code ("T", "AS1"). Composite labels, such as a boundary's 1-1 (US), use a label: key derived from their part and order, as in the example above. Always read the key from the render rather than guessing it.

An entry may set position, rotation (radians), or both. A bare [lon, lat] position is also accepted as shorthand for { position }. Invalid values are ignored. Remove an entry to restore the doctrinal placement.

You rarely write these by hand. In TacticalDraw, the "labeldrag" edit mode lets users drag and rotate labels and stores the result here.