@orbat-mapper/tactical-map-sheet 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,9 +5,9 @@ Public package for deterministic, scale-accurate tactical map sheets.
5
5
  The package creates transparent or explicitly backed, grid-north-up UTM/UPS SVG and PDF
6
6
  sheets from physical page data, a geographic center, nominal true-ground scale, and normalized authored layers.
7
7
  It supports automatic or explicit projection selection, deterministic UTM/UPS MGRS grids, control
8
- measures, renderer-neutral point symbols, custom vector symbols, and complete configurable
9
- marginalia. One center-selected CRS is retained across zone and polar transitions and any usable
10
- distortion or fallback is reported through structured warnings.
8
+ measures, renderer-neutral point symbols, custom vector symbols, physically registered overlays,
9
+ and complete configurable marginalia. One center-selected CRS is retained across zone and polar
10
+ transitions and any usable distortion or fallback is reported through structured warnings.
11
11
 
12
12
  ## Install and entry points
13
13
 
@@ -74,9 +74,61 @@ Regular/Caps, Italic, and Light semantic text use complete package-owned Open Sa
74
74
  one final-size metrics service. Their license, immutable provenance, and SHA-256 identities live
75
75
  under `src/assets/`.
76
76
 
77
- Projection and grid primitives remain private. The `geographiclib-mgrs` backend is qualified against
78
- GeographicLib/GEOTRANS vectors, Norway/Svalbard zone rules, explicit-zone and round-trip behavior,
79
- UTM/UPS transitions, poles, MGRS precision boundaries, TypeScript/ESM, Node, and browser bundling.
77
+ The package owns public MGRS utilities for parsing, normalization, conversion, formatting, and
78
+ snapping. MGRS text accepts case and whitespace variations and normalizes to compact uppercase.
79
+ Finite-precision references identify the southwest intersection of their grid cell, never its
80
+ center. Geographic positions use GeoJSON order (`[longitude, latitude]`); projected results carry an
81
+ explicit UTM or UPS `crs` alongside metre `easting` and `northing` values. The
82
+ `geographiclib-mgrs` backend is qualified against GeographicLib/GEOTRANS vectors, Norway/Svalbard
83
+ zone rules, explicit-zone and round-trip behavior, UTM/UPS transitions, poles, MGRS precision
84
+ boundaries, TypeScript/ESM, Node, and browser bundling.
85
+
86
+ ## Registered physical overlays
87
+
88
+ Add `registration` to turn a normal sheet into a registered overlay. The page, map frame,
89
+ geographic center, projection, and nominal scale remain authoritative: registration validates the
90
+ chosen sheet and never fits, rotates, resizes, or relocates it.
91
+
92
+ ```ts
93
+ const registered = createTacticalMapSheet({
94
+ page: { widthMm: 297, heightMm: 210, marginMm: 10 },
95
+ center: [-2.5, 49.2],
96
+ scaleDenominator: 25_000,
97
+ registration: {
98
+ marks: [
99
+ { id: "southwest", mgrs: "30u wv 33924 48309", label: "SW" },
100
+ { id: "northeast", mgrs: "30UWV3892451309" },
101
+ ],
102
+ snapIntervalMeters: 1_000,
103
+ },
104
+ marginalia: { showFrameCornerMarks: false, showCalibrationMarks: false },
105
+ printerCalibration: {},
106
+ });
107
+ ```
108
+
109
+ Registration requires at least two marks with unique non-empty IDs. Every MGRS reference is
110
+ normalized and resolves to its southwest grid intersection, then optionally snaps at 100 m, 1 km,
111
+ 10 km, or 100 km. A mark must use the sheet's UTM zone and hemisphere, or the same UPS hemisphere.
112
+ Its complete fixed 8 mm black crosshair and labels must fit inside the printable frame. A non-empty
113
+ authored label is placed inward from the crosshair. Otherwise, the MGRS easting is placed to its left
114
+ and the northing below it in the conventional abbreviated arrangement. At 100 km precision, the
115
+ grid-square column and row letters take those positions. At least one pair must span 50% of the frame
116
+ diagonal and 25% of both frame axes. Any invalid mark rejects the complete request; marks are never
117
+ omitted or moved.
118
+
119
+ Registered overlays are transparent. An explicit non-transparent `page.background` is rejected.
120
+ SVG and PDF results expose the same ordered `registrationMarks`, each containing the authored ID,
121
+ normalized `mgrs`, GeoJSON-order `position`, and derived `sheetPositionMm`. The optional full MGRS
122
+ grid, tactical layers, and marginalia remain independent.
123
+
124
+ `printerCalibration: {}` adds a 100 mm diagnostic line, endpoint ticks, and length label centered
125
+ in the bottom page margin. Set a positive `lengthMm` to choose another length. The request fails if
126
+ the complete calibration artwork does not fit; calibration never changes sheet geometry, coverage,
127
+ scale, or mark positions.
128
+
129
+ Print registered SVG or PDF output at **100% / Actual Size** with **Fit to page disabled**. If the
130
+ calibration line measures incorrectly, correct the printer settings and reprint; do not compensate
131
+ by resizing the overlay.
80
132
 
81
133
  Point-symbol renderers enter through a capability that returns complete SVG, bounds, doctrinal and
82
134
  octagon anchors. Point and custom symbols then use the same conservative vector subset. Raster,
@@ -142,6 +194,9 @@ Application stores and map-engine output are not part of the public request cont
142
194
  matching precision 3, 2, 1, or 0; plus color, line width, and label size).
143
195
  - `layers`: ordered `TacticalMapSheetLayer[]`; `pointSymbols`: the optional renderer capability;
144
196
  `marginalia`: `false` or display options; `metadata`: optional document text.
197
+ - `registration`: optional grouped MGRS marks and `snapIntervalMeters`; its presence activates
198
+ strict transparent registered-overlay validation.
199
+ - `printerCalibration`: optional bottom-margin diagnostic line; `lengthMm` defaults to 100.
145
200
 
146
201
  Each layer has a stable `id`, ordered `{ graphic }[]`, and optional shared portrayal defaults for
147
202
  color, line/dash/cap/join, and label size/clamp. A graphic is a control-measure `ControlMeasure`, a
@@ -153,9 +208,37 @@ must return a `ControlledVectorSymbol`: stable resource ID, controlled-subset SV
153
208
  height, octagon size, placement anchor, octagon anchor, and optional validation state. Custom symbols
154
209
  carry that same vector record directly. Metre sizes may specify pixel clamps.
155
210
 
211
+ ### Paper presets
212
+
213
+ `PAPER_SIZES` contains exact portrait millimetre dimensions for `a0` through `a4`, `letter`, `legal`,
214
+ `tabloid`, `ansi-c`, `ansi-d`, `ansi-e`, and `arch-d`. `ledger` aliases `tabloid`; `ansi-a` aliases
215
+ `letter`; and `ansi-b` aliases `tabloid`, so aliases share one physical definition. ANSI D is
216
+ 558.8 × 863.6 mm and remains distinct from 609.6 × 914.4 mm ARCH D.
217
+
218
+ `resolvePaperSize(name, orientation)` returns `{ widthMm, heightMm }` for direct use as a page input.
219
+ Portrait is the default and landscape swaps the axes. Presets are optional conveniences: callers can
220
+ continue supplying arbitrary positive `widthMm` and `heightMm`, which remain authoritative during
221
+ rendering.
222
+
223
+ ### MGRS utilities
224
+
225
+ - `parseMgrsReference` validates a complete UTM or UPS reference and returns its compact-uppercase
226
+ form, precision, explicit grid CRS, projected metre coordinates, and GeoJSON-order position.
227
+ - `normalizeMgrsReference`, `mgrsToProjected`, and `mgrsToGeographic` expose focused forms of that
228
+ parsed result. Geographic conversion always returns the southwest grid-cell intersection.
229
+ - `geographicToMgrs` and `projectedToMgrs` format coordinates at precision 0 through 5 (100 km
230
+ through 1 m). Projected input includes its explicit UTM zone/hemisphere or UPS hemisphere.
231
+ - `snapGeographicToMgrs` and `snapProjectedToMgrs` accept 100 m, 1 km, 10 km, or 100 km. Already
232
+ aligned intersections stay fixed; exact halfway ties move toward increasing easting and northing.
233
+
156
234
  `MapSheetMarginaliaOptions` controls color and visibility of declared scale, scale bar, grid/north,
