Skip to content

API reference / @zaes/tactical-graphics/maplibre

@zaes/tactical-graphics/maplibre ​

@zaes/tactical-graphics/maplibre — the MapLibre renderer.

Status: shipped in 2.0.0 ​

A complete renderer, not a spike. It paints 291 of the 292 registered graphics — every one of them, since AxisOfAttack was removed in 3.2.0 and no UI path anywhere in the package — and it carries draw, modify and all four handle gestures. Both engines read one shared paint layer, so what a symbol looks like is the same fact on each rather than two implementations that can drift.

Ask isPaintable(name) (from the root entry, @zaes/tactical-graphics) if you want the answer for a specific graphic in code rather than trusting this paragraph.

Two renderers, deliberately ​

Both candidate architectures were built and measured, and both are exported because the choice is a real trade rather than a solved question. createTacticalGraphics uses NativeLayerRenderer unless you hand it another one:

  • CanvasOverlayRenderer — a 2D canvas synced to MapLibre's camera. Style functions port 1:1 and text is measured with the same ruler that draws it. MapLibre renders only the basemap. Redraws every frame: ~7.7 ms for 1,000 graphics, paid continuously while panning.
  • NativeLayerRenderer — geometry realized into GeoJSON sources and drawn by MapLibre's own layers, on the GPU. Rebuilds geometry on zoom change only: ~4.7 ms for the same 1,000 graphics, and free while panning. Costs a glyph server for text, and shares one canvas with the basemap.

What a host must do that OpenLayers does not require ​

  1. Serve MapLibre's worker. maplibre-gl v6 locates it through import.meta.url, which most bundlers break; call setWorkerUrl with a URL you serve. Getting this wrong is silent — every GeoJSON source simply never loads. Only the native renderer needs it.

See ​

  • scripts/copy-maplibre-worker.js
  1. Serve glyphs, for the native renderer. MapLibre draws text from SDF PBFs; there is no system-font path. By default the renderer points the style at MapLibre's public demo glyph server and draws in Noto Sans Bold; production should pass its own glyphs URL and fontStack (or glyphs: false to keep the style's own).
  • NativeLayerRendererOptions
  1. Not put a CSS filter on the map canvas. MapLibre composites every layer into one canvas, so a filter meant for the basemap repaints the graphics too. The canvas overlay is immune; the native renderer has no escape, which is why a dark basemap there means a real dark style rather than a filter.

maplibre-gl is an optional peer dependency, like ol: installing this package for its geometry alone pulls in neither.

Classes ​

Interfaces ​

Type Aliases ​

FontStack ​

ts
type FontStack = string | readonly string[];

Defined in: components/maplibre/native/paintToLayers.ts:485

A font stack as MapLibre's text-font wants it: font names tried in order against the style's glyphs server. A single name is a one-entry stack.

Variables ​

DEFAULT_FONT_STACK ​

ts
const DEFAULT_FONT_STACK: "Noto Sans Bold" = 'Noto Sans Bold';

Defined in: components/maplibre/native/NativeLayerRenderer.ts:133

The stack name has to exist on the glyph server, and a wrong one fails almost silently. Open Sans Bold — the obvious transliteration of this library's bold …px sans-serif — is not served by MapLibre's demo server, which has Open Sans Semibold, Noto Sans Regular and Noto Sans Bold. The 404 is a network entry and nothing else: the map renders, every other layer is fine, and the labels come out as unreadable specks.

This is the second half of the glyph problem. The first is that text needs a server at all; the second is that the font a paint list names (a CSS shorthand, resolved by the browser against whatever it has) and the font MapLibre renders (a fixed stack, baked into a PBF) are different namespaces, so every font this library uses has to be mapped to a stack that has been pre-generated. The canvas overlay has neither problem.


DEFAULT_GLYPHS_URL ​

ts
const DEFAULT_GLYPHS_URL: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf" = 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf';

Defined in: components/maplibre/native/NativeLayerRenderer.ts:117

The glyph stack MapLibre renders labels with.

MapLibre draws text from pre-generated SDF glyph PBFs served over HTTP; there is no path to the system font a canvas would use. That makes text a hosting dependency the OpenLayers renderer does not have, and it is the sharpest practical difference between the two paths: a deployment either self-hosts a glyph set or points at someone else's server.

