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

Styling ​

Control measures normally use one doctrinal color for every stroke and fill. Set it with ControlMeasureStyle, then override individual paint channels only when needed. The renderer writes the final values to properties.style on each GeoJSON feature, so any map engine or SVG writer can paint them without knowing about control measures.

Try the controls below. Each change reruns renderControlMeasure and redraws the SVG directly, without a map engine.

measure.style = {
  "color": "#c1121f",
  "strokeWidth": 2
}

Style fields ​

ts
interface ControlMeasureStyle {
  color?: string; // the single symbol color (input only)
  strokeColor?: string; // per-channel override
  strokeWidth?: number;
  strokeDash?: number[];
  fillColor?: string; // per-channel override
  fillPattern?:
    | "solid"
    | "hatch"
    | "reverse-hatch"
    | "cross-hatch"
    | "horizontal"
    | "vertical"
    | "dots";
  opacity?: number; // graphic opacity, 0 through 1
  textHalo?: boolean; // contrasting halo behind label text
  highlight?: boolean; // casing beneath the stroke (look set app-wide)
}

color sets every stroke and fill. Use strokeColor or fillColor for channel-specific overrides, for example a transparent fill:

ts
const render = renderControlMeasure({
  id: "block-1",
  kind: "block",
  controlPoints,
  style: { color: "#1d3557", strokeWidth: 3 },
});

Style never decides whether a part is filled. Doctrine defines that per part, and the generator applies it. Setting fillColor on an outlined part has no effect.

Style priority ​

Styles come from three layers. Each property is resolved separately, and a higher layer wins:

  1. Generator hint (highest): intrinsic doctrine, such as which parts are filled or a pinned hatch pattern. It never picks a color.
  2. measure.style: applies to one ControlMeasure.
  3. graphicsStyle (lowest): defaults for the whole render call, or for every graphic in a TacticalDraw instance.

If no layer sets a color, the kind's doctrinal default applies (see below), then black (#000000).

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

const render = renderControlMeasure(
  {
    id: "block-1",
    kind: "block",
    controlPoints,
    style: { color: "#c1121f", strokeWidth: 2 }, // layer 2
  },
  {
    graphicsStyle: { color: "#1d3557" }, // layer 3: used only if layer 2 has no color
  },
);

A missing or undefined value means "no opinion" and never clears a lower layer. The resolved properties.style contains strokeColor and fillColor, not the input shorthand color.

Doctrinal color defaults ​