157
- registration marks, calibration marks, and footprint. `MapSheetMetadata` supplies optional title,
158
- subtitle, classification, and preparer text.
235
+ frame-corner marks, legacy calibration marks, and footprint. Grid/north marginalia includes the
236
+ MGRS grid-zone designation for the sheet center. Use `showFrameCornerMarks` for the
237
+ canonical ungeoreferenced corner crosses. Deprecated `showRegistrationMarks` remains an alias; when
238
+ both are supplied, `showFrameCornerMarks` takes precedence. Deprecated `showCalibrationMarks`
239
+ continues to control the existing pair of 10 mm legacy marks and is independent of
240
+ `printerCalibration`. `MapSheetMetadata` supplies optional title, subtitle, classification, and
241
+ preparer text.
159
242
 
160
243
  ### Results and resolved metadata
161
244
 
@@ -168,9 +251,12 @@ subtitle, classification, and preparer text.
168
251
  hemisphere, EPSG, center point scale, center convergence in degrees, and maximum frame scale
169
252
  deviation).
170
253
  - `resources`: identities for embedded Open Sans fonts and deterministic pattern resources.
254
+ - `registrationMarks`: when registered, the ordered resolved IDs, normalized MGRS references,
255
+ longitude/latitude positions, and physical sheet positions in millimetres.
171
256
 
172
257
  `renderMapSheetPdf(request)` accepts the identical request and returns `TacticalMapSheetPdfResult`:
173
- `pdf: Uint8Array` plus the same footprint, warnings, metadata, and resources as the SVG result.
258
+ `pdf: Uint8Array` plus the same footprint, warnings, metadata, resources, and resolved registration
259
+ marks as the SVG result.
174
260
 
175
261
  ### Warning union
176
262
 
@@ -5,7 +5,7 @@ import { DEFAULT_LABEL_HEIGHT_CSS_PIXELS, DEFAULT_LABEL_SIZE_CLAMP_CSS_PIXELS, D
5
5
  import { SaxesParser } from "saxes";
6
6
  //#region src/projection/geographic-lib.ts
7
7
  const backend = GeographicLibMgrs.UTMUPS;
8
- const mgrsBackend = GeographicLibMgrs.MGRS;
8
+ const mgrsBackend$1 = GeographicLibMgrs.MGRS;
9
9
  const FORCE_UTM_ZONE_SELECTION = -2;
10
10
  const UPS_FALSE_ORIGIN_METERS = 2e6;
11
11
  const polarBackend = PolarStereographic.UPS();
@@ -106,7 +106,7 @@ const geographicLibGridProjection = {
106
106
  assertGridCrs(crs);
107
107
  if (!Number.isInteger(precision) || precision < 0 || precision > 5) throw new RangeError("MGRS precision must be an integer from 0 through 5");
108
108
  if (!Number.isFinite(point.easting) || !Number.isFinite(point.northing)) throw new RangeError("Projected coordinates must be finite");
109
- return mgrsBackend.forward(crs.kind === "utm" ? crs.zone : 0, crs.hemisphere === "north", point.easting, point.northing, precision);
109
+ return mgrsBackend$1.forward(crs.kind === "utm" ? crs.zone : 0, crs.hemisphere === "north", point.easting, point.northing, precision);
110
110
  }
111
111
  };
112
112
  //#endregion
@@ -255,6 +255,23 @@ function createRegularSceneText(input) {
255
255
  };
256
256
  }
257
257
  //#endregion
258
+ //#region src/scene-line.ts
259
+ /** Stroke width shared by marginalia, registration, and calibration line work. */
260
+ const DEFAULT_LINE_STROKE_WIDTH_MM = .2;
261
+ /** Build a plain solid scene line: butt caps, mitre joins, no dashes. */
262
+ function createSceneLine(points, color, options = {}) {
263
+ return {
264
+ kind: "path",
265
+ points,
266
+ strokeColor: color,
267
+ strokeWidthMm: options.strokeWidthMm ?? .2,
268
+ strokeDashMm: [],
269
+ lineCap: "butt",
270
+ lineJoin: "miter",
271
+ ...options.attributes ? { attributes: options.attributes } : {}
272
+ };
273
+ }
274
+ //#endregion
258
275
  //#region src/validation.ts