MapLibre's own demo server is the default because the demo is keyless by decision. A real deployment should self-host and say so through NativeLayerRendererOptions.glyphs: an external font server is a runtime dependency, breaks offline and under a strict CSP, and is not something to build a product on.


MERCATOR_HALF_WORLD ​

ts
const MERCATOR_HALF_WORLD: number;

Defined in: components/maplibre/projection.ts:23

Half the projected world, in meters. π · R, i.e. ±20037508.34.


MERCATOR_WORLD_SIZE ​

ts
const MERCATOR_WORLD_SIZE: number;

Defined in: components/maplibre/projection.ts:26

Full width of the projected world, in meters.

Functions ​

bucketPaints() ​

ts
function bucketPaints(paints, graphicId?): LayerBuckets;

Defined in: components/maplibre/native/paintToLayers.ts:231

Parameters ​

paints ​

Paint[]

graphicId? ​

string

Returns ​

LayerBuckets


centerOf() ​

ts
function centerOf(geometry, name?): Position;

Defined in: components/maplibre/interaction/editGeometry.ts:82

The point a rotate or a resize is measured about.

The rule is the library's, not this renderer's. It differs per base shape — a point turns about itself, a drawn line about its first vertex, a polygon about a point inside itself — and choosing here rather than asking is what made the two engines edit differently from the same drag.

Parameters ​

geometry ​

Geometry

name? ​

TacticalGraphicName

Returns ​

Position

See ​

rotationAnchor


circleLayer() ​

ts
function circleLayer(id, source): LayerSpecification;

Defined in: components/maplibre/native/paintToLayers.ts:467

Parameters ​

id ​

string

source ​

string

Returns ​

LayerSpecification


createBasemapStyle() ​

ts
function createBasemapStyle(dark?): StyleSpecification;

Defined in: components/maplibre/basemapStyle.ts:92

Parameters ​

dark? ​

boolean = true

Returns ​

StyleSpecification


createTacticalGraphics() ​

ts
function createTacticalGraphics(map, options?): TacticalGraphicsEngine;

Defined in: components/maplibre/createTacticalGraphics.ts:69

Tactical graphics on a MapLibre map.

Adds the sources and layers to map and binds the pointer handlers. Call destroy() to take them off again.

Parameters ​

map ​

Map$1

options? ​

MapLibreEngineOptions = {}

Returns ​

TacticalGraphicsEngine


featureCollection() ​

ts
function featureCollection(features): FeatureCollection;

Defined in: components/maplibre/native/paintToLayers.ts:339

Parameters ​

features ​

Feature<Geometry, GeoJsonProperties>[]

Returns ​

FeatureCollection


fillLayer() ​

ts
function fillLayer(id, source): LayerSpecification;

Defined in: components/maplibre/native/paintToLayers.ts:441

Plain fills — everything with no hatch.

Two layers, not one, and this is not a tidiness choice. A single layer carrying both fill-color and a data-driven fill-pattern looked like it would serve both cases, on the reasoning that an empty image name would fall back to the color. It does not: MapLibre draws nothing for a feature whose fill-pattern resolves to an unknown image, and the fill-color beside it is ignored entirely.

So every solid fill in this renderer was invisible — the ferry crossing's arrowheads, fix, turn, the aviation direction of attack, area defense's teeth, exploitation. Each still drew its outline, from the stroke on the same mark, which is what made it look like a thin-line rendering choice rather than a missing fill.

Filtering on the property instead gives each case its own layer and neither can silently swallow the other.

The price is that paint order no longer holds between the two. Within one layer the source's feature order decides, so a paint list's order survives; across the two it is the order the layers were added, which applies to every graphic at once. NativeLayerRenderer therefore adds the pattern layer first, so hatches sit under solid fills — see the note there for why that is the right way round and what asserts it stays true.

Parameters ​

id ​

string

source ​

string

Returns ​

LayerSpecification


insertVertex() ​

ts
function insertVertex(
   description, 
   index, 
   at
): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:415

Adds a vertex to the base, at index — the reshape gesture that OpenLayers gives you by dragging a segment rather than a corner.

Indexed like moveVertex, against positionsOf, so the caller's index means the same thing to both: the new vertex lands before the position currently at index, which is what "insert into this segment" means when the segment is the one ending there.

