@orbat-mapper/tactical-map-sheet 0.1.2 → 0.2.1
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 +94 -9
- package/dist/{create-tactical-map-sheet-Wpo49_IR.mjs → create-tactical-map-sheet-Ch8io55k.mjs} +387 -38
- package/dist/{create-tactical-map-sheet-DAKVJ3Bx.d.mts → create-tactical-map-sheet-T2q1nHWO.d.mts} +49 -4
- package/dist/index.d.mts +27 -2
- package/dist/index.mjs +83 -2
- package/dist/pdf.d.mts +2 -1
- package/dist/pdf.mjs +3 -2
- package/package.json +1 -1
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,
|
|
9
|
-
marginalia. One center-selected CRS is retained across zone and polar
|
|
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,60 @@ 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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 two marks must resolve to distinct
|
|
116
|
+
positions. Any invalid mark rejects the complete request; marks are never omitted or moved.
|
|
117
|
+
|
|
118
|
+
Registered overlays are transparent. An explicit non-transparent `page.background` is rejected.
|
|
119
|
+
SVG and PDF results expose the same ordered `registrationMarks`, each containing the authored ID,
|
|
120
|
+
normalized `mgrs`, GeoJSON-order `position`, and derived `sheetPositionMm`. The optional full MGRS
|
|
121
|
+
grid, tactical layers, and marginalia remain independent.
|
|
122
|
+
|
|
123
|
+
`printerCalibration: {}` adds a 100 mm diagnostic line, endpoint ticks, and length label centered
|
|
124
|
+
in the bottom page margin. Set a positive `lengthMm` to choose another length. The request fails if
|
|
125
|
+
the complete calibration artwork does not fit; calibration never changes sheet geometry, coverage,
|
|
126
|
+
scale, or mark positions.
|
|
127
|
+
|
|
128
|
+
Print registered SVG or PDF output at **100% / Actual Size** with **Fit to page disabled**. If the
|
|
129
|
+
calibration line measures incorrectly, correct the printer settings and reprint; do not compensate
|
|
130
|
+
by resizing the overlay.
|
|
80
131
|
|
|
81
132
|
Point-symbol renderers enter through a capability that returns complete SVG, bounds, doctrinal and
|
|
82
133
|
octagon anchors. Point and custom symbols then use the same conservative vector subset. Raster,
|
|
@@ -142,6 +193,9 @@ Application stores and map-engine output are not part of the public request cont
|
|
|
142
193
|
matching precision 3, 2, 1, or 0; plus color, line width, and label size).
|
|
143
194
|
- `layers`: ordered `TacticalMapSheetLayer[]`; `pointSymbols`: the optional renderer capability;
|
|
144
195
|
`marginalia`: `false` or display options; `metadata`: optional document text.
|
|
196
|
+
- `registration`: optional grouped MGRS marks and `snapIntervalMeters`; its presence activates
|
|
197
|
+
strict transparent registered-overlay validation.
|
|
198
|
+
- `printerCalibration`: optional bottom-margin diagnostic line; `lengthMm` defaults to 100.
|
|
145
199
|
|
|
146
200
|
Each layer has a stable `id`, ordered `{ graphic }[]`, and optional shared portrayal defaults for
|
|
147
201
|
color, line/dash/cap/join, and label size/clamp. A graphic is a control-measure `ControlMeasure`, a
|
|
@@ -153,9 +207,37 @@ must return a `ControlledVectorSymbol`: stable resource ID, controlled-subset SV
|
|
|
153
207
|
height, octagon size, placement anchor, octagon anchor, and optional validation state. Custom symbols
|
|
154
208
|
carry that same vector record directly. Metre sizes may specify pixel clamps.
|
|
155
209
|
|
|
210
|
+
### Paper presets
|
|
211
|
+
|
|
212
|
+
`PAPER_SIZES` contains exact portrait millimetre dimensions for `a0` through `a4`, `letter`, `legal`,
|
|
213
|
+
`tabloid`, `ansi-c`, `ansi-d`, `ansi-e`, and `arch-d`. `ledger` aliases `tabloid`; `ansi-a` aliases
|
|
214
|
+
`letter`; and `ansi-b` aliases `tabloid`, so aliases share one physical definition. ANSI D is
|
|
215
|
+
558.8 × 863.6 mm and remains distinct from 609.6 × 914.4 mm ARCH D.
|
|
216
|
+
|
|
217
|
+
`resolvePaperSize(name, orientation)` returns `{ widthMm, heightMm }` for direct use as a page input.
|
|
218
|
+
Portrait is the default and landscape swaps the axes. Presets are optional conveniences: callers can
|
|
219
|
+
continue supplying arbitrary positive `widthMm` and `heightMm`, which remain authoritative during
|
|
220
|
+
rendering.
|
|
221
|
+
|
|
222
|
+
### MGRS utilities
|
|
223
|
+
|
|
224
|
+
- `parseMgrsReference` validates a complete UTM or UPS reference and returns its compact-uppercase
|
|
225
|
+
form, precision, explicit grid CRS, projected metre coordinates, and GeoJSON-order position.
|
|
226
|
+
- `normalizeMgrsReference`, `mgrsToProjected`, and `mgrsToGeographic` expose focused forms of that
|
|
227
|
+
parsed result. Geographic conversion always returns the southwest grid-cell intersection.
|
|
228
|
+
- `geographicToMgrs` and `projectedToMgrs` format coordinates at precision 0 through 5 (100 km
|
|
229
|
+
through 1 m). Projected input includes its explicit UTM zone/hemisphere or UPS hemisphere.
|
|
230
|
+
- `snapGeographicToMgrs` and `snapProjectedToMgrs` accept 100 m, 1 km, 10 km, or 100 km. Already
|
|
231
|
+
aligned intersections stay fixed; exact halfway ties move toward increasing easting and northing.
|
|
232
|
+
|
|
156
233
|
`MapSheetMarginaliaOptions` controls color and visibility of declared scale, scale bar, grid/north,
|
|
157
|
-
|
|
158
|
-
|
|
234
|
+
frame-corner marks, legacy calibration marks, and footprint. Grid/north marginalia includes the
|
|
235
|
+
MGRS grid-zone designation for the sheet center. Use `showFrameCornerMarks` for the
|
|
236
|
+
canonical ungeoreferenced corner crosses. Deprecated `showRegistrationMarks` remains an alias; when
|
|
237
|
+
both are supplied, `showFrameCornerMarks` takes precedence. Deprecated `showCalibrationMarks`
|
|
238
|
+
continues to control the existing pair of 10 mm legacy marks and is independent of
|
|
239
|
+
`printerCalibration`. `MapSheetMetadata` supplies optional title, subtitle, classification, and
|
|
240
|
+
preparer text.
|
|
159
241
|
|
|
160
242
|
### Results and resolved metadata
|
|
161
243
|
|
|
@@ -168,9 +250,12 @@ subtitle, classification, and preparer text.
|
|
|
168
250
|
hemisphere, EPSG, center point scale, center convergence in degrees, and maximum frame scale
|
|
169
251
|
deviation).
|
|
170
252
|
- `resources`: identities for embedded Open Sans fonts and deterministic pattern resources.
|
|
253
|
+
- `registrationMarks`: when registered, the ordered resolved IDs, normalized MGRS references,
|
|
254
|
+
longitude/latitude positions, and physical sheet positions in millimetres.
|
|
171
255
|
|
|
172
256
|
`renderMapSheetPdf(request)` accepts the identical request and returns `TacticalMapSheetPdfResult`:
|
|
173
|
-
`pdf: Uint8Array` plus the same footprint, warnings, metadata,
|
|
257
|
+
`pdf: Uint8Array` plus the same footprint, warnings, metadata, resources, and resolved registration
|
|
258
|
+
marks as the SVG result.
|
|
174
259
|
|
|
175
260
|
### Warning union
|
|
176
261
|
|
package/dist/{create-tactical-map-sheet-Wpo49_IR.mjs → create-tactical-map-sheet-Ch8io55k.mjs}
RENAMED
|
@@ -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-
|
|
281
|
-
const
|
|
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 =
|
|
311
|
+
const automatic = MGRS_SNAP_INTERVALS.find((candidate) => candidate / context.projectedMetresPerMillimetre >= 10);
|
|
289
312
|
const intervalMeters = options.intervalMeters ?? automatic ?? 1e5;
|
|
290
|
-
if (!
|
|
291
|
-
const expectedPrecision =
|
|
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(
|
|
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
|
|
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,324 @@ 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 hasDistinctPair(marks) {
|
|
1818
|
+
for (let left = 0; left < marks.length; left += 1) for (let right = left + 1; right < marks.length; right += 1) {
|
|
1819
|
+
const first = marks[left].sheetPositionMm;
|
|
1820
|
+
const second = marks[right].sheetPositionMm;
|
|
1821
|
+
if (first.xMm !== second.xMm || first.yMm !== second.yMm) return true;
|
|
1822
|
+
}
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1825
|
+
function renderRegistration(request, context) {
|
|
1826
|
+
if (!Array.isArray(request.marks) || request.marks.length < 2) throw new RangeError("Registration requires at least two authored marks");
|
|
1827
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1828
|
+
const graphics = [];
|
|
1829
|
+
const marks = [];
|
|
1830
|
+
for (const authored of request.marks) {
|
|
1831
|
+
if (typeof authored.id !== "string" || authored.id.trim() === "") throw new RangeError("Every registration mark requires a non-empty ID");
|
|
1832
|
+
const idKey = authored.id.trim();
|
|
1833
|
+
if (ids.has(idKey)) throw new RangeError(`Registration mark ID ${JSON.stringify(authored.id)} must be unique`);
|
|
1834
|
+
ids.add(idKey);
|
|
1835
|
+
const parsed = parseMgrsReference(authored.mgrs);
|
|
1836
|
+
const resolved = request.snapIntervalMeters !== void 0 ? snapProjectedToMgrs(parsed, request.snapIntervalMeters) : parsed;
|
|
1837
|
+
if (!sameCrs(resolved.crs, context.crs)) throw new RangeError(`Registration mark ${JSON.stringify(authored.id)} must use the sheet grid coordinate reference system`);
|
|
1838
|
+
const sheetPositionMm = projectedToScenePoint(resolved, context);
|
|
1839
|
+
const labels = authored.label && authored.label.trim() !== "" ? [createCustomLabel(authored.label, sheetPositionMm, context.frame, authored.id)] : createCoordinateLabels(resolved, sheetPositionMm, authored.id);
|
|
1840
|
+
assertMarkFits(sheetPositionMm, labels, context.frame, authored.id);
|
|
1841
|
+
marks.push({
|
|
1842
|
+
id: authored.id,
|
|
1843
|
+
mgrs: resolved.normalized,
|
|
1844
|
+
position: [resolved.position[0], resolved.position[1]],
|
|
1845
|
+
sheetPositionMm
|
|
1846
|
+
});
|
|
1847
|
+
const elements = [
|
|
1848
|
+
createCrosshairAxis([{
|
|
1849
|
+
xMm: sheetPositionMm.xMm - CROSSHAIR_HALF_SIZE_MM,
|
|
1850
|
+
yMm: sheetPositionMm.yMm
|
|
1851
|
+
}, {
|
|
1852
|
+
xMm: sheetPositionMm.xMm + CROSSHAIR_HALF_SIZE_MM,
|
|
1853
|
+
yMm: sheetPositionMm.yMm
|
|
1854
|
+
}], authored.id, "horizontal"),
|
|
1855
|
+
createCrosshairAxis([{
|
|
1856
|
+
xMm: sheetPositionMm.xMm,
|
|
1857
|
+
yMm: sheetPositionMm.yMm - CROSSHAIR_HALF_SIZE_MM
|
|
1858
|
+
}, {
|
|
1859
|
+
xMm: sheetPositionMm.xMm,
|
|
1860
|
+
yMm: sheetPositionMm.yMm + CROSSHAIR_HALF_SIZE_MM
|
|
1861
|
+
}], authored.id, "vertical"),
|
|
1862
|
+
...labels
|
|
1863
|
+
];
|
|
1864
|
+
graphics.push({
|
|
1865
|
+
id: authored.id,
|
|
1866
|
+
elements
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
if (!hasDistinctPair(marks)) throw new RangeError("Registration requires marks at two distinct positions");
|
|
1870
|
+
return {
|
|
1871
|
+
layer: {
|
|
1872
|
+
id: "registration-marks-0",
|
|
1873
|
+
graphics,
|
|
1874
|
+
attributes: { "data-registration-marks": marks.length }
|
|
1875
|
+
},
|
|
1876
|
+
marks
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
//#endregion
|
|
1880
|
+
//#region src/printer-calibration.ts
|
|
1881
|
+
const DEFAULT_LENGTH_MM = 100;
|
|
1882
|
+
const TICK_HALF_HEIGHT_MM = 2;
|
|
1883
|
+
const LABEL_OFFSET_MM = 2.5;
|
|
1884
|
+
const STROKE_WIDTH_MM = DEFAULT_LINE_STROKE_WIDTH_MM;
|
|
1885
|
+
function line(points, attributes) {
|
|
1886
|
+
return createSceneLine(points, "#000000", { attributes });
|
|
1887
|
+
}
|
|
1888
|
+
function renderPrinterCalibration(request, page, frame) {
|
|
1889
|
+
const lengthMm = request.lengthMm ?? DEFAULT_LENGTH_MM;
|
|
1890
|
+
requirePositiveFinite(lengthMm, "Printer-calibration length");
|
|
1891
|
+
const centerX = page.widthMm / 2;
|
|
1892
|
+
const frameBottom = frame.yMm + frame.heightMm;
|
|
1893
|
+
const yMm = frameBottom + (page.heightMm - frameBottom) / 2;
|
|
1894
|
+
const startX = centerX - lengthMm / 2;
|
|
1895
|
+
const endX = centerX + lengthMm / 2;
|
|
1896
|
+
const label = createRegularSceneText({
|
|
1897
|
+
text: `${lengthMm} mm`,
|
|
1898
|
+
xMm: centerX,
|
|
1899
|
+
yMm: yMm - LABEL_OFFSET_MM,
|
|
1900
|
+
sizeCssPixels: 6,
|
|
1901
|
+
color: "#000000",
|
|
1902
|
+
attributes: { "data-printer-calibration-label": lengthMm }
|
|
1903
|
+
});
|
|
1904
|
+
const strokeEdge = STROKE_WIDTH_MM / 2;
|
|
1905
|
+
const fitsHorizontally = startX - strokeEdge >= 0 && endX + strokeEdge <= page.widthMm;
|
|
1906
|
+
const labelLeft = label.position.xMm - label.widthMm / 2;
|
|
1907
|
+
const labelRight = label.position.xMm + label.widthMm / 2;
|
|
1908
|
+
const contentTop = Math.min(label.position.yMm - label.heightMm / 2, yMm - TICK_HALF_HEIGHT_MM - strokeEdge);
|
|
1909
|
+
const contentBottom = Math.max(label.position.yMm + label.heightMm / 2, yMm + TICK_HALF_HEIGHT_MM + strokeEdge);
|
|
1910
|
+
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");
|
|
1911
|
+
return {
|
|
1912
|
+
id: "printer-calibration-0",
|
|
1913
|
+
graphics: [{
|
|
1914
|
+
id: "printer-calibration",
|
|
1915
|
+
elements: [
|
|
1916
|
+
line([{
|
|
1917
|
+
xMm: startX,
|
|
1918
|
+
yMm
|
|
1919
|
+
}, {
|
|
1920
|
+
xMm: endX,
|
|
1921
|
+
yMm
|
|
1922
|
+
}], { "data-printer-calibration-length-mm": lengthMm }),
|
|
1923
|
+
line([{
|
|
1924
|
+
xMm: startX,
|
|
1925
|
+
yMm: yMm - TICK_HALF_HEIGHT_MM
|
|
1926
|
+
}, {
|
|
1927
|
+
xMm: startX,
|
|
1928
|
+
yMm: yMm + TICK_HALF_HEIGHT_MM
|
|
1929
|
+
}], { "data-printer-calibration-tick": "start" }),
|
|
1930
|
+
line([{
|
|
1931
|
+
xMm: endX,
|
|
1932
|
+
yMm: yMm - TICK_HALF_HEIGHT_MM
|
|
1933
|
+
}, {
|
|
1934
|
+
xMm: endX,
|
|
1935
|
+
yMm: yMm + TICK_HALF_HEIGHT_MM
|
|
1936
|
+
}], { "data-printer-calibration-tick": "end" }),
|
|
1937
|
+
label
|
|
1938
|
+
]
|
|
1939
|
+
}]
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
//#endregion
|
|
1619
1943
|
//#region src/create-tactical-map-sheet.ts
|
|
1620
1944
|
const FOOTPRINT_SEGMENTS_PER_EDGE = 16;
|
|
1621
1945
|
const SCALE_SEARCH_BOUNDARY_DIVISIONS = 256;
|
|
@@ -1737,6 +2061,17 @@ function epsgFor(crs) {
|
|
|
1737
2061
|
if (crs.kind === "ups") return crs.hemisphere === "north" ? 32661 : 32761;
|
|
1738
2062
|
return (crs.hemisphere === "north" ? 32600 : 32700) + crs.zone;
|
|
1739
2063
|
}
|
|
2064
|
+
function mgrsGridZoneFor(crs, center) {
|
|
2065
|
+
const longitude = center[0];
|
|
2066
|
+
if (crs.kind === "ups") {
|
|
2067
|
+
if (crs.hemisphere === "north") return longitude < 0 ? "Y" : "Z";
|
|
2068
|
+
return longitude < 0 ? "A" : "B";
|
|
2069
|
+
}
|
|
2070
|
+
const latitude = center[1];
|
|
2071
|
+
const bands = "CDEFGHJKLMNPQRSTUVWX";
|
|
2072
|
+
const bandIndex = Math.min(19, Math.max(0, Math.floor((latitude + 80) / 8)));
|
|
2073
|
+
return `${crs.zone}${bands[bandIndex]}`;
|
|
2074
|
+
}
|
|
1740
2075
|
function scaleDeviationWarning(maximumRelativeDeviation) {
|
|
1741
2076
|
return maximumRelativeDeviation >= .001 ? [{
|
|
1742
2077
|
code: "scale-deviation",
|
|
@@ -1771,6 +2106,13 @@ function createTacticalMapSheetDocument(request) {
|
|
|
1771
2106
|
projectedMetresPerMillimetre,
|
|
1772
2107
|
frame: mapFrame
|
|
1773
2108
|
});
|
|
2109
|
+
if (request.registration && request.page.background !== void 0 && request.page.background !== "transparent") throw new RangeError("Registered overlays require a transparent page background");
|
|
2110
|
+
const renderedRegistration = request.registration && renderRegistration(request.registration, {
|
|
2111
|
+
crs,
|
|
2112
|
+
center,
|
|
2113
|
+
projectedMetresPerMillimetre,
|
|
2114
|
+
frame: mapFrame
|
|
2115
|
+
});
|
|
1774
2116
|
const epsg = epsgFor(crs);
|
|
1775
2117
|
const renderedMarginalia = request.marginalia && renderMarginalia(request.marginalia, {
|
|
1776
2118
|
page,
|
|
@@ -1778,17 +2120,23 @@ function createTacticalMapSheetDocument(request) {
|
|
|
1778
2120
|
scaleDenominator: request.scaleDenominator,
|
|
1779
2121
|
crs,
|
|
1780
2122
|
epsg,
|
|
2123
|
+
mgrsGridZone: mgrsGridZoneFor(crs, request.center),
|
|
1781
2124
|
convergenceDegrees: center.convergenceDegrees,
|
|
1782
2125
|
footprintRing,
|
|
1783
2126
|
metadata: request.metadata
|
|
1784
2127
|
});
|
|
1785
|
-
const
|
|
2128
|
+
const renderedPrinterCalibration = request.printerCalibration && renderPrinterCalibration(request.printerCalibration, page, mapFrame);
|
|
2129
|
+
const fontStyles = [...renderedGrid || renderedMarginalia || renderedRegistration || renderedPrinterCalibration ? ["Regular"] : [], ...renderedLayers.fontStyles];
|
|
1786
2130
|
const scene = {
|
|
1787
2131
|
page,
|
|
1788
2132
|
mapFrame,
|
|
1789
2133
|
...request.page.background && request.page.background !== "transparent" ? { backgroundColor: request.page.background } : {},
|
|
1790
|
-
layers: [
|
|
1791
|
-
|
|
2134
|
+
layers: [
|
|
2135
|
+
...renderedGrid ? [renderedGrid.layer] : [],
|
|
2136
|
+
...renderedLayers.layers,
|
|
2137
|
+
...renderedRegistration ? [renderedRegistration.layer] : []
|
|
2138
|
+
],
|
|
2139
|
+
...renderedMarginalia || renderedPrinterCalibration ? { marginalia: [...renderedMarginalia ? [renderedMarginalia] : [], ...renderedPrinterCalibration ? [renderedPrinterCalibration] : []] } : {},
|
|
1792
2140
|
fonts: openSansResources(fontStyles),
|
|
1793
2141
|
patterns: renderedLayers.patterns
|
|
1794
2142
|
};
|
|
@@ -1809,6 +2157,7 @@ function createTacticalMapSheetDocument(request) {
|
|
|
1809
2157
|
fonts: openSansIdentities(fontStyles),
|
|
1810
2158
|
patterns: renderedLayers.patterns.map((pattern) => pattern.identity)
|
|
1811
2159
|
},
|
|
2160
|
+
...renderedRegistration ? { registrationMarks: renderedRegistration.marks } : {},
|
|
1812
2161
|
metadata: {
|
|
1813
2162
|
center: [request.center[0], request.center[1]],
|
|
1814
2163
|
scaleDenominator: request.scaleDenominator,
|
|
@@ -1831,4 +2180,4 @@ function createTacticalMapSheet(request) {
|
|
|
1831
2180
|
return createTacticalMapSheetDocument(request).result;
|
|
1832
2181
|
}
|
|
1833
2182
|
//#endregion
|
|
1834
|
-
export {
|
|
2183
|
+
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 };
|
package/dist/{create-tactical-map-sheet-DAKVJ3Bx.d.mts → create-tactical-map-sheet-T2q1nHWO.d.mts}
RENAMED
|
@@ -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?:
|
|
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?:
|
|
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 {
|
|
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
|
|
2
|
-
|
|
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-
|
|
2
|
-
|
|
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-Ch8io55k.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 {
|
|
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 {
|
|
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-Ch8io55k.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
|