@orbat-mapper/tactical-map-sheet
v0.4.7
Published
Scale-accurate projected vector tactical map-sheet renderer.
Readme
Tactical map sheet
Public package for deterministic, scale-accurate tactical map sheets.
The package creates transparent or explicitly backed, grid-north-up UTM/UPS SVG and PDF sheets from physical page data, a geographic center, nominal true-ground scale, and normalized authored layers. It supports automatic or explicit projection selection, deterministic UTM/UPS MGRS grids, control measures, renderer-neutral point symbols, custom vector symbols, host-prepared raster basemaps, physically registered overlays, and complete configurable marginalia. One center-selected CRS is retained across zone and polar transitions and any usable distortion or fallback is reported through structured warnings.
Install and entry points
pnpm add @orbat-mapper/tactical-map-sheetThe root entry point exports createTacticalMapSheet and all request, result, warning, metadata,
layer, MGRS, marginalia, and vector-symbol capability types. The optional PDF implementation is
only exported from @orbat-mapper/tactical-map-sheet/pdf, keeping PDF code and dependencies out of
the main SVG module graph.
import {
createTacticalMapSheet,
type TacticalMapSheetRequest,
} from "@orbat-mapper/tactical-map-sheet";
const request: TacticalMapSheetRequest = {
page: { widthMm: 297, heightMm: 210, marginMm: 10, orientation: "landscape" },
center: [-2.5, 49.2],
scaleDenominator: 25_000,
grid: { intervalMeters: 1_000 },
layers: [],
marginalia: {},
};
const { svg, footprint, warnings, metadata, resources } = createTacticalMapSheet(request);TacticalMapSheetResult contains deterministic SVG bytes, the densified geographic printable
footprint, structured warnings, resolved projection/page/grid metadata, and the font and pattern
resources used by the document. Identical requests produce identical results independently of a
browser viewport or device-pixel ratio.
Contract and failure behavior
The page and map frame use millimetres. The package derives one printable footprint from the page,
center, and scale; it does not accept a competing extent. projection: "auto" selects one UTM or
UPS CRS from the center and retains it for the whole sheet. Layers render bottom-to-top in request
order, and each layer preserves authored graphic order. Requested control measures and symbols stay
vector in both output formats.
Invalid coordinates, contradictory page geometry, unavailable point-symbol capabilities, projection failures, and SVG content outside the controlled vector subset throw. A usable but lossy result is returned only with an identifying warning; requested content is never silently omitted or rasterized.
Host-prepared raster basemaps
An ordinary map sheet may include one rasterBasemap beneath its MGRS grid and every authored
vector layer. The basemap is an ordered source-over stack. Each layer has a stable ID, one shared
opacity, structured attribution, and ordered PNG/JPEG tiles. Each tile has a stable ID, original
bytes, declared intrinsic pixel dimensions, and a page-relative millimetre destination. Every layer
must independently tessellate the complete map frame with axis-aligned rectangles; a regular tile
grid is not required.
Resolve the target before a host renders or warps source map pixels:
import {
mapSheetRasterPointToGeographic,
resolveMapSheetRasterPlan,
} from "@orbat-mapper/tactical-map-sheet";
const plan = resolveMapSheetRasterPlan({
page: { widthMm: 297, heightMm: 210, marginMm: 10 },
center: [-2.5, 49.2],
scaleDenominator: 25_000,
});
const topLeft = mapSheetRasterPointToGeographic(plan, 0, 0);plan reports the resolved grid CRS and EPSG code, physical frame, projected bounds, and projected
metres per millimetre from the same resolver used by document generation. Normalized coordinates
are finite values from 0 through 1, use a top-left origin, and convert through the package's
GeographicLib UTM/UPS backend.
request.rasterBasemap = {
layers: [
{
id: "terrain",
opacity: 0.85,
attribution: [{ text: "National mapping authority", required: true }],
tiles: [
{
id: "terrain-full",
mediaType: "image/png",
data: pngBytes,
pixelWidth: 3272,
pixelHeight: 2244,
frame: plan.frame,
},
],
},
],
};
request.marginalia = {};Validation runs before serialization. IDs and attribution text must be non-blank; layer and tile IDs
must be unique; opacity must be in [0, 1]; pixel dimensions must be positive integers; and tiles
must cover the frame without gaps or overlaps within 1e-6 mm. PNG signature, dimensions, and APNG
markers are inspected. JPEG signature, baseline/progressive dimensions, and EXIF orientation are
inspected; only absent orientation or orientation 1 is accepted. Empty, malformed, mismatched, or
unsupported images reject the complete request. Inspection reads image containers only — compressed
pixel data is never decoded, and tile bytes are embedded exactly as supplied.
Resolved raster metadata preserves canonical layer/tile order, opacity, de-duplicated attribution,
physical placement, media type, intrinsic dimensions, and effective horizontal/vertical DPI without
echoing image bytes. low-raster-dpi identifies a tile below 150 DPI on either axis;
raster-dpi-anisotropy identifies a horizontal/vertical difference above 1%. Required attribution
is de-duplicated in stable first-occurrence order and wrapped into a reserved marginalia band. The
request fails if marginalia is disabled or the required text cannot fit without overlap.
SVG embeds self-contained data URLs; PDF consumes the same PNG/JPEG payload and physical rectangles. Both preserve PNG transparency, layer opacity, clipping, and ordinary source-over layer order. MGRS, authored graphics, symbols, text, and marginalia remain vector above the basemap. Identical request values and image bytes produce deterministic output. Registered transparent overlays cannot include a raster basemap.
The host remains responsible for basemap selection and licensing, network acquisition, engine/style
rendering, reprojection into plan.projectedBounds, warp quality, DPI choice, image encoding, and
tile sizing. The package accepts no MapLibre/OpenLayers/Leaflet object, style, URL, canvas, or Web
Mercator tile coordinate and performs no I/O. For a MapLibre reference host, render a flat Mercator
map at pitch/bearing zero with no globe or terrain, wait for idle, keep style layout at 96 CSS
px/in, use drawing-buffer pixel ratio for export DPI, and warp the captured texture into the
UTM/UPS target before encoding tiles. The preview reference host qualifies one coherent MapLibre
source canvas per layer with 256 logical CSS pixels of padding on every side, a 0.5-source-pixel
adaptive warp tolerance, and sequential destination tiles no larger than 4096 pixels per edge. It
never automatically reduces output DPI, padding, tolerance, dimensions, or layer count.
MapLibre's Web Mercator latitude domain and the browser/GPU single-source-canvas and memory limits
qualify that preview-host implementation only. They do not narrow this package's raster plan:
resolveMapSheetRasterPlan and mapSheetRasterPointToGeographic remain UTM/UPS-capable, including
polar vector-only exports. A host using another renderer or a different source strategy may support
other source domains while supplying the same projection-aligned package input.
Warnings are deterministic records with these public codes and payloads:
cross-zone-footprintreports the center-selected UTMselectedZoneand allintersectedZoneswhen the printable footprint enters another UTM zone.cross-projection-footprintreports theselectedProjectionand the orderedintersectedProjections(ups,utm) when the footprint crosses the UTM/UPS boundary. A UTM footprint crossing that boundary and another UTM zone reports both warnings.scale-deviationreportsmaximumRelativeDeviationwhen frame scale differs from center scale by at least0.001(0.1%). Smaller usable distortion remains available in result metadata.graphic-clipped,graphic-outside-frame, andgraphic-hiddenidentify the affectedlayerIdandgraphicId.point-symbol-fallbackidentifies alayerIdandgraphicIdwhose point-symbol capability returned visible fallback vector content.low-raster-dpiidentifieslayerId/tileId, both effective DPI values, and the 150 DPI threshold.raster-dpi-anisotropyidentifieslayerId/tileIdand the relative axis difference above 1%.
Regular/Caps, Italic, and Light semantic text use complete package-owned Open Sans static TTFs and
one final-size metrics service. Their license, immutable provenance, and SHA-256 identities live
under src/assets/.
The package owns public MGRS utilities for parsing, normalization, conversion, formatting, and
snapping. MGRS text accepts case and whitespace variations and normalizes to compact uppercase.
Finite-precision references identify the southwest intersection of their grid cell, never its
center. Geographic positions use GeoJSON order ([longitude, latitude]); projected results carry an
explicit UTM or UPS crs alongside metre easting and northing values. The
geographiclib-mgrs backend is qualified against GeographicLib/GEOTRANS vectors, Norway/Svalbard
zone rules, explicit-zone and round-trip behavior, UTM/UPS transitions, poles, MGRS precision
boundaries, TypeScript/ESM, Node, and browser bundling.
Registered physical overlays
Add registration to turn a normal sheet into a registered overlay. The page, map frame,
geographic center, projection, and nominal scale remain authoritative: registration validates the
chosen sheet and never fits, rotates, resizes, or relocates it.
const registered = createTacticalMapSheet({
page: { widthMm: 297, heightMm: 210, marginMm: 10 },
center: [-2.5, 49.2],
scaleDenominator: 25_000,
registration: {
marks: [
{ id: "southwest", mgrs: "30u wv 33924 48309", label: "SW" },
{ id: "northeast", mgrs: "30UWV3892451309" },
],
snapIntervalMeters: 1_000,
},
marginalia: { showFrameCornerMarks: false, showCalibrationMarks: false },
printerCalibration: {},
});Registration requires at least two marks with unique non-empty IDs. Every MGRS reference is normalized and resolves to its southwest grid intersection, then optionally snaps at 100 m, 1 km, 10 km, or 100 km. A mark must use the sheet's UTM zone and hemisphere, or the same UPS hemisphere. Its complete fixed 8 mm black crosshair and labels must fit inside the printable frame. A non-empty authored label is placed inward from the crosshair. Otherwise, the MGRS easting is placed to its left and the northing below it in the conventional abbreviated arrangement. At 100 km precision, the grid-square column and row letters take those positions. At least two marks must resolve to distinct positions. Any invalid mark rejects the complete request; marks are never omitted or moved.
Registered overlays are transparent. An explicit non-transparent page.background is rejected.
SVG and PDF results expose the same ordered registrationMarks, each containing the authored ID,
normalized mgrs, GeoJSON-order position, and derived sheetPositionMm. The optional full MGRS
grid, tactical layers, and marginalia remain independent.
printerCalibration: {} adds a 100 mm diagnostic line, endpoint ticks, and length label centered
in the bottom page margin. Set a positive lengthMm to choose another length. The request fails if
the complete calibration artwork does not fit; calibration never changes sheet geometry, coverage,
scale, or mark positions.
Print registered SVG or PDF output at 100% / Actual Size with Fit to page disabled. If the calibration line measures incorrectly, correct the printer settings and reprint; do not compensate by resizing the overlay.
Point-symbol renderers enter through a capability that returns complete SVG, bounds, doctrinal and octagon anchors. Point and custom symbols then use the same conservative vector subset. Raster, linked, scripted, malformed, or otherwise unsupported SVG throws instead of being omitted or rasterized.
The checked conformance assets live beside normandy-conformance.spec.ts. Update both the canonical
SVG bytes and the 1188-pixel-wide PNG rendered by the exactly pinned @resvg/resvg-js dependency
with:
pnpm --filter @orbat-mapper/tactical-map-sheet fixtures:updateReview the SVG and PNG visually before committing an update. The visual comparison permits at most 0.01% of pixels to differ by more than 8 in any RGBA channel; semantic assertions remain exact.
The optional direct PDF backend is a separate entry point, so SVG consumers never load jsPDF or its dependencies:
import { renderMapSheetPdf } from "@orbat-mapper/tactical-map-sheet/pdf";
const result = renderMapSheetPdf(request);
const pdfBytes = result.pdf;PDF is serialized directly from the same private millimetre scene as SVG. It retains vector paths, tiling patterns, map-frame clipping, opacity, symbol transforms, embedded searchable Open Sans text, canonical ordering, warnings, resources, footprint, and resolved metadata. The serializer fixes the document timestamp and identifier, disables variable compression, and normalizes geometry through the shared six-decimal formatter so identical requests produce identical bytes. Update the ordinary, patterned, cross-zone, and polar canonical PDF and rendered PNG fixtures with:
pnpm --filter @orbat-mapper/tactical-map-sheet fixtures:update:pdfThe PDF fixtures are rendered by pinned PDF.js and @napi-rs/canvas, then compared both byte-for-byte
with their checked PNGs and visually with the sibling SVG render. The cross-renderer comparison allows
at most 2% of pixels to differ by more than 24 in one RGBA channel, or 10% for one-pixel repeating
hatching where the two vector rasterizers cover opposite edge pixels.
Application code remains responsible for content selection, authorization, export preferences, footprint preview, displaying warnings and validation failures, and saving the returned bytes. Application stores and map-engine output are not part of the public request contract.
API reference
Request
TacticalMapSheetRequest has these fields:
page: required physicalwidthMmandheightMm, with optional non-negativemarginMm,orientation, and CSSbackground(or"transparent").center: required GeoJSON longitude/latitude position;scaleDenominator: required positive nominal true-ground scale at that center.mapFrame: optional{ xMm, yMm, widthMm, heightMm }inside the page. It is mutually exclusive withpage.marginMm.projection:"auto"(default),{ kind: "utm", zone, hemisphere }, or{ kind: "ups", hemisphere }.grid:falseorMgrsGridOptions(intervalMetersof 100, 1,000, 10,000, or 100,000; matching precision 3, 2, 1, or 0; plus color, line width, and label size).layers: orderedTacticalMapSheetLayer[];pointSymbols: the optional renderer capability;marginalia:falseor display options;metadata: optional document text.registration: optional grouped MGRS marks andsnapIntervalMeters; its presence activates strict transparent registered-overlay validation.printerCalibration: optional bottom-margin diagnostic line;lengthMmdefaults to 100.rasterBasemap: optional ordered raster layer stack described above. It is incompatible withregistration.
Each layer has a stable id, ordered { graphic }[], and optional shared portrayal defaults for
color, line/dash/cap/join, and label size/clamp. A graphic is a control-measure ControlMeasure, a
MapSheetPointSymbol, or a MapSheetCustomSymbol.
Control-measure label sizes and Text sizes authored in metres retain their existing Web Mercator
construction-unit meaning. The renderer converts the sheet's true-ground scale at each graphic's
control-point bounding-box midpoint latitude for both label layout and final SVG/PDF text sizing.
For example, a labelSize of 280 at 60° latitude prints at 14 CSS pixels when the sheet represents
10 true-ground metres per CSS pixel. Ordinary ground labels use the layer's clamp band; ground Text
graphics instead scale freely and hide above their maxSizePixels threshold. Screen sizes remain
fixed CSS reference pixels. Authored graphics are never rewritten by export.
Point symbols carry id, sidc, position, rotation in radians, pixel- or metre-based size, optional
amplifiers/modifiers, and renderer style/options. MapSheetPointSymbolCapability.render(symbol)
must return a ControlledVectorSymbol: stable resource ID, controlled-subset SVG, complete width and
height, octagon size, placement anchor, octagon anchor, and optional validation state. Custom symbols
carry that same vector record directly. Metre sizes may specify pixel clamps.
Paper presets
PAPER_SIZES contains exact portrait millimetre dimensions for a0 through a4, letter, legal,
tabloid, ansi-c, ansi-d, ansi-e, and arch-d. ledger aliases tabloid; ansi-a aliases
letter; and ansi-b aliases tabloid, so aliases share one physical definition. ANSI D is
558.8 × 863.6 mm and remains distinct from 609.6 × 914.4 mm ARCH D.
resolvePaperSize(name, orientation) returns { widthMm, heightMm } for direct use as a page input.
Portrait is the default and landscape swaps the axes. Presets are optional conveniences: callers can
continue supplying arbitrary positive widthMm and heightMm, which remain authoritative during
rendering.
MGRS utilities
parseMgrsReferencevalidates a complete UTM or UPS reference and returns its compact-uppercase form, precision, explicit grid CRS, projected metre coordinates, and GeoJSON-order position.normalizeMgrsReference,mgrsToProjected, andmgrsToGeographicexpose focused forms of that parsed result. Geographic conversion always returns the southwest grid-cell intersection.geographicToMgrsandprojectedToMgrsformat coordinates at precision 0 through 5 (100 km through 1 m). Projected input includes its explicit UTM zone/hemisphere or UPS hemisphere.snapGeographicToMgrsandsnapProjectedToMgrsaccept 100 m, 1 km, 10 km, or 100 km. Already aligned intersections stay fixed; exact halfway ties move toward increasing easting and northing.
MapSheetMarginaliaOptions controls color and visibility of declared scale, scale bar, grid/north,
frame-corner marks, legacy calibration marks, and footprint. Grid/north marginalia includes the
MGRS grid-zone designation for the sheet center. Use showFrameCornerMarks for the
canonical ungeoreferenced corner crosses. Deprecated showRegistrationMarks remains an alias; when
both are supplied, showFrameCornerMarks takes precedence. Deprecated showCalibrationMarks
continues to control the existing pair of 10 mm legacy marks and is independent of
printerCalibration. MapSheetMetadata supplies optional title, subtitle, classification, and
preparer text.
Results and resolved metadata
createTacticalMapSheet(request) returns TacticalMapSheetResult:
svg: complete deterministic SVG;footprint: densified GeoJSON polygon.warnings: ordered structured warnings described below.metadata: resolved center, scale denominator, physical page and map frame, optional source document metadata, optional resolved MGRS interval/precision, and resolved CRS (kind, zone or hemisphere, EPSG, center point scale, center convergence in degrees, and maximum frame scale deviation).metadata.rasterBasemap: canonical layer, attribution, tile placement, media type, dimensions, and effective DPI metadata; image bytes are intentionally omitted.resources: identities for embedded Open Sans fonts and deterministic pattern resources.registrationMarks: when registered, the ordered resolved IDs, normalized MGRS references, longitude/latitude positions, and physical sheet positions in millimetres.
renderMapSheetPdf(request) accepts the identical request and returns TacticalMapSheetPdfResult:
pdf: Uint8Array plus the same footprint, warnings, metadata, resources, and resolved registration
marks as the SVG result.
Warning union
TacticalMapSheetWarning is a discriminated union on code. Zone, projection, and scale warnings
carry the payloads documented earlier. graphic-clipped, graphic-outside-frame, graphic-hidden,
and point-symbol-fallback carry both layerId and graphicId. Warning order is deterministic.