A ring's closing vertex is not an insertion point. A polygon repeats its first position last; inserting at 0 or at the end would put a vertex outside the ring's closure and open the shape. Those two indices are refused, and every segment between them is available — which is every segment an operator can actually see.

One past the end appends, on a line. That is the edge back from the last point to the first on a base that is a ring stored as a line, which the caller only offers where hasClosingEdge says the edge is real.

Parameters ​

description ​

GraphicDescription

index ​

number

at ​

Position

Returns ​

GraphicDescription

See ​

hasClosingEdge


lineLayer() ​

ts
function lineLayer(
   id, 
   source, 
   dashPx, 
   cap?
): LayerSpecification;

Defined in: components/maplibre/native/paintToLayers.ts:400

Parameters ​

id ​

string

source ​

string

dashPx ​

number[] | undefined

cap? ​

"butt" | "round" | "square"

Returns ​

LayerSpecification


moveVertex() ​

ts
function moveVertex(
   description, 
   index, 
   to
): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:378

Moves one vertex of the base — the reshape gesture.

Indexed against positionsOf, which walks the geometry in the order it is stored, so the caller's index and this one agree by construction. A ring's closing vertex is the same point as its first, so both move together or the polygon comes apart.

Parameters ​

description ​

GraphicDescription

index ​

number

to ​

Position

Returns ​

GraphicDescription


paintToCanvas() ​

ts
function paintToCanvas(
   ctx, 
   paints, 
   view
): void;

Defined in: components/maplibre/canvas/paintToCanvas.ts:178

Draws a whole paint list.

Sorted by zIndex before drawing, with a stable sort — marks without one keep their emitted order, which is the order a paint function chose. Handles carry HANDLE_Z_INDEX so they land last: a handle you cannot see is a handle you cannot use.

Parameters ​

ctx ​

CanvasRenderingContext2D

paints ​

Paint[]

view ​

ViewTransform

Returns ​

void


positionsOf() ​

ts
function positionsOf(geometry): Position[];

Defined in: components/maplibre/interaction/editGeometry.ts:65

Every position of a geometry, in order.

Parameters ​

geometry ​

Geometry

Returns ​

Position[]


resize() ​

ts
function resize(
   description, 
   from, 
   to
): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:243

Grows or shrinks a graphic by the ratio of the cursor's distance from its center, before and after.

A ratio rather than a delta, so the gesture feels the same whether the graphic is a hundred meters or a hundred kilometers across — and so a drag toward the center never flips the shape inside out, which a subtractive delta does the moment it passes zero.

Point-anchored graphics scale their stored radius; drawn ones scale their vertices about the center, which is the same operation one level down.

Parameters ​

description ​

GraphicDescription

from ​

Position

to ​

Position

Returns ​

GraphicDescription


resolutionOf() ​

ts
function resolutionOf(map): number;

Defined in: components/maplibre/projection.ts:60

MapLibre's zoom as an OpenLayers resolution: projected meters per screen pixel.

This is the number every zoom-invariant size in this library is expressed against — n * resolution is a constant number of screen pixels at any zoom — so getting it wrong silently mis-sizes all 128 synthesized decorations rather than throwing.

Two traps, both of which produce a plausible-looking wrong map ​

1. MapLibre's zoom is not OpenLayers' zoom. MapLibre defines the world as 512 · 2^zoom pixels across; OpenLayers' EPSG:3857 view uses 256, so the same zoom number is one power of two apart. A raster source declaring tileSize: 256 changes which tiles are fetched, not this definition.

2. There is no cos(latitude) term. An early estimate wrote this conversion as 156543.03392 × cos(latitude) / 2^zoom, which is the formula for ground meters per pixel. EPSG:3857 meters already carry the Mercator stretch, so applying the cosine again double-counts it: at 45° every decoration would come out 30% too small, and correctly sized only on the equator. The doc has been corrected.

Parameters ​

map ​

Map$1

Returns ​

number


rotate() ​

ts
function rotate(
   description, 
   from, 
   to
): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:138

Turns a graphic about its center by the angle the cursor swept.