259
276
  function requirePositiveFinite(value, name) {
260
277
  if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be a positive finite number`);
@@ -277,18 +294,24 @@ function projectedToScenePoint(projected, context) {
277
294
  };
278
295
  }
279
296
  //#endregion
280
- //#region src/mgrs-grid.ts
281
- const SUPPORTED_INTERVALS = [
297
+ //#region src/mgrs-domain.ts
298
+ const MGRS_SNAP_INTERVALS = [
282
299
  100,
283
300
  1e3,
284
301
  1e4,
285
302
  1e5
286
303
  ];
304
+ function mgrsPrecisionForInterval(interval) {
305
+ if (!MGRS_SNAP_INTERVALS.includes(interval)) throw new RangeError("MGRS interval must be 100, 1000, 10000, or 100000 metres");
306
+ return Math.round(Math.log10(1e5 / interval));
307
+ }
308
+ //#endregion
309
+ //#region src/mgrs-grid.ts
287
310
  function resolveGrid(options, context) {
288
- const automatic = SUPPORTED_INTERVALS.find((candidate) => candidate / context.projectedMetresPerMillimetre >= 10);
311
+ const automatic = MGRS_SNAP_INTERVALS.find((candidate) => candidate / context.projectedMetresPerMillimetre >= 10);
289
312
  const intervalMeters = options.intervalMeters ?? automatic ?? 1e5;
290
- if (!SUPPORTED_INTERVALS.includes(intervalMeters)) throw new RangeError("MGRS interval must be 100, 1000, 10000, or 100000 metres");
291
- const expectedPrecision = Math.round(Math.log10(1e5 / intervalMeters));
313
+ if (!MGRS_SNAP_INTERVALS.includes(intervalMeters)) throw new RangeError("MGRS interval must be 100, 1000, 10000, or 100000 metres");
314
+ const expectedPrecision = mgrsPrecisionForInterval(intervalMeters);
292
315
  const precision = options.precision ?? expectedPrecision;
293
316
  if (precision !== expectedPrecision) throw new RangeError("MGRS precision must match the selected grid interval");
294
317
  const lineWidthCssPixels = options.lineWidthCssPixels ?? .75;
@@ -339,15 +362,7 @@ function renderMgrsGrid(options, context) {
339
362
  const xMm = (easting) => eastingToMillimetres(easting, context);
340
363
  const yMm = (northing) => northingToMillimetres(northing, context);
341
364
  const strokeWidthMm = resolved.lineWidthCssPixels * MILLIMETRES_PER_CSS_PIXEL;
342
- const gridLine = (points) => ({
343
- kind: "path",
344
- points,
345
- strokeColor: resolved.color,
346
- strokeWidthMm,
347
- strokeDashMm: [],
348
- lineCap: "butt",
349
- lineJoin: "miter"
350
- });
365
+ const gridLine = (points) => createSceneLine(points, resolved.color, { strokeWidthMm });
351
366
  const elements = [...eastings.flatMap((easting) => {
352
367
  const gridLabel = label(geographicLibGridProjection.formatMgrs(context.crs, {
353
368
  easting,
@@ -401,17 +416,8 @@ function text(value, xMm, yMm, sizeCssPixels, color, anchor = "middle") {
401
416
  anchor
402
417
  });
403
418
  }
404
- function line(points, color, attributes) {
405
- return {
406
- kind: "path",
407
- points,
408
- strokeColor: color,
409
- strokeWidthMm: .2,
410
- strokeDashMm: [],
411
- lineCap: "butt",
412
- lineJoin: "miter",
413
- ...attributes ? { attributes } : {}
414
- };
419
+ function line$1(points, color, attributes) {
420
+ return createSceneLine(points, color, { attributes });
415
421
  }
416
422
  function formatScale(scaleDenominator) {
417
423
  return Math.round(scaleDenominator).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -445,7 +451,7 @@ function renderMarginalia(options, context) {
445
451
  const barX = context.frame.xMm;
446
452
  const barY = bottomY;
447
453
  const representedMeters = barWidthMm * context.scaleDenominator / 1e3;
448
- elements.push(line([{
454
+ elements.push(line$1([{
449
455
  xMm: barX,
450
456
  yMm: barY
451
457
  }, {
@@ -454,7 +460,7 @@ function renderMarginalia(options, context) {
454
460
  }], color, { "data-scale-bar-m": representedMeters }));
455
461
  for (let step = 0; step <= 4; step += 1) {
456
462
  const xMm = barX + barWidthMm * step / 4;
457
- elements.push(line([{
463
+ elements.push(line$1([{
458
464
  xMm,
459
465
  yMm: barY - 1
460
466
  }, {
@@ -467,21 +473,21 @@ function renderMarginalia(options, context) {
467
473
  if (options.showGridAndNorth !== false) {
468
474
  const gridName = context.crs.kind === "utm" ? `UTM ${context.crs.zone}${context.crs.hemisphere === "north" ? "N" : "S"}` : `UPS ${context.crs.hemisphere === "north" ? "NORTH" : "SOUTH"}`;
469
475
  const rightX = context.frame.xMm + context.frame.widthMm;
470
- elements.push(text(`${gridName} / EPSG:${context.epsg}`, rightX, bottomY - 1.5, 6, color, "end"));
476
+ elements.push(text(`MGRS GRID ZONE ${context.mgrsGridZone} · ${gridName} / EPSG:${context.epsg}`, rightX, bottomY - 1.5, 6, color, "end"));
471
477
  elements.push(text(`GRID NORTH · TRUE NORTH · CONVERGENCE ${context.convergenceDegrees.toFixed(3)}°`, rightX, bottomY + 1.5, 5, color, "end"));
472
478
  }
473
- if (options.showRegistrationMarks !== false) for (const [index, [xMm, yMm]] of [
479
+ if (options.showFrameCornerMarks ?? options.showRegistrationMarks ?? true) for (const [index, [xMm, yMm]] of [
474
480
  [context.frame.xMm, context.frame.yMm],
475
481
  [context.frame.xMm + context.frame.widthMm, context.frame.yMm],
476
482
  [context.frame.xMm + context.frame.widthMm, context.frame.yMm + context.frame.heightMm],
477
483
  [context.frame.xMm, context.frame.yMm + context.frame.heightMm]
478
- ].entries()) elements.push(line([{
484
+ ].entries()) elements.push(line$1([{
479
485
  xMm: xMm - 2,
480
486
  yMm
481
487
  }, {
482
488
  xMm: xMm + 2,
483
489
  yMm
484
- }], color, { "data-registration-mark": index }), line([{
490
+ }], color, { "data-registration-mark": index }), line$1([{
485
491
  xMm,
486
492
  yMm: yMm - 2
487
493
  }, {
@@ -502,7 +508,7 @@ function renderMarginalia(options, context) {
502
508
  }, {
503
509
  xMm: centerX + calibrationLengthMm / 2,
504
510
  yMm: context.frame.yMm + context.frame.heightMm + 1
505
- }]].entries()) elements.push(line([start, end], color, {
511
+ }]].entries()) elements.push(line$1([start, end], color, {
506
512
  "data-calibration-mark": index,
507
513
  "data-calibration-length-mm": calibrationLengthMm
508
514
  }));
@@ -1616,6 +1622,329 @@ function assertNever(value) {
1616
1622
  throw new Error(`Unsupported geometry: ${String(value)}`);
1617
1623
  }
1618
1624
  //#endregion
1625
+ //#region src/mgrs.ts
1626
+ const mgrsBackend = GeographicLibMgrs.MGRS;
1627
+ function invalidReference(reference, cause) {
1628
+ const detail = cause instanceof Error ? cause.message : String(cause);
1629
+ return /* @__PURE__ */ new RangeError(`Invalid MGRS reference ${JSON.stringify(reference)}: ${detail}`);
1630
+ }
1631
+ function compactReference(reference) {
1632
+ if (typeof reference !== "string") throw invalidReference(String(reference), "expected a string");
1633
+ const normalized = reference.replace(/\s/g, "").toUpperCase();
1634
+ const match = /^(?:0[1-9]|[1-9]|[1-5][0-9]|60)[C-HJ-NP-X][A-HJ-NP-Z]{2}(\d*)$/.exec(normalized) ?? /^[ABYZ][A-HJ-NP-Z]{2}(\d*)$/.exec(normalized);
1635
+ if (!match) throw invalidReference(reference, "expected a complete UTM or UPS grid reference");
1636
+ if (match[1].length % 2 !== 0) throw invalidReference(reference, "easting and northing must have equal precision");
1637
+ if (match[1].length > 10) throw invalidReference(reference, "precision cannot exceed 5 digits per axis");
1638
+ return normalized;
1639
+ }
1640
+ function assertPrecision(precision) {
1641
+ if (!Number.isInteger(precision) || precision < 0 || precision > 5) throw new RangeError("MGRS precision must be an integer from 0 through 5");
1642
+ }
1643
+ function pointForMgrsFormatting(coordinate, precision) {
1644
+ if (coordinate.crs.kind !== "ups") return coordinate;
1645
+ const cellSizeMeters = 1e5 / 10 ** precision;
1646
+ if (coordinate.northing % cellSizeMeters !== 0) return coordinate;
1647
+ return {
1648
+ easting: coordinate.easting,
1649
+ northing: coordinate.northing + 1e-6
1650
+ };
1651
+ }
1652
+ /** Parse a complete MGRS cell and resolve its southwest grid intersection. */
1653
+ function parseMgrsReference(reference) {
1654
+ const normalized = compactReference(reference);
1655
+ try {
1656
+ const result = mgrsBackend.reverse(normalized, false);
1657
+ assertPrecision(result.prec);
1658
+ const crs = result.zone === 0 ? {
1659
+ kind: "ups",
1660
+ hemisphere: result.northp ? "north" : "south"
1661
+ } : {
1662
+ kind: "utm",
1663
+ zone: result.zone,
1664
+ hemisphere: result.northp ? "north" : "south"
1665
+ };
1666
+ const position = geographicLibGridProjection.reverse(crs, {
1667
+ easting: result.x,
1668
+ northing: result.y
1669
+ });
1670
+ return {
1671
+ normalized: projectedToMgrs({
1672
+ crs,
1673
+ easting: result.x,
1674
+ northing: result.y
1675
+ }, result.prec),
1676
+ precision: result.prec,
1677
+ crs,
1678
+ easting: result.x,
1679
+ northing: result.y,
1680
+ position
1681
+ };
1682
+ } catch (error) {
1683
+ if (error instanceof RangeError && error.message.startsWith("Invalid MGRS reference")) throw error;
1684
+ throw invalidReference(reference, error);
1685
+ }
1686
+ }
1687
+ function normalizeMgrsReference(reference) {
1688
+ return parseMgrsReference(reference).normalized;
1689
+ }
1690
+ /** Convert an MGRS cell to its southwest intersection in its native grid CRS. */
1691
+ function mgrsToProjected(reference) {
1692
+ const { crs, easting, northing } = parseMgrsReference(reference);
1693
+ return {
1694
+ crs,
1695
+ easting,
1696
+ northing
1697
+ };
1698
+ }
1699
+ /** Convert an MGRS cell to its southwest intersection in GeoJSON coordinate order. */
1700
+ function mgrsToGeographic(reference) {
1701
+ return parseMgrsReference(reference).position;
1702
+ }
1703
+ function projectedToMgrs(coordinate, precision = 5) {
1704
+ assertPrecision(precision);
1705
+ return geographicLibGridProjection.formatMgrs(coordinate.crs, pointForMgrsFormatting(coordinate, precision), precision);
1706
+ }
1707
+ function geographicToMgrs(position, precision = 5) {
1708
+ assertPrecision(precision);
1709
+ const crs = geographicLibGridProjection.selectStandard(position);
1710
+ return projectedToMgrs({
1711
+ crs,
1712
+ ...geographicLibGridProjection.forward(crs, position)
1713
+ }, precision);
1714
+ }
1715
+ function snapUpOnHalf(value, interval) {
1716
+ return Math.floor(value / interval + .5) * interval;
1717
+ }
1718
+ function snapProjectedToMgrs(coordinate, interval) {
1719
+ const precision = mgrsPrecisionForInterval(interval);
1720
+ const easting = snapUpOnHalf(coordinate.easting, interval);
1721
+ const northing = snapUpOnHalf(coordinate.northing, interval);
1722
+ return parseMgrsReference(projectedToMgrs({
1723
+ crs: coordinate.crs,
1724
+ easting,
1725
+ northing
1726
+ }, precision));
1727
+ }
1728
+ function snapGeographicToMgrs(position, interval) {
1729
+ const crs = geographicLibGridProjection.selectStandard(position);
1730
+ return snapProjectedToMgrs({
1731
+ crs,
1732
+ ...geographicLibGridProjection.forward(crs, position)
1733
+ }, interval);
1734
+ }
1735
+ //#endregion
1736
+ //#region src/registration.ts
1737
+ const CROSSHAIR_HALF_SIZE_MM = 4;
1738
+ const CROSSHAIR_STROKE_WIDTH_MM = DEFAULT_LINE_STROKE_WIDTH_MM;
1739
+ const LABEL_GAP_MM = 1;
1740
+ const LABEL_SIZE_CSS_PIXELS = 7;
1741
+ function sameCrs(left, right) {
1742
+ return left.kind === right.kind && left.hemisphere === right.hemisphere && (left.kind === "ups" || right.kind === "utm" && left.zone === right.zone);
1743
+ }
1744
+ function createCrosshairAxis(points, id, axis) {
1745
+ return createSceneLine(points, "#000000", {
1746
+ strokeWidthMm: CROSSHAIR_STROKE_WIDTH_MM,
1747
+ attributes: {
1748
+ "data-registration-id": id,
1749
+ "data-registration-axis": axis,
1750
+ "data-registration-size-mm": CROSSHAIR_HALF_SIZE_MM * 2
1751
+ }
1752
+ });
1753
+ }
1754
+ function textBounds(text) {
1755
+ const left = text.anchor === "start" ? text.position.xMm : text.anchor === "end" ? text.position.xMm - text.widthMm : text.position.xMm - text.widthMm / 2;
1756
+ return {
1757
+ left,
1758
+ right: left + text.widthMm,
1759
+ top: text.position.yMm - text.heightMm / 2,
1760
+ bottom: text.position.yMm + text.heightMm / 2
1761
+ };
1762
+ }
1763
+ function contains(frame, bounds) {
1764
+ return bounds.left >= frame.xMm && bounds.right <= frame.xMm + frame.widthMm && bounds.top >= frame.yMm && bounds.bottom <= frame.yMm + frame.heightMm;
1765
+ }
1766
+ function createCustomLabel(value, point, frame, id) {
1767
+ const towardRight = point.xMm <= frame.xMm + frame.widthMm / 2;
1768
+ return createRegularSceneText({
1769
+ text: value,
1770
+ xMm: point.xMm + (towardRight ? 1 : -1) * 5,
1771
+ yMm: point.yMm,
1772
+ sizeCssPixels: LABEL_SIZE_CSS_PIXELS,
1773
+ color: "#000000",
1774
+ anchor: towardRight ? "start" : "end",
1775
+ attributes: {
1776
+ "data-registration-id": id,
1777
+ "data-registration-label": value
1778
+ }
1779
+ });
1780
+ }
1781
+ function splitGridCoordinates(reference) {
1782
+ if (reference.precision === 0) return [reference.normalized.at(-2), reference.normalized.at(-1)];
1783
+ const coordinates = reference.normalized.slice(-2 * reference.precision);
1784
+ return [coordinates.slice(0, reference.precision), coordinates.slice(reference.precision)];
1785
+ }
1786
+ function createCoordinateLabels(reference, point, id) {
1787
+ const [easting, northing] = splitGridCoordinates(reference);
1788
+ return [createRegularSceneText({
1789
+ text: easting,
1790
+ xMm: point.xMm - CROSSHAIR_HALF_SIZE_MM - LABEL_GAP_MM,
1791
+ yMm: point.yMm,
1792
+ sizeCssPixels: LABEL_SIZE_CSS_PIXELS,
1793
+ color: "#000000",
1794
+ anchor: "end",
1795
+ attributes: {
1796
+ "data-registration-id": id,
1797
+ "data-registration-label": easting,
1798
+ "data-registration-coordinate": "easting"
1799
+ }
1800
+ }), createRegularSceneText({
1801
+ text: northing,
1802
+ xMm: point.xMm,
1803
+ yMm: point.yMm + CROSSHAIR_HALF_SIZE_MM + LABEL_GAP_MM,
1804
+ sizeCssPixels: LABEL_SIZE_CSS_PIXELS,
1805
+ color: "#000000",
1806
+ attributes: {
1807
+ "data-registration-id": id,
1808
+ "data-registration-label": northing,
1809
+ "data-registration-coordinate": "northing"
1810
+ }
1811
+ })];
1812
+ }
1813
+ function assertMarkFits(point, labels, frame, id) {
1814
+ const strokeEdge = 4.1;
1815
+ if (!(point.xMm - strokeEdge >= frame.xMm && point.xMm + strokeEdge <= frame.xMm + frame.widthMm && point.yMm - strokeEdge >= frame.yMm && point.yMm + strokeEdge <= frame.yMm + frame.heightMm) || labels.some((label) => !contains(frame, textBounds(label)))) throw new RangeError(`Registration mark ${JSON.stringify(id)} and its label must fit within the printable map frame`);
1816
+ }
1817
+ function hasUsablePair(marks, frame) {
1818
+ const minimumX = frame.widthMm * .25;
1819
+ const minimumY = frame.heightMm * .25;
1820
+ const minimumDiagonal = Math.hypot(frame.widthMm, frame.heightMm) * .5;
1821
+ for (let left = 0; left < marks.length; left += 1) for (let right = left + 1; right < marks.length; right += 1) {
1822
+ const first = marks[left].sheetPositionMm;
1823
+ const second = marks[right].sheetPositionMm;
1824
+ const x = Math.abs(first.xMm - second.xMm);
1825
+ const y = Math.abs(first.yMm - second.yMm);
1826
+ if (x >= minimumX && y >= minimumY && Math.hypot(x, y) >= minimumDiagonal) return true;
1827
+ }
1828
+ return false;
1829
+ }
1830
+ function renderRegistration(request, context) {
1831
+ if (!Array.isArray(request.marks) || request.marks.length < 2) throw new RangeError("Registration requires at least two authored marks");
1832
+ const ids = /* @__PURE__ */ new Set();
1833
+ const graphics = [];
1834
+ const marks = [];
1835
+ for (const authored of request.marks) {
1836
+ if (typeof authored.id !== "string" || authored.id.trim() === "") throw new RangeError("Every registration mark requires a non-empty ID");
1837
+ const idKey = authored.id.trim();
1838
+ if (ids.has(idKey)) throw new RangeError(`Registration mark ID ${JSON.stringify(authored.id)} must be unique`);
1839
+ ids.add(idKey);
1840
+ const parsed = parseMgrsReference(authored.mgrs);
1841
+ const resolved = request.snapIntervalMeters !== void 0 ? snapProjectedToMgrs(parsed, request.snapIntervalMeters) : parsed;
1842
+ if (!sameCrs(resolved.crs, context.crs)) throw new RangeError(`Registration mark ${JSON.stringify(authored.id)} must use the sheet grid coordinate reference system`);
1843
+ const sheetPositionMm = projectedToScenePoint(resolved, context);
1844
+ const labels = authored.label && authored.label.trim() !== "" ? [createCustomLabel(authored.label, sheetPositionMm, context.frame, authored.id)] : createCoordinateLabels(resolved, sheetPositionMm, authored.id);
1845
+ assertMarkFits(sheetPositionMm, labels, context.frame, authored.id);
1846
+ marks.push({
1847
+ id: authored.id,
1848
+ mgrs: resolved.normalized,
1849
+ position: [resolved.position[0], resolved.position[1]],
1850
+ sheetPositionMm
1851
+ });
1852
+ const elements = [
1853
+ createCrosshairAxis([{
1854
+ xMm: sheetPositionMm.xMm - CROSSHAIR_HALF_SIZE_MM,
1855
+ yMm: sheetPositionMm.yMm
1856
+ }, {
1857
+ xMm: sheetPositionMm.xMm + CROSSHAIR_HALF_SIZE_MM,
1858
+ yMm: sheetPositionMm.yMm
1859
+ }], authored.id, "horizontal"),
1860
+ createCrosshairAxis([{
1861
+ xMm: sheetPositionMm.xMm,
1862
+ yMm: sheetPositionMm.yMm - CROSSHAIR_HALF_SIZE_MM
1863
+ }, {
1864
+ xMm: sheetPositionMm.xMm,
1865
+ yMm: sheetPositionMm.yMm + CROSSHAIR_HALF_SIZE_MM
1866
+ }], authored.id, "vertical"),
1867
+ ...labels
1868
+ ];
1869
+ graphics.push({
1870
+ id: authored.id,
1871
+ elements
1872
+ });
1873
+ }
1874
+ if (!hasUsablePair(marks, context.frame)) throw new RangeError("Registration requires a pair spanning 50% of the printable diagonal and 25% of both frame axes");
1875
+ return {
1876
+ layer: {
1877
+ id: "registration-marks-0",
1878
+ graphics,
1879
+ attributes: { "data-registration-marks": marks.length }
1880
+ },
1881
+ marks
1882
+ };
1883
+ }
1884
+ //#endregion
1885
+ //#region src/printer-calibration.ts
1886
+ const DEFAULT_LENGTH_MM = 100;
1887
+ const TICK_HALF_HEIGHT_MM = 2;
1888
+ const LABEL_OFFSET_MM = 2.5;
1889
+ const STROKE_WIDTH_MM = DEFAULT_LINE_STROKE_WIDTH_MM;
1890
+ function line(points, attributes) {
1891
+ return createSceneLine(points, "#000000", { attributes });
1892
+ }
1893
+ function renderPrinterCalibration(request, page, frame) {
1894
+ const lengthMm = request.lengthMm ?? DEFAULT_LENGTH_MM;
1895
+ requirePositiveFinite(lengthMm, "Printer-calibration length");
1896
+ const centerX = page.widthMm / 2;
1897
+ const frameBottom = frame.yMm + frame.heightMm;
1898
+ const yMm = frameBottom + (page.heightMm - frameBottom) / 2;
1899
+ const startX = centerX - lengthMm / 2;
1900
+ const endX = centerX + lengthMm / 2;
1901
+ const label = createRegularSceneText({
1902
+ text: `${lengthMm} mm`,
1903
+ xMm: centerX,
1904
+ yMm: yMm - LABEL_OFFSET_MM,
1905
+ sizeCssPixels: 6,
1906
+ color: "#000000",
1907
+ attributes: { "data-printer-calibration-label": lengthMm }
1908
+ });
1909
+ const strokeEdge = STROKE_WIDTH_MM / 2;
1910
+ const fitsHorizontally = startX - strokeEdge >= 0 && endX + strokeEdge <= page.widthMm;
1911
+ const labelLeft = label.position.xMm - label.widthMm / 2;
1912
+ const labelRight = label.position.xMm + label.widthMm / 2;
1913
+ const contentTop = Math.min(label.position.yMm - label.heightMm / 2, yMm - TICK_HALF_HEIGHT_MM - strokeEdge);
1914
+ const contentBottom = Math.max(label.position.yMm + label.heightMm / 2, yMm + TICK_HALF_HEIGHT_MM + strokeEdge);
1915
+ if (!fitsHorizontally || labelLeft < 0 || labelRight > page.widthMm || contentTop < frameBottom || contentBottom > page.heightMm) throw new RangeError("The bottom page margin must fit the complete printer-calibration line, ticks, and label");
1916
+ return {
1917
+ id: "printer-calibration-0",
1918
+ graphics: [{
1919
+ id: "printer-calibration",
1920
+ elements: [
1921
+ line([{
1922
+ xMm: startX,
1923
+ yMm
1924
+ }, {
1925
+ xMm: endX,
1926
+ yMm
1927
+ }], { "data-printer-calibration-length-mm": lengthMm }),
1928
+ line([{
1929
+ xMm: startX,
1930
+ yMm: yMm - TICK_HALF_HEIGHT_MM
1931
+ }, {
1932
+ xMm: startX,
1933
+ yMm: yMm + TICK_HALF_HEIGHT_MM
1934
+ }], { "data-printer-calibration-tick": "start" }),
1935
+ line([{
1936
+ xMm: endX,
1937
+ yMm: yMm - TICK_HALF_HEIGHT_MM
1938
+ }, {
1939
+ xMm: endX,
1940
+ yMm: yMm + TICK_HALF_HEIGHT_MM
1941
+ }], { "data-printer-calibration-tick": "end" }),
1942
+ label
1943
+ ]
1944
+ }]
1945
+ };
1946
+ }
1947
+ //#endregion
1619
1948
  //#region src/create-tactical-map-sheet.ts
1620
1949
  const FOOTPRINT_SEGMENTS_PER_EDGE = 16;
1621
1950
  const SCALE_SEARCH_BOUNDARY_DIVISIONS = 256;
@@ -1737,6 +2066,17 @@ function epsgFor(crs) {
1737
2066
  if (crs.kind === "ups") return crs.hemisphere === "north" ? 32661 : 32761;
1738
2067
  return (crs.hemisphere === "north" ? 32600 : 32700) + crs.zone;
1739
2068
  }
2069
+ function mgrsGridZoneFor(crs, center) {
2070
+ const longitude = center[0];
2071
+ if (crs.kind === "ups") {
2072
+ if (crs.hemisphere === "north") return longitude < 0 ? "Y" : "Z";
2073
+ return longitude < 0 ? "A" : "B";
2074
+ }
2075
+ const latitude = center[1];
2076
+ const bands = "CDEFGHJKLMNPQRSTUVWX";
2077
+ const bandIndex = Math.min(19, Math.max(0, Math.floor((latitude + 80) / 8)));
2078
+ return `${crs.zone}${bands[bandIndex]}`;
2079
+ }
1740
2080
  function scaleDeviationWarning(maximumRelativeDeviation) {
1741
2081
  return maximumRelativeDeviation >= .001 ? [{
1742
2082
  code: "scale-deviation",
@@ -1771,6 +2111,13 @@ function createTacticalMapSheetDocument(request) {
1771
2111
  projectedMetresPerMillimetre,
1772
2112
  frame: mapFrame
1773
2113
  });
2114
+ if (request.registration && request.page.background !== void 0 && request.page.background !== "transparent") throw new RangeError("Registered overlays require a transparent page background");
2115
+ const renderedRegistration = request.registration && renderRegistration(request.registration, {
2116
+ crs,
2117
+ center,
2118
+ projectedMetresPerMillimetre,
2119
+ frame: mapFrame
2120
+ });
1774
2121
  const epsg = epsgFor(crs);
1775
2122
  const renderedMarginalia = request.marginalia && renderMarginalia(request.marginalia, {
1776
2123
  page,
@@ -1778,17 +2125,23 @@ function createTacticalMapSheetDocument(request) {
1778
2125
  scaleDenominator: request.scaleDenominator,
1779
2126
  crs,
1780
2127
  epsg,
2128
+ mgrsGridZone: mgrsGridZoneFor(crs, request.center),
1781
2129
  convergenceDegrees: center.convergenceDegrees,
1782
2130
  footprintRing,
1783
2131
  metadata: request.metadata
1784
2132
  });
1785
- const fontStyles = [...renderedGrid || renderedMarginalia ? ["Regular"] : [], ...renderedLayers.fontStyles];
2133
+ const renderedPrinterCalibration = request.printerCalibration && renderPrinterCalibration(request.printerCalibration, page, mapFrame);
2134
+ const fontStyles = [...renderedGrid || renderedMarginalia || renderedRegistration || renderedPrinterCalibration ? ["Regular"] : [], ...renderedLayers.fontStyles];
1786
2135
  const scene = {
1787
2136
  page,
1788
2137
  mapFrame,
1789
2138
  ...request.page.background && request.page.background !== "transparent" ? { backgroundColor: request.page.background } : {},
1790
- layers: [...renderedGrid ? [renderedGrid.layer] : [], ...renderedLayers.layers],
1791
- ...renderedMarginalia ? { marginalia: [renderedMarginalia] } : {},
2139
+ layers: [
2140
+ ...renderedGrid ? [renderedGrid.layer] : [],
2141
+ ...renderedLayers.layers,
2142
+ ...renderedRegistration ? [renderedRegistration.layer] : []
2143
+ ],
2144
+ ...renderedMarginalia || renderedPrinterCalibration ? { marginalia: [...renderedMarginalia ? [renderedMarginalia] : [], ...renderedPrinterCalibration ? [renderedPrinterCalibration] : []] } : {},
1792
2145
  fonts: openSansResources(fontStyles),
1793
2146
  patterns: renderedLayers.patterns
1794
2147
  };
@@ -1809,6 +2162,7 @@ function createTacticalMapSheetDocument(request) {
1809
2162
  fonts: openSansIdentities(fontStyles),
1810
2163
  patterns: renderedLayers.patterns.map((pattern) => pattern.identity)
1811
2164
  },
2165
+ ...renderedRegistration ? { registrationMarks: renderedRegistration.marks } : {},
1812
2166
  metadata: {
1813
2167
  center: [request.center[0], request.center[1]],
1814
2168
  scaleDenominator: request.scaleDenominator,
@@ -1831,4 +2185,4 @@ function createTacticalMapSheet(request) {
1831
2185
  return createTacticalMapSheetDocument(request).result;
1832
2186
  }
1833
2187
  //#endregion
1834
- export { POINTS_PER_MILLIMETRE as a, MILLIMETRES_PER_CSS_PIXEL as i, createTacticalMapSheetDocument as n, openSansCentralBaselineOffset as o, formatDecimal as r, createTacticalMapSheet as t };
2188
+ export { mgrsToProjected as a, projectedToMgrs as c, formatDecimal as d, MILLIMETRES_PER_CSS_PIXEL as f, mgrsToGeographic as i, snapGeographicToMgrs as l, openSansCentralBaselineOffset as m, createTacticalMapSheetDocument as n, normalizeMgrsReference as o, POINTS_PER_MILLIMETRE as p, geographicToMgrs as r, parseMgrsReference as s, createTacticalMapSheet as t, snapProjectedToMgrs as u };
@@ -200,6 +200,10 @@ interface PhysicalPageSize {
200
200
  readonly widthMm: number;
201
201
  readonly heightMm: number;
202
202
  }
203
+ interface ScenePoint {
204
+ readonly xMm: number;
205
+ readonly yMm: number;
206
+ }
203
207
  interface MapSheetPatternIdentity {
204
208
  readonly id: string;
205
209
  readonly pattern: PatternId;
@@ -210,10 +214,14 @@ interface MapSheetPatternIdentity {
210
214
  readonly phaseYMm: number;
211
215
  }
212
216
  //#endregion
217
+ //#region src/mgrs-domain.d.ts
218
+ declare const MGRS_SNAP_INTERVALS: readonly [100, 1000, 10000, 100000];
219
+ type MgrsPrecision = 0 | 1 | 2 | 3 | 4 | 5;
220
+ type MgrsSnapInterval = (typeof MGRS_SNAP_INTERVALS)[number];
221
+ //#endregion
213
222
  //#region src/mgrs-grid.d.ts
214
- declare const SUPPORTED_INTERVALS: readonly [100, 1000, 10000, 100000];
215
223
  interface MgrsGridOptions {
216
- readonly intervalMeters?: (typeof SUPPORTED_INTERVALS)[number];
224
+ readonly intervalMeters?: MgrsSnapInterval;
217
225
  readonly precision?: 0 | 1 | 2 | 3;
218
226
  readonly color?: string;
219
227
  readonly lineWidthCssPixels?: number;
@@ -237,7 +245,10 @@ interface MapSheetMarginaliaOptions {
237
245
  readonly showScale?: boolean;
238
246
  readonly showScaleBar?: boolean;
239
247
  readonly showGridAndNorth?: boolean;
248
+ readonly showFrameCornerMarks?: boolean;
249
+ /** @deprecated Use showFrameCornerMarks. The canonical option takes precedence. */
240
250
  readonly showRegistrationMarks?: boolean;
251
+ /** @deprecated Legacy 10 mm marks. Use the printerCalibration request instead. */
241
252
  readonly showCalibrationMarks?: boolean;
242
253
  readonly showFootprint?: boolean;
243
254
  }
@@ -351,11 +362,42 @@ interface PointSymbolFallbackWarning extends IdentifiedGraphicWarning {
351
362
  type GraphicVisibilityWarning = GraphicClippedWarning | GraphicOutsideFrameWarning | GraphicHiddenWarning;
352
363
  type GraphicRenderWarning = GraphicVisibilityWarning | PointSymbolFallbackWarning;
353
364
  //#endregion
365
+ //#region src/paper.d.ts
366
+ type PaperOrientation = "portrait" | "landscape";
367
+ type PaperSizeName = "a0" | "a1" | "a2" | "a3" | "a4" | "letter" | "legal" | "tabloid" | "ledger" | "ansi-a" | "ansi-b" | "ansi-c" | "ansi-d" | "ansi-e" | "arch-d";
368
+ /** Exact physical paper dimensions in their portrait orientation. */
369
+ declare const PAPER_SIZES: Readonly<Record<PaperSizeName, PhysicalPageSize>>;
370
+ /** Resolve a named paper size into values accepted by `MapSheetPageInput`. */
371
+ declare function resolvePaperSize(name: PaperSizeName, orientation?: PaperOrientation): PhysicalPageSize;
372
+ //#endregion
373
+ //#region src/registration.d.ts
374
+ interface AuthoredRegistrationMark {
375
+ readonly id: string;
376
+ readonly mgrs: string;
377
+ readonly label?: string;
378
+ }
379
+ interface MapSheetRegistrationRequest {
380
+ readonly marks: readonly AuthoredRegistrationMark[];
381
+ readonly snapIntervalMeters?: MgrsSnapInterval;
382
+ }
383
+ interface ResolvedRegistrationMark {
384
+ readonly id: string;
385
+ readonly mgrs: string;
386
+ /** GeoJSON coordinate order: longitude, then latitude. */
387
+ readonly position: Position;
388
+ readonly sheetPositionMm: ScenePoint;
389
+ }
390
+ //#endregion
391
+ //#region src/printer-calibration.d.ts
392
+ interface PrinterCalibrationRequest {
393
+ readonly lengthMm?: number;
394
+ }
395
+ //#endregion
354
396
  //#region src/create-tactical-map-sheet.d.ts
355
397
  interface MapSheetPageInput extends PhysicalPageSize {
356
398
  readonly marginMm?: number;
357
399
  readonly background?: string | "transparent";
358
- readonly orientation?: "portrait" | "landscape";
400
+ readonly orientation?: PaperOrientation;
359
401
  }
360
402
  interface TacticalMapSheetRequest {
361
403
  readonly page: MapSheetPageInput;
@@ -368,6 +410,8 @@ interface TacticalMapSheetRequest {
368
410
  readonly pointSymbols?: MapSheetPointSymbolCapability;
369
411
  readonly marginalia?: MapSheetMarginaliaOptions | false;
370
412
  readonly metadata?: MapSheetMetadata;
413
+ readonly registration?: MapSheetRegistrationRequest;
414
+ readonly printerCalibration?: PrinterCalibrationRequest;
371
415
  }
372
416
  type UtmProjection = UtmCrs;
373
417
  type UpsProjection = UpsCrs;
@@ -419,7 +463,8 @@ interface TacticalMapSheetResult {
419
463
  readonly warnings: readonly TacticalMapSheetWarning[];
420
464
  readonly metadata: ResolvedMapSheetMetadata;
421
465
  readonly resources: TacticalMapSheetResources;
466
+ readonly registrationMarks?: readonly ResolvedRegistrationMark[];
422
467
  }
423
468
  declare function createTacticalMapSheet(request: TacticalMapSheetRequest): TacticalMapSheetResult;
424
469
  //#endregion
425
- export { MgrsGridOptions as A, MapSheetCustomSymbol as C, VectorSymbolAnchor as D, MapSheetSymbolSize as E, MapSheetPatternIdentity as M, Polygon as N, MapSheetMarginaliaOptions as O, ControlledVectorSymbol as S, MapSheetPointSymbolCapability as T, GraphicOutsideFrameWarning as _, ResolvedMapSheetMetadata as a, PointSymbolFallbackWarning as b, TacticalMapSheetResources as c, UpsProjection as d, UtmProjection as f, GraphicHiddenWarning as g, GraphicClippedWarning as h, MapSheetPageInput as i, ResolvedMgrsGridMetadata as j, MapSheetMetadata as k, TacticalMapSheetResult as l, AuthoredMapSheetGraphic as m, CrossZoneFootprintWarning as n, ScaleDeviationWarning as o, createTacticalMapSheet as p, MapSheetFontIdentity as r, TacticalMapSheetRequest as s, CrossProjectionFootprintWarning as t, TacticalMapSheetWarning as u, GraphicRenderWarning as v, MapSheetPointSymbol as w, TacticalMapSheetLayer as x, MapSheetPortrayalDefaults as y };
470
+ export { ControlledVectorSymbol as A, MgrsPrecision as B, GraphicClippedWarning as C, MapSheetPortrayalDefaults as D, GraphicRenderWarning as E, VectorSymbolAnchor as F, UpsCrs as G, MapSheetPatternIdentity as H, MapSheetMarginaliaOptions as I, Position as J, UtmCrs as K, MapSheetMetadata as L, MapSheetPointSymbol as M, MapSheetPointSymbolCapability as N, PointSymbolFallbackWarning as O, MapSheetSymbolSize as P, MgrsGridOptions as R, AuthoredMapSheetGraphic as S, GraphicOutsideFrameWarning as T, GeographicPosition as U, MgrsSnapInterval as V, GridCrs as W, ResolvedRegistrationMark as _, ResolvedMapSheetMetadata as a, PaperSizeName as b, TacticalMapSheetResources as c, UpsProjection as d, UtmProjection as f, MapSheetRegistrationRequest as g, AuthoredRegistrationMark as h, MapSheetPageInput as i, MapSheetCustomSymbol as j, TacticalMapSheetLayer as k, TacticalMapSheetResult as l, PrinterCalibrationRequest as m, CrossZoneFootprintWarning as n, ScaleDeviationWarning as o, createTacticalMapSheet as p, Polygon as q, MapSheetFontIdentity as r, TacticalMapSheetRequest as s, CrossProjectionFootprintWarning as t, TacticalMapSheetWarning as u, PAPER_SIZES as v, GraphicHiddenWarning as w, resolvePaperSize as x, PaperOrientation as y, ResolvedMgrsGridMetadata as z };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,27 @@
1
- import { A as MgrsGridOptions, C as MapSheetCustomSymbol, D as VectorSymbolAnchor, E as MapSheetSymbolSize, M as MapSheetPatternIdentity, O as MapSheetMarginaliaOptions, S as ControlledVectorSymbol, T as MapSheetPointSymbolCapability, _ as GraphicOutsideFrameWarning, a as ResolvedMapSheetMetadata, b as PointSymbolFallbackWarning, c as TacticalMapSheetResources, d as UpsProjection, f as UtmProjection, g as GraphicHiddenWarning, h as GraphicClippedWarning, i as MapSheetPageInput, j as ResolvedMgrsGridMetadata, k as MapSheetMetadata, l as TacticalMapSheetResult, m as AuthoredMapSheetGraphic, n as CrossZoneFootprintWarning, o as ScaleDeviationWarning, p as createTacticalMapSheet, r as MapSheetFontIdentity, s as TacticalMapSheetRequest, t as CrossProjectionFootprintWarning, u as TacticalMapSheetWarning, v as GraphicRenderWarning, w as MapSheetPointSymbol, x as TacticalMapSheetLayer, y as MapSheetPortrayalDefaults } from "./create-tactical-map-sheet-DAKVJ3Bx.mjs";
2
- export { type AuthoredMapSheetGraphic, type ControlledVectorSymbol, type CrossProjectionFootprintWarning, type CrossZoneFootprintWarning, type GraphicClippedWarning, type GraphicHiddenWarning, type GraphicOutsideFrameWarning, type GraphicRenderWarning, type MapSheetCustomSymbol, type MapSheetFontIdentity, type MapSheetMarginaliaOptions, type MapSheetMetadata, type MapSheetPageInput, type MapSheetPatternIdentity, type MapSheetPointSymbol, type MapSheetPointSymbolCapability, type MapSheetPortrayalDefaults, type MapSheetSymbolSize, type MgrsGridOptions, type PointSymbolFallbackWarning, type ResolvedMapSheetMetadata, type ResolvedMgrsGridMetadata, type ScaleDeviationWarning, type TacticalMapSheetLayer, type TacticalMapSheetRequest, type TacticalMapSheetResources, type TacticalMapSheetResult, type TacticalMapSheetWarning, type UpsProjection, type UtmProjection, type VectorSymbolAnchor, createTacticalMapSheet };
1
+ import { A as ControlledVectorSymbol, B as MgrsPrecision, C as GraphicClippedWarning, D as MapSheetPortrayalDefaults, E as GraphicRenderWarning, F as VectorSymbolAnchor, G as UpsCrs, H as MapSheetPatternIdentity, I as MapSheetMarginaliaOptions, J as Position, K as UtmCrs, L as MapSheetMetadata, M as MapSheetPointSymbol, N as MapSheetPointSymbolCapability, O as PointSymbolFallbackWarning, P as MapSheetSymbolSize, R as MgrsGridOptions, S as AuthoredMapSheetGraphic, T as GraphicOutsideFrameWarning, U as GeographicPosition, V as MgrsSnapInterval, W as GridCrs, _ as ResolvedRegistrationMark, a as ResolvedMapSheetMetadata, b as PaperSizeName, c as TacticalMapSheetResources, d as UpsProjection, f as UtmProjection, g as MapSheetRegistrationRequest, h as AuthoredRegistrationMark, i as MapSheetPageInput, j as MapSheetCustomSymbol, k as TacticalMapSheetLayer, l as TacticalMapSheetResult, m as PrinterCalibrationRequest, n as CrossZoneFootprintWarning, o as ScaleDeviationWarning, p as createTacticalMapSheet, r as MapSheetFontIdentity, s as TacticalMapSheetRequest, t as CrossProjectionFootprintWarning, u as TacticalMapSheetWarning, v as PAPER_SIZES, w as GraphicHiddenWarning, x as resolvePaperSize, y as PaperOrientation, z as ResolvedMgrsGridMetadata } from "./create-tactical-map-sheet-T2q1nHWO.mjs";
2
+
3
+ //#region src/mgrs.d.ts
4
+ interface ProjectedGridCoordinate {
5
+ readonly crs: GridCrs;
6
+ readonly easting: number;
7
+ readonly northing: number;
8
+ }
9
+ interface ResolvedMgrsReference extends ProjectedGridCoordinate {
10
+ readonly normalized: string;
11
+ readonly precision: MgrsPrecision;
12
+ /** GeoJSON coordinate order: longitude, then latitude. */
13
+ readonly position: Position;
14
+ }
15
+ /** Parse a complete MGRS cell and resolve its southwest grid intersection. */
16
+ declare function parseMgrsReference(reference: string): ResolvedMgrsReference;
17
+ declare function normalizeMgrsReference(reference: string): string;
18
+ /** Convert an MGRS cell to its southwest intersection in its native grid CRS. */
19
+ declare function mgrsToProjected(reference: string): ProjectedGridCoordinate;
20
+ /** Convert an MGRS cell to its southwest intersection in GeoJSON coordinate order. */
21
+ declare function mgrsToGeographic(reference: string): Position;
22
+ declare function projectedToMgrs(coordinate: ProjectedGridCoordinate, precision?: MgrsPrecision): string;
23
+ declare function geographicToMgrs(position: GeographicPosition, precision?: MgrsPrecision): string;
24
+ declare function snapProjectedToMgrs(coordinate: ProjectedGridCoordinate, interval: MgrsSnapInterval): ResolvedMgrsReference;
25
+ declare function snapGeographicToMgrs(position: GeographicPosition, interval: MgrsSnapInterval): ResolvedMgrsReference;
26
+ //#endregion
27
+ export { type AuthoredMapSheetGraphic, type AuthoredRegistrationMark, type ControlledVectorSymbol, type CrossProjectionFootprintWarning, type CrossZoneFootprintWarning, type GraphicClippedWarning, type GraphicHiddenWarning, type GraphicOutsideFrameWarning, type GraphicRenderWarning, type GridCrs, type MapSheetCustomSymbol, type MapSheetFontIdentity, type MapSheetMarginaliaOptions, type MapSheetMetadata, type MapSheetPageInput, type MapSheetPatternIdentity, type MapSheetPointSymbol, type MapSheetPointSymbolCapability, type MapSheetPortrayalDefaults, type MapSheetRegistrationRequest, type MapSheetSymbolSize, type MgrsGridOptions, type MgrsPrecision, type MgrsSnapInterval, PAPER_SIZES, type PaperOrientation, type PaperSizeName, type PointSymbolFallbackWarning, type PrinterCalibrationRequest, type ProjectedGridCoordinate, type ResolvedMapSheetMetadata, type ResolvedMgrsGridMetadata, type ResolvedMgrsReference, type ResolvedRegistrationMark, type ScaleDeviationWarning, type TacticalMapSheetLayer, type TacticalMapSheetRequest, type TacticalMapSheetResources, type TacticalMapSheetResult, type TacticalMapSheetWarning, type UpsCrs, type UpsProjection, type UtmCrs, type UtmProjection, type VectorSymbolAnchor, createTacticalMapSheet, geographicToMgrs, mgrsToGeographic, mgrsToProjected, normalizeMgrsReference, parseMgrsReference, projectedToMgrs, resolvePaperSize, snapGeographicToMgrs, snapProjectedToMgrs };
package/dist/index.mjs CHANGED
@@ -1,2 +1,83 @@
1
- import { t as createTacticalMapSheet } from "./create-tactical-map-sheet-Wpo49_IR.mjs";
2
- export { createTacticalMapSheet };
1
+ import { a as mgrsToProjected, c as projectedToMgrs, i as mgrsToGeographic, l as snapGeographicToMgrs, o as normalizeMgrsReference, r as geographicToMgrs, s as parseMgrsReference, t as createTacticalMapSheet, u as snapProjectedToMgrs } from "./create-tactical-map-sheet-DSmO71Oa.mjs";
2
+ //#region src/paper.ts
3
+ const A0 = Object.freeze({
4
+ widthMm: 841,
5
+ heightMm: 1189
6
+ });
7
+ const A1 = Object.freeze({
8
+ widthMm: 594,
9
+ heightMm: 841
10
+ });
11
+ const A2 = Object.freeze({
12
+ widthMm: 420,
13
+ heightMm: 594
14
+ });
15
+ const A3 = Object.freeze({
16
+ widthMm: 297,
17
+ heightMm: 420
18
+ });
19
+ const A4 = Object.freeze({
20
+ widthMm: 210,
21
+ heightMm: 297
22
+ });
23
+ const LETTER = Object.freeze({
24
+ widthMm: 215.9,
25
+ heightMm: 279.4
26
+ });
27
+ const LEGAL = Object.freeze({
28
+ widthMm: 215.9,
29
+ heightMm: 355.6
30
+ });
31
+ const TABLOID = Object.freeze({
32
+ widthMm: 279.4,
33
+ heightMm: 431.8
34
+ });
35
+ const ANSI_C = Object.freeze({
36
+ widthMm: 431.8,
37
+ heightMm: 558.8
38
+ });
39
+ const ANSI_D = Object.freeze({
40
+ widthMm: 558.8,
41
+ heightMm: 863.6
42
+ });
43
+ const ANSI_E = Object.freeze({
44
+ widthMm: 863.6,
45
+ heightMm: 1117.6
46
+ });
47
+ const ARCH_D = Object.freeze({
48
+ widthMm: 609.6,
49
+ heightMm: 914.4
50
+ });
51
+ /** Exact physical paper dimensions in their portrait orientation. */
52
+ const PAPER_SIZES = Object.freeze({
53
+ a0: A0,
54
+ a1: A1,
55
+ a2: A2,
56
+ a3: A3,
57
+ a4: A4,
58
+ letter: LETTER,
59
+ legal: LEGAL,
60
+ tabloid: TABLOID,
61
+ ledger: TABLOID,
62
+ "ansi-a": LETTER,
63
+ "ansi-b": TABLOID,
64
+ "ansi-c": ANSI_C,
65
+ "ansi-d": ANSI_D,
66
+ "ansi-e": ANSI_E,
67
+ "arch-d": ARCH_D
68
+ });
69
+ /** Resolve a named paper size into values accepted by `MapSheetPageInput`. */
70
+ function resolvePaperSize(name, orientation = "portrait") {
71
+ const size = PAPER_SIZES[name];
72
+ if (!size) throw new RangeError(`Unknown paper size: ${String(name)}`);
73
+ if (orientation !== "portrait" && orientation !== "landscape") throw new RangeError("Paper orientation must be portrait or landscape");
74
+ return orientation === "portrait" ? {
75
+ widthMm: size.widthMm,
76
+ heightMm: size.heightMm
77
+ } : {
78
+ widthMm: size.heightMm,
79
+ heightMm: size.widthMm
80
+ };
81
+ }
82
+ //#endregion
83
+ export { PAPER_SIZES, createTacticalMapSheet, geographicToMgrs, mgrsToGeographic, mgrsToProjected, normalizeMgrsReference, parseMgrsReference, projectedToMgrs, resolvePaperSize, snapGeographicToMgrs, snapProjectedToMgrs };
package/dist/pdf.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { N as Polygon, a as ResolvedMapSheetMetadata, c as TacticalMapSheetResources, s as TacticalMapSheetRequest, u as TacticalMapSheetWarning } from "./create-tactical-map-sheet-DAKVJ3Bx.mjs";
1
+ import { a as ResolvedMapSheetMetadata, c as TacticalMapSheetResources, l as TacticalMapSheetResult, q as Polygon, s as TacticalMapSheetRequest, u as TacticalMapSheetWarning } from "./create-tactical-map-sheet-T2q1nHWO.mjs";
2
2
 
3
3
  //#region src/pdf.d.ts
4
4
  interface TacticalMapSheetPdfResult {
@@ -7,6 +7,7 @@ interface TacticalMapSheetPdfResult {
7
7
  readonly warnings: readonly TacticalMapSheetWarning[];
8
8
  readonly metadata: ResolvedMapSheetMetadata;
9
9
  readonly resources: TacticalMapSheetResources;
10
+ readonly registrationMarks?: TacticalMapSheetResult["registrationMarks"];
10
11
  }
11
12
  declare function renderMapSheetPdf(request: TacticalMapSheetRequest): TacticalMapSheetPdfResult;
12
13
  //#endregion
package/dist/pdf.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as POINTS_PER_MILLIMETRE, i as MILLIMETRES_PER_CSS_PIXEL, n as createTacticalMapSheetDocument, o as openSansCentralBaselineOffset, r as formatDecimal } from "./create-tactical-map-sheet-Wpo49_IR.mjs";
1
+ import { d as formatDecimal, f as MILLIMETRES_PER_CSS_PIXEL, m as openSansCentralBaselineOffset, n as createTacticalMapSheetDocument, p as POINTS_PER_MILLIMETRE } from "./create-tactical-map-sheet-DSmO71Oa.mjs";
2
2
  import { jsPDF } from "jspdf";
3
3
  //#region src/pdf-transparency.ts
4
4
  const FORM_DICTIONARY_MARKER = new TextEncoder().encode("/Subtype /Form\n/BBox");
@@ -676,7 +676,8 @@ function renderMapSheetPdf(request) {
676
676
  footprint: result.footprint,
677
677
  warnings: result.warnings,
678
678
  metadata: result.metadata,
679
- resources: result.resources
679
+ resources: result.resources,
680
+ ...result.registrationMarks ? { registrationMarks: result.registrationMarks } : {}
680
681
  };
681
682
  }
682
683
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orbat-mapper/tactical-map-sheet",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Scale-accurate projected vector tactical map-sheet renderer.",
5
5
  "license": "MIT",
6
6
  "author": "Orbat Mapper",