🚧 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.

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

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

Doctrinal color defaults ​

Obstacles default to neon green (#00ff00). Configure their color once without changing other control measures:

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

const symbolColors = { obstacle: MIL_STD_GREEN.dark };

const td = new TacticalDraw(adapter, { symbolColors });
// The same configuration works without TacticalDraw:
renderControlMeasure(measure, { symbolColors });

MIL_STD_GREEN provides neon (#00ff00), dark (#00a000), medium (#00e200), and light (#aaffaa). Custom CSS colors also work; use "#000000" for black obstacles. These are defaults: explicit graphicsStyle, measure style, and generator color hints retain their existing precedence. 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, available as convenient presets. Choosing an alternative obstacle shade should account for background contrast.

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
}

color sets every stroke and fill. Use strokeColor or fillColor for channel-specific overrides. The default is black (#000000).

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 remain 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.

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 },
});

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; 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 automatically: white behind dark text and near-black behind light text. It does this per label, so labels within one graphic may receive different halo colors.

The field applies only to features with a text property. It never affects strokes or fills.

Rather than adding textHalo to the resolved style, the renderer writes textHalo and textHaloColor at the top level of each label feature. Map adapters read those properties directly.

This works for every labeled measure, including the generic Text graphic.

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.

Each adapter uses its engine's native mechanism: a text stroke in OpenLayers, text-halo-* paint properties in MapLibre, and stacked text shadows in Leaflet. MapLibre also adds a slight blur.

Style priority ​

Styles resolve in this order, from highest to lowest priority:

  1. Generator hint: identifies filled parts but does not choose their color.
  2. measure.style: applies to one ControlMeasure.
  3. graphicsStyle: provides defaults for the render call.
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 if layer 2 has no color.
  },
);

The resolved properties.style contains strokeColor and fillColor, not the input shorthand color.

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 a stroke color/width control.
  • text gates a text color control.
  • fill gates the fill controls, with three possible values:
    • "none" — no emitted part is ever filled. Hide the fill control.
    • "fixed" — a filled part exists, but the generator pins its fillPattern (a solid accent, or a doctrinal hatch such as Limited Access Area's back-hatch). The pattern control is inert: cm.style.fillPattern never reaches the output. A fillColor override still applies, so keep the fill color control live.
    • "user" — a filled part exists whose pattern the generator leaves unset, so cm.style.fillPattern reaches the output. The fill control is live. These are exactly the kinds with a filled boolean param (polygon, rectangle, circle, block-arrow).

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

paints is static per kind and does not inspect options. Every definition must declare it, and tests compare each declaration with rendered output.

Exporting 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, so dashed lines become solid. 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 consistent across supported engines.