Two graphics rotate two different ways and the difference is not cosmetic:

  • A point-anchored graphic keeps its base — a single point has no orientation to change — and carries its bearing in properties.rotation, which the generator reads. Rotating its geometry would do nothing at all.
  • A drawn graphic has no such property; its orientation is its vertices, so they are the thing that turns.

Getting this backwards is silent: the rotate appears to do nothing on half the catalog, which reads as a broken gesture rather than a wrong branch.

Parameters ​

description ​

GraphicDescription

from ​

Position

to ​

Position

Returns ​

GraphicDescription


setBandRange() ​

ts
function setBandRange(
   description, 
   index, 
   cursor
): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:902

The geometry edits themselves, for a host driving them from its own UI rather than from the pointer — a slider that sets a width, a form that sets a bearing. Each takes a description and returns a new one; none of them touch a map.

Parameters ​

description ​

GraphicDescription

index ​

number

cursor ​

Position

Returns ​

GraphicDescription


setBend() ​

ts
function setBend(
   description, 
   cursor, 
   clamp, 
   fromFrame?
): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:700

Sets a curve's bend from the cursor's signed perpendicular distance to the chord, over the graphic's own size.

Over the size, so the handle tracks the pointer exactly at any scale, and dragging across the chord flips which way the curve bows. The chord's direction comes from rotation — planar degrees, 0 = east — and then its clockwise perpendicular, which is the side the generator bows toward.

Parameters ​

description ​

GraphicDescription

cursor ​

Position

clamp ​

(bend) => number

fromFrame? ​

(along, perpendicular, size, currentBend) => number

Reads the bend from the cursor's own frame instead of the perpendicular offset. Envelopment needs it: its tip sits along the axis, so the perpendicular carries no radius and the default rule collapses the hook.

See

envelopmentBendFrom

Returns ​

GraphicDescription


setMirror() ​

ts
function setMirror(
   description, 
   cursor, 
   resolution, 
   mirrorAxis?
): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:624

Flips the graphic to the other side of its own line, and changes nothing else.

The retrograde tasks' second handle. Unlike an offset drag it carries no width: in OpenLayers, dragging a retirement's handle 170 px either way leaves width exactly where it was and moves no vertex — the only thing that changes is which side the cane hangs on. So this reads the sign of the perpendicular and discards the magnitude.

Negative is the mirrored side, matching setOffset: the axis is the segment's left normal and an unmirrored symbol already hangs on that side, so treating a positive perpendicular as mirrored would flip it the moment the operator dragged along the side it was already on.

The threshold is the same one, and for the same reason — crossing the line is easy to do by accident, going a little way past it is not.

Parameters ​

description ​

GraphicDescription

cursor ​

Position

resolution ​

number

mirrorAxis? ​

"across" | "along"

Returns ​

GraphicDescription


setOffset() ​

ts
function setOffset(
   description, 
   cursor, 
   options
): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:495

Sets a graphic's width from how far the cursor sits off its base — the offset handle.

Measured against the base segment the cursor is nearest, not always the first: a movement graphic drawn with several bends has a width that means the same thing at every one of them, and measuring from the first segment makes the handle fight the cursor as soon as the path turns. A two-point base has one segment, so this picks the same one either way.

The stored field is a full width, edge to edge, while the generator takes a half-width offset — the factor of two lives in toGraphicOptions and must not be applied twice.

One handle, two jobs: the magnitude sets the width and the sign sets the side.

Parameters ​

description ​

GraphicDescription

cursor ​

Position

options ​
grab? ​

Position

offsetScale? ​

number

resolution ​

number

Returns ​

GraphicDescription


setReach() ​

ts
function setReach(description, cursor): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:801

Sets both size and bearing from one cursor position — the tip handle.

The far end of a chord carries both of its inputs: how long it is and which way it points. bend is unitless and rides along unchanged, so the curve keeps its proportion through a resize.

Parameters ​

description ​

GraphicDescription

cursor ​

Position

Returns ​

GraphicDescription


symbolLayer() ​

ts
function symbolLayer(
   id, 
   source, 
   fontStack
): LayerSpecification;

Defined in: components/maplibre/native/paintToLayers.ts:496

The upright labels: text a paint did not turn, which stays level on screen however the map is turned or tilted. Turned text is in turnedSymbolLayer, from the same source.

Parameters ​

id ​

string

source ​

string

fontStack ​

FontStack

Returns ​

LayerSpecification


toScreen() ​

ts
function toScreen(position, view): ProjectedPosition;

Defined in: components/maplibre/projection.ts:108

Projected meters → canvas pixels, north-up.

Parameters ​

position ​

ProjectedPosition

view ​

ViewTransform

Returns ​

ProjectedPosition


translate() ​

ts
function translate(
   description, 
   from, 
   to
): GraphicDescription;

Defined in: components/maplibre/interaction/editGeometry.ts:109

Moves a graphic by the metric offset between two lon/lat points.

Parameters ​

description ​

GraphicDescription

from ​

Position

to ​

Position

Returns ​

GraphicDescription


viewTransformOf() ​

ts
function viewTransformOf(map): ViewTransform;

Defined in: components/maplibre/projection.ts:93

Parameters ​

map ​

Map$1

Returns ​

ViewTransform


zoomForResolution() ​

ts
function zoomForResolution(resolution): number;

Defined in: components/maplibre/projection.ts:65

The inverse of resolutionOf — what to hand map.setZoom for a given resolution.

Parameters ​

resolution ​

number

Returns ​

number

References ​

BASE_FONT_SIZE_PX ​

Re-exports BASE_FONT_SIZE_PX


buildTacticalGraphic ​

Renames and re-exports buildPaintedGraphic


configureTacticalGraphics ​

Re-exports configureTacticalGraphics


DEFAULT_LINE_WIDTH ​

Re-exports DEFAULT_LINE_WIDTH


DEFAULT_PALETTE ​

Re-exports DEFAULT_PALETTE


DEFAULT_SYMBOL_SIZE_PX ​

Re-exports DEFAULT_SYMBOL_SIZE_PX


getDefaultLabelSize ​

Re-exports getDefaultLabelSize


getDefaultLineWidth ​

Re-exports getDefaultLineWidth


getDrawMarkerColor ​

Re-exports getDrawMarkerColor


getDrawMarkerOutlineColor ​

Re-exports getDrawMarkerOutlineColor


getHandleColor ​

Re-exports getHandleColor


getInertHandleColor ​

Re-exports getInertHandleColor


getTacticalGraphicsConfig ​

Re-exports getTacticalGraphicsConfig


MapLibreTacticalGraphic ​

Renames and re-exports PaintedGraphic


MAX_LABEL_SIZE ​

Re-exports MAX_LABEL_SIZE


MAX_LINE_WIDTH ​

Re-exports MAX_LINE_WIDTH


MAX_SYMBOL_SIZE_PX ​

Re-exports MAX_SYMBOL_SIZE_PX


MilsymbolModule ​

Re-exports MilsymbolModule


MIN_LABEL_SIZE ​

Re-exports MIN_LABEL_SIZE


MIN_LINE_WIDTH ​

Re-exports MIN_LINE_WIDTH


MIN_SYMBOL_SIZE_PX ​

Re-exports MIN_SYMBOL_SIZE_PX


paintTacticalGraphic ​

Renames and re-exports paintGraphic


projectGeometry ​

Renames and re-exports projectToMercator


resetTacticalGraphicsConfig ​

Re-exports resetTacticalGraphicsConfig


setDefaultLabelSize ​

Re-exports setDefaultLabelSize


setDefaultLineWidth ​

Re-exports setDefaultLineWidth


setTacticalGraphicsConfig ​

Re-exports setTacticalGraphicsConfig


SNAPSHOT_VERSION ​

Re-exports SNAPSHOT_VERSION


snapshotVersionOf ​

Re-exports snapshotVersionOf


supportsHostility ​

Re-exports supportsHostility


TACTICAL_GRAPHIC_KEY ​

Re-exports TACTICAL_GRAPHIC_KEY


TacticalGraphicsConfig ​

Re-exports TacticalGraphicsConfig


TacticalGraphicsConfigOptions ​

Re-exports TacticalGraphicsConfigOptions


TacticalGraphicsSnapshot ​

Re-exports TacticalGraphicsSnapshot


toLonLat ​

Renames and re-exports mercatorToLonLat


toMercator ​

Renames and re-exports lonLatToMercator


toSnapshot ​

Re-exports toSnapshot

Released under the MIT License.