Obstacles, such as minefields and roadblocks, default to neon green (#00ff00). Configure their color once without changing other control measures:

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

const symbolColors = { obstacle: MIL_STD_GREEN.dark };

renderControlMeasure(measure, { symbolColors });
// Or once for a whole TacticalDraw instance:
const td = new TacticalDraw(adapter, { symbolColors });

MIL_STD_GREEN provides neon (#00ff00), dark (#00a000), medium (#00e200), and light (#aaffaa). Any CSS color also works; use "#000000" for black obstacles.

These are fallbacks: an explicit color in graphicsStyle or measure.style still wins. Opacity applies after the default is resolved.

The neon value comes from MIL-STD-2525D Table XVI; the other shades are Table XV's neutral identity fill samples, provided as convenient presets. When you choose another obstacle shade, check its contrast against your basemap.

Fill pattern ​

fillPattern replaces a solid interior with a pattern in the resolved fill color. Pattern angle, spacing, and line width are fixed.

ts
const render = renderControlMeasure({
  id: "area-1",
  kind: "fortified-area",
  controlPoints,
  style: { color: "#1d3557", fillPattern: "hatch" },
});

Set fillPattern: "solid" to override a pattern from a lower style layer. Small doctrinal parts such as arrowheads and teeth stay solid for legibility. Dynamic Minefield and Mined Area select one of six intrinsic mine-* pictogram patterns from Modifier 1. Renderers without pattern support fall back to a solid fill.

The pattern geometry is exported as FILL_PATTERN_DEFINITIONS if you write your own renderer.

Graphic opacity ​

opacity dims a complete graphic. It ranges from 0 to 1 and defaults to 1. The renderer multiplies it into each resolved color's alpha:

final alpha = opacity × color alpha

This affects strokes, fills, patterns, and labels together while leaving the original color alpha unchanged. Values outside the range are clamped. The resolved feature style contains the multiplied color values, not an opacity field.

ts
const dimmed = renderControlMeasure({
  id: "block-2",
  kind: "block",
  controlPoints,
  style: { color: "#c1121f", opacity: 0.35 },
});

Each part fades separately, so overlapping parts of one graphic may look darker where they cross.

Graphic opacity is display-only

Selection and hit-testing use color alpha, not this multiplier. A graphic with opacity: 0 is invisible but still selectable. Remove it from the render when it should not receive input.

Point symbols use the same field on PointSymbolStyle, but fade as one image; see Optional point symbols.

Text halo ​

Set textHalo to keep labels readable over satellite imagery or a busy vector basemap.

ts
const render = renderControlMeasure({
  id: "boundary-1",
  kind: "boundary",
  controlPoints,
  textAmplifiers: { T: "1-1 IN" },
  style: { color: "#1d3557", textHalo: true },
});

The renderer chooses a contrasting halo per label: white behind dark text and near-black behind light text. Labels within one graphic may therefore get different halo colors. The field affects only features with a text property, including the generic Text graphic; it never affects strokes or fills.

The renderer writes textHalo and textHaloColor at the top level of each label feature's properties, not inside properties.style. Each adapter uses its engine's native mechanism: a text stroke in OpenLayers, text-halo-* paint properties (with a slight blur) in MapLibre, and stacked text shadows in Leaflet.

The halo option of the Text graphic is gone

Early versions of the generic Text graphic used a halo option. It is no longer read. Move the value to style.textHalo.

Highlight ​

Set highlight to point the reader at a graphic. By default adapters draw a solid orange (#ff6a00) casing beneath the stroke, 3 CSS pixels past each side of it, so the default 2 px stroke gets an 8 px casing. The app can change the color, width and glow; see Highlight appearance.

ts
const render = renderControlMeasure({
  id: "axis-1",
  kind: "main-attack",
  controlPoints,
  style: { highlight: true },
});
  • An area gets the casing on its outline only. The fill is unchanged.
  • A fully transparent stroke still gets the casing along its geometry.
  • Labels keep their own text halo and get no casing.
  • opacity fades the casing along with the rest of the graphic.
  • The casing is display-only. It never adds to the hit area.

Highlight appearance ​

The look of the highlight is set once for the whole app, not per graphic. Pass highlightAppearance to TacticalDraw and change it at any time with setHighlightAppearance:

ts
const td = new TacticalDraw(adapter, {
  highlightAppearance: { color: "#ffd400", width: 4, glow: 6 },
});

// Later, for example from a settings panel. Omitted fields are kept.
td.setHighlightAppearance({ glow: 0 });
FieldDefaultMeaning
color#ff6a00Casing and glow color
width3CSS pixels the casing reaches past each side of the stroke
glow0CSS pixels the fading glow reaches past the casing
  • The glow is two translucent bands, at half and the full glow distance, drawn as plain strokes. It costs no more to draw than the casing and prints the same.
  • width: 0 with a glow leaves only the glow.
  • Set a field to undefined in setHighlightAppearance to return it to its default.
  • setHighlightAppearance re-renders the graphics once, including a graphic being edited. Nothing is redrawn per frame.

renderControlMeasure takes the same object as RenderOptions.highlightAppearance, and tactical-map-sheet takes it in a layer's portrayal, so a printed sheet can match the map.

The renderer resolves highlight into a highlightColor on every line and area part's properties.style, with opacity already applied, and drops the highlight key. It adds highlightWidth and highlightGlow only when they differ from their defaults. All glow bands of a graphic paint beneath all of its casings, and all of them beneath its fills and strokes, so one part's highlight never covers a neighboring part. tactical-map-sheet draws the bands in SVG and PDF too. Adapters for other engines can call resolveHighlightBands(feature, fallbackStrokeWidth) from tactical-draw to get each band's color, width and opacity, bottom to top.

Paint capability ​

Use ControlMeasureMetadata.paints to decide which controls to show in a style panel:

ts
interface ControlMeasurePaintCapability {
  stroke: boolean; // some emitted part is stroked (non-Point) geometry
  fill: "none" | "fixed" | "user";
  text: boolean; // the kind emits text label features
}
  • stroke gates stroke color and width controls.
  • text gates a text color control.
  • fill gates the fill controls:
    • "none": no part is ever filled. Hide the fill controls.
    • "fixed": a filled part exists, but the generator pins its pattern (a solid accent, or a doctrinal hatch such as Limited Access Area's back-hatch). Hide the pattern control, because style.fillPattern never reaches the output. Keep the fill color control: fillColor still applies.
    • "user": a filled part exists and style.fillPattern reaches it. Show all fill controls. These are exactly the kinds with a filled option (polygon, rectangle, circle, block-arrow).

For a "user" kind, paints says only that fill is supported. Check the instance's filled option to see whether it is currently filled.

paints describes the kind, not an instance, and does not inspect options.

Export to simplestyle-spec ​

Some GeoJSON tools expect simplestyle-spec fields such as stroke, fill, and stroke-width instead of a nested style object. Convert a render with toSimpleStyle:

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

const simple = toSimpleStyle(renderControlMeasure(measure));
// Features now have `stroke`, `fill`, `stroke-width`, and `*-opacity` keys.

The conversion is lossy: simplestyle-spec has no strokeDash or fill patterns, so dashed lines become solid and patterns become solid fills. Colors must use hex, rgb(), or rgba() syntax; other formats are omitted.

Engine styling

TacticalDraw adapters use the same resolved styles, so a graphic looks the same in every supported engine.