@orbat-mapper/tactical-map-sheet 0.2.2 → 0.3.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/COMPATIBILITY.md +28 -19
- package/README.md +101 -2
- package/dist/{create-tactical-map-sheet-T2q1nHWO.d.mts → create-tactical-map-sheet-Cydh9ghW.d.mts} +55 -2
- package/dist/{create-tactical-map-sheet-Ch8io55k.mjs → create-tactical-map-sheet-D2G1PW2H.mjs} +420 -40
- package/dist/index.d.mts +20 -2
- package/dist/index.mjs +2 -2
- package/dist/pdf.d.mts +1 -1
- package/dist/pdf.mjs +16 -2
- package/package.json +3 -3
package/COMPATIBILITY.md
CHANGED
|
@@ -3,23 +3,32 @@
|
|
|
3
3
|
This audit records the publication gate for the product contract in
|
|
4
4
|
[tactical-graphics#394](https://github.com/orbat-mapper/tactical-graphics/issues/394).
|
|
5
5
|
|
|
6
|
-
| Completion criterion
|
|
7
|
-
|
|
|
8
|
-
| Package remains private until completion
|
|
9
|
-
| Deterministic SVG and PDF, independent of viewport/DPR
|
|
10
|
-
| Exact page dimensions, center scale, and reported scale factors
|
|
11
|
-
| UTM, UPS, MGRS, exceptions, transitions, and polar cases
|
|
12
|
-
| Cross-zone and polar behavior is documented and warning-driven
|
|
13
|
-
| Portable control-measure portrayal and ordering
|
|
14
|
-
| Vector point and custom symbols preserve their contract
|
|
15
|
-
| Complete marginalia
|
|
16
|
-
| No rasterized requested graphics
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
6
|
+
| Completion criterion | Evidence |
|
|
7
|
+
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| Package remains private until completion | Privacy is removed only by the #415 release change; `publication.spec.ts` verifies the public package metadata and entry points. |
|
|
9
|
+
| Deterministic SVG and PDF, independent of viewport/DPR | Canonical byte fixtures and repeated-render assertions live in `product-contract.spec.ts`, `normandy-conformance.spec.ts`, and `pdf.spec.ts`. Rendering accepts no viewport or DPR input. |
|
|
10
|
+
| Exact page dimensions, center scale, and reported scale factors | `create-tactical-map-sheet.spec.ts` verifies physical dimensions, center compensation, footprint, center point scale, and maximum deviation; `pdf.spec.ts` verifies the PDF MediaBox. |
|
|
11
|
+
| UTM, UPS, MGRS, exceptions, transitions, and polar cases | `projection.spec.ts` qualifies GeographicLib/GEOTRANS vectors, hemisphere behavior, Norway/Svalbard rules, explicit zones, transitions, poles, round trips, and MGRS precision; sheet tests cover UTM and UPS grids. |
|
|
12
|
+
| Cross-zone and polar behavior is documented and warning-driven | The README documents `cross-zone-footprint`, `cross-projection-footprint`, and `scale-deviation`; sheet and PDF tests assert compatible structured warnings. |
|
|
13
|
+
| Portable control-measure portrayal and ordering | Normandy and product-contract fixtures cover ground-scale portrayal, canonical defaults/order, patterns, clipping, label clearance, and injected Open Sans metrics. |
|
|
14
|
+
| Vector point and custom symbols preserve their contract | Sheet tests cover bounds, anchors, size, rotation, colors, opacity, amplifiers, fallback warnings, and rejected unsupported SVG; PDF tests cover vector composition. |
|
|
15
|
+
| Complete marginalia | Sheet and product-contract fixtures cover metadata, declared scale, scale bar, CRS/zone, grid and true north, convergence, registration marks, and printable-footprint information. |
|
|
16
|
+
| No rasterized requested graphics | Controlled vector import rejects raster/linked/scripted content; SVG/PDF structural tests assert vector paths, patterns, symbols, and text. |
|
|
17
|
+
| Engine-neutral raster basemap boundary | `raster-plan.spec.ts` verifies shared UTM/UPS target planning and inverse conversion; `raster-basemap.spec.ts` verifies complete layer/tile validation, metadata, warnings, attribution, ordering, and deterministic self-contained SVG/PDF images without engine objects or I/O. |
|
|
18
|
+
| Hybrid SVG/PDF physical parity | `pdf.spec.ts` renders stacked JPEG and transparent PNG layers with opacity beneath searchable vector text and compares sibling SVG/PDF placement and compositing. |
|
|
19
|
+
| Raster quality and attribution are truthful | Binary headers/dimensions, APNG, JPEG orientation, IDs, opacity, and `1e-6` mm tessellation fail before serialization. Effective DPI and anisotropy are structured warnings; required attribution reserves marginalia or fails. |
|
|
20
|
+
| PDF vector parity and exact dimensions | `pdf.spec.ts` checks MediaBox, clipping, patterns, ordering, embedded searchable fonts, deterministic bytes, vector structure, and visual parity with sibling SVG fixtures. |
|
|
21
|
+
| Main SVG entry does not load PDF | `node-packaging.spec.ts` and `browser-packaging.spec.ts` build both exports and verify that the root module graph contains neither jsPDF nor the PDF renderer. |
|
|
22
|
+
| Invalid input throws; all usable loss is identified | Public-boundary tests cover invalid geometry, projection failure, missing capabilities, unsupported vectors, clipping, off-frame and hidden graphics, fallback symbols, zone/projection crossings, and scale deviation. |
|
|
23
|
+
| Independent application adaptation | Closed issue #414 proves TacTrace adapts representative UTM/UPS requests without leaking application, framework, or map-engine types or runtime dependencies into this package. The deferred ORBAT Mapper integration is explicitly excluded from this publication gate by the current #415 product scope. |
|
|
24
|
+
| Required semantic, byte, structural, packaging, and visual coverage | The package suite contains semantic contract tests, checked SVG/PDF bytes, PDF/vector structure assertions, Node/browser packaging qualification, and pinned rendered goldens with explicit tolerances. |
|
|
22
25
|
|
|
23
|
-
The public compatibility boundary is normalized authored layers
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
The public compatibility boundary is normalized authored layers plus optional host-prepared,
|
|
27
|
+
projection-aligned raster basemap bytes in, and deterministic hybrid documents out. Acquisition,
|
|
28
|
+
basemap-style rendering, reprojection, warp qualification, encoding, application stores,
|
|
29
|
+
engine-rendered features, export UI, authorization, preview, and file saving remain application
|
|
30
|
+
responsibilities.
|
|
31
|
+
|
|
32
|
+
The preview MapLibre host's one-padded-source-canvas model, Web Mercator latitude domain, actual
|
|
33
|
+
device limits, and conservative memory ceiling are host qualifications rather than package
|
|
34
|
+
restrictions. The public raster plan continues to resolve and invert both UTM and UPS targets.
|
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ 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
|
-
and complete configurable marginalia. One center-selected CRS is retained across zone and polar
|
|
8
|
+
measures, renderer-neutral point symbols, custom vector symbols, host-prepared raster basemaps,
|
|
9
|
+
physically registered overlays, and complete configurable marginalia. One center-selected CRS is retained across zone and polar
|
|
10
10
|
transitions and any usable distortion or fallback is reported through structured warnings.
|
|
11
11
|
|
|
12
12
|
## Install and entry points
|
|
@@ -56,6 +56,98 @@ projection failures, and SVG content outside the controlled vector subset throw.
|
|
|
56
56
|
result is returned only with an identifying warning; requested content is never silently omitted or
|
|
57
57
|
rasterized.
|
|
58
58
|
|
|
59
|
+
## Host-prepared raster basemaps
|
|
60
|
+
|
|
61
|
+
An ordinary map sheet may include one `rasterBasemap` beneath its MGRS grid and every authored
|
|
62
|
+
vector layer. The basemap is an ordered source-over stack. Each layer has a stable ID, one shared
|
|
63
|
+
opacity, structured attribution, and ordered PNG/JPEG tiles. Each tile has a stable ID, original
|
|
64
|
+
bytes, declared intrinsic pixel dimensions, and a page-relative millimetre destination. Every layer
|
|
65
|
+
must independently tessellate the complete map frame with axis-aligned rectangles; a regular tile
|
|
66
|
+
grid is not required.
|
|
67
|
+
|
|
68
|
+
Resolve the target before a host renders or warps source map pixels:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import {
|
|
72
|
+
mapSheetRasterPointToGeographic,
|
|
73
|
+
resolveMapSheetRasterPlan,
|
|
74
|
+
} from "@orbat-mapper/tactical-map-sheet";
|
|
75
|
+
|
|
76
|
+
const plan = resolveMapSheetRasterPlan({
|
|
77
|
+
page: { widthMm: 297, heightMm: 210, marginMm: 10 },
|
|
78
|
+
center: [-2.5, 49.2],
|
|
79
|
+
scaleDenominator: 25_000,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const topLeft = mapSheetRasterPointToGeographic(plan, 0, 0);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`plan` reports the resolved grid CRS and EPSG code, physical frame, projected bounds, and projected
|
|
86
|
+
metres per millimetre from the same resolver used by document generation. Normalized coordinates
|
|
87
|
+
are finite values from 0 through 1, use a top-left origin, and convert through the package's
|
|
88
|
+
GeographicLib UTM/UPS backend.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
request.rasterBasemap = {
|
|
92
|
+
layers: [
|
|
93
|
+
{
|
|
94
|
+
id: "terrain",
|
|
95
|
+
opacity: 0.85,
|
|
96
|
+
attribution: [{ text: "National mapping authority", required: true }],
|
|
97
|
+
tiles: [
|
|
98
|
+
{
|
|
99
|
+
id: "terrain-full",
|
|
100
|
+
mediaType: "image/png",
|
|
101
|
+
data: pngBytes,
|
|
102
|
+
pixelWidth: 3272,
|
|
103
|
+
pixelHeight: 2244,
|
|
104
|
+
frame: plan.frame,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
request.marginalia = {};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Validation runs before serialization. IDs and attribution text must be non-blank; layer and tile IDs
|
|
114
|
+
must be unique; opacity must be in `[0, 1]`; pixel dimensions must be positive integers; and tiles
|
|
115
|
+
must cover the frame without gaps or overlaps within `1e-6` mm. PNG signature, dimensions, and APNG
|
|
116
|
+
markers are inspected. JPEG signature, baseline/progressive dimensions, and EXIF orientation are
|
|
117
|
+
inspected; only absent orientation or orientation 1 is accepted. Empty, malformed, mismatched, or
|
|
118
|
+
unsupported images reject the complete request. Inspection reads image containers only — compressed
|
|
119
|
+
pixel data is never decoded, and tile bytes are embedded exactly as supplied.
|
|
120
|
+
|
|
121
|
+
Resolved raster metadata preserves canonical layer/tile order, opacity, de-duplicated attribution,
|
|
122
|
+
physical placement, media type, intrinsic dimensions, and effective horizontal/vertical DPI without
|
|
123
|
+
echoing image bytes. `low-raster-dpi` identifies a tile below 150 DPI on either axis;
|
|
124
|
+
`raster-dpi-anisotropy` identifies a horizontal/vertical difference above 1%. Required attribution
|
|
125
|
+
is de-duplicated in stable first-occurrence order and wrapped into a reserved marginalia band. The
|
|
126
|
+
request fails if marginalia is disabled or the required text cannot fit without overlap.
|
|
127
|
+
|
|
128
|
+
SVG embeds self-contained data URLs; PDF consumes the same PNG/JPEG payload and physical
|
|
129
|
+
rectangles. Both preserve PNG transparency, layer opacity, clipping, and ordinary source-over layer
|
|
130
|
+
order. MGRS, authored graphics, symbols, text, and marginalia remain vector above the basemap.
|
|
131
|
+
Identical request values and image bytes produce deterministic output. Registered transparent
|
|
132
|
+
overlays cannot include a raster basemap.
|
|
133
|
+
|
|
134
|
+
The host remains responsible for basemap selection and licensing, network acquisition, engine/style
|
|
135
|
+
rendering, reprojection into `plan.projectedBounds`, warp quality, DPI choice, image encoding, and
|
|
136
|
+
tile sizing. The package accepts no MapLibre/OpenLayers/Leaflet object, style, URL, canvas, or Web
|
|
137
|
+
Mercator tile coordinate and performs no I/O. For a MapLibre reference host, render a flat Mercator
|
|
138
|
+
map at pitch/bearing zero with no globe or terrain, wait for `idle`, keep style layout at 96 CSS
|
|
139
|
+
px/in, use drawing-buffer pixel ratio for export DPI, and warp the captured texture into the
|
|
140
|
+
UTM/UPS target before encoding tiles. The preview reference host qualifies one coherent MapLibre
|
|
141
|
+
source canvas per layer with 256 logical CSS pixels of padding on every side, a 0.5-source-pixel
|
|
142
|
+
adaptive warp tolerance, and sequential destination tiles no larger than 4096 pixels per edge. It
|
|
143
|
+
never automatically reduces output DPI, padding, tolerance, dimensions, or layer count.
|
|
144
|
+
|
|
145
|
+
MapLibre's Web Mercator latitude domain and the browser/GPU single-source-canvas and memory limits
|
|
146
|
+
qualify that preview-host implementation only. They do not narrow this package's raster plan:
|
|
147
|
+
`resolveMapSheetRasterPlan` and `mapSheetRasterPointToGeographic` remain UTM/UPS-capable, including
|
|
148
|
+
polar vector-only exports. A host using another renderer or a different source strategy may support
|
|
149
|
+
other source domains while supplying the same projection-aligned package input.
|
|
150
|
+
|
|
59
151
|
Warnings are deterministic records with these public codes and payloads:
|
|
60
152
|
|
|
61
153
|
- `cross-zone-footprint` reports the center-selected UTM `selectedZone` and all
|
|
@@ -69,6 +161,9 @@ Warnings are deterministic records with these public codes and payloads:
|
|
|
69
161
|
and `graphicId`.
|
|
70
162
|
- `point-symbol-fallback` identifies a `layerId` and `graphicId` whose point-symbol capability
|
|
71
163
|
returned visible fallback vector content.
|
|
164
|
+
- `low-raster-dpi` identifies `layerId`/`tileId`, both effective DPI values, and the 150 DPI
|
|
165
|
+
threshold.
|
|
166
|
+
- `raster-dpi-anisotropy` identifies `layerId`/`tileId` and the relative axis difference above 1%.
|
|
72
167
|
|
|
73
168
|
Regular/Caps, Italic, and Light semantic text use complete package-owned Open Sans static TTFs and
|
|
74
169
|
one final-size metrics service. Their license, immutable provenance, and SHA-256 identities live
|
|
@@ -196,6 +291,8 @@ Application stores and map-engine output are not part of the public request cont
|
|
|
196
291
|
- `registration`: optional grouped MGRS marks and `snapIntervalMeters`; its presence activates
|
|
197
292
|
strict transparent registered-overlay validation.
|
|
198
293
|
- `printerCalibration`: optional bottom-margin diagnostic line; `lengthMm` defaults to 100.
|
|
294
|
+
- `rasterBasemap`: optional ordered raster layer stack described above. It is incompatible with
|
|
295
|
+
`registration`.
|
|
199
296
|
|
|
200
297
|
Each layer has a stable `id`, ordered `{ graphic }[]`, and optional shared portrayal defaults for
|
|
201
298
|
color, line/dash/cap/join, and label size/clamp. A graphic is a control-measure `ControlMeasure`, a
|
|
@@ -249,6 +346,8 @@ preparer text.
|
|
|
249
346
|
document metadata, optional resolved MGRS interval/precision, and resolved CRS (`kind`, zone or
|
|
250
347
|
hemisphere, EPSG, center point scale, center convergence in degrees, and maximum frame scale
|
|
251
348
|
deviation).
|
|
349
|
+
- `metadata.rasterBasemap`: canonical layer, attribution, tile placement, media type, dimensions,
|
|
350
|
+
and effective DPI metadata; image bytes are intentionally omitted.
|
|
252
351
|
- `resources`: identities for embedded Open Sans fonts and deterministic pattern resources.
|
|
253
352
|
- `registrationMarks`: when registered, the ordered resolved IDs, normalized MGRS references,
|
|
254
353
|
longitude/latitude positions, and physical sheet positions in millimetres.
|
package/dist/{create-tactical-map-sheet-T2q1nHWO.d.mts → create-tactical-map-sheet-Cydh9ghW.d.mts}
RENAMED
|
@@ -393,6 +393,57 @@ interface PrinterCalibrationRequest {
|
|
|
393
393
|
readonly lengthMm?: number;
|
|
394
394
|
}
|
|
395
395
|
//#endregion
|
|
396
|
+
//#region src/raster-basemap.d.ts
|
|
397
|
+
interface MapSheetRasterAttribution {
|
|
398
|
+
readonly text: string;
|
|
399
|
+
readonly required: boolean;
|
|
400
|
+
}
|
|
401
|
+
interface MapSheetRasterTile {
|
|
402
|
+
readonly id: string;
|
|
403
|
+
readonly mediaType: "image/png" | "image/jpeg";
|
|
404
|
+
readonly data: Uint8Array;
|
|
405
|
+
readonly pixelWidth: number;
|
|
406
|
+
readonly pixelHeight: number;
|
|
407
|
+
readonly frame: SceneRectangle;
|
|
408
|
+
}
|
|
409
|
+
interface MapSheetRasterLayer {
|
|
410
|
+
readonly id: string;
|
|
411
|
+
readonly opacity: number;
|
|
412
|
+
readonly attribution: readonly MapSheetRasterAttribution[];
|
|
413
|
+
readonly tiles: readonly MapSheetRasterTile[];
|
|
414
|
+
}
|
|
415
|
+
interface MapSheetRasterBasemap {
|
|
416
|
+
readonly layers: readonly MapSheetRasterLayer[];
|
|
417
|
+
}
|
|
418
|
+
interface ResolvedMapSheetRasterTileMetadata extends Omit<MapSheetRasterTile, "data"> {
|
|
419
|
+
readonly effectiveHorizontalDpi: number;
|
|
420
|
+
readonly effectiveVerticalDpi: number;
|
|
421
|
+
}
|
|
422
|
+
interface ResolvedMapSheetRasterLayerMetadata {
|
|
423
|
+
readonly id: string;
|
|
424
|
+
readonly opacity: number;
|
|
425
|
+
readonly attribution: readonly MapSheetRasterAttribution[];
|
|
426
|
+
readonly tiles: readonly ResolvedMapSheetRasterTileMetadata[];
|
|
427
|
+
}
|
|
428
|
+
interface ResolvedMapSheetRasterBasemapMetadata {
|
|
429
|
+
readonly layers: readonly ResolvedMapSheetRasterLayerMetadata[];
|
|
430
|
+
}
|
|
431
|
+
interface LowRasterDpiWarning {
|
|
432
|
+
readonly code: "low-raster-dpi";
|
|
433
|
+
readonly layerId: string;
|
|
434
|
+
readonly tileId: string;
|
|
435
|
+
readonly effectiveHorizontalDpi: number;
|
|
436
|
+
readonly effectiveVerticalDpi: number;
|
|
437
|
+
readonly thresholdDpi: 150;
|
|
438
|
+
}
|
|
439
|
+
interface RasterDpiAnisotropyWarning {
|
|
440
|
+
readonly code: "raster-dpi-anisotropy";
|
|
441
|
+
readonly layerId: string;
|
|
442
|
+
readonly tileId: string;
|
|
443
|
+
readonly relativeDifference: number;
|
|
444
|
+
}
|
|
445
|
+
type MapSheetRasterWarning = LowRasterDpiWarning | RasterDpiAnisotropyWarning;
|
|
446
|
+
//#endregion
|
|
396
447
|
//#region src/create-tactical-map-sheet.d.ts
|
|
397
448
|
interface MapSheetPageInput extends PhysicalPageSize {
|
|
398
449
|
readonly marginMm?: number;
|
|
@@ -412,6 +463,7 @@ interface TacticalMapSheetRequest {
|
|
|
412
463
|
readonly metadata?: MapSheetMetadata;
|
|
413
464
|
readonly registration?: MapSheetRegistrationRequest;
|
|
414
465
|
readonly printerCalibration?: PrinterCalibrationRequest;
|
|
466
|
+
readonly rasterBasemap?: MapSheetRasterBasemap;
|
|
415
467
|
}
|
|
416
468
|
type UtmProjection = UtmCrs;
|
|
417
469
|
type UpsProjection = UpsCrs;
|
|
@@ -430,7 +482,7 @@ interface ScaleDeviationWarning {
|
|
|
430
482
|
readonly maximumRelativeDeviation: number;
|
|
431
483
|
}
|
|
432
484
|
type MapSheetProjectionWarning = CrossZoneFootprintWarning | CrossProjectionFootprintWarning | ScaleDeviationWarning;
|
|
433
|
-
type TacticalMapSheetWarning = MapSheetProjectionWarning | GraphicRenderWarning;
|
|
485
|
+
type TacticalMapSheetWarning = MapSheetProjectionWarning | GraphicRenderWarning | MapSheetRasterWarning;
|
|
434
486
|
interface MapSheetFontIdentity {
|
|
435
487
|
readonly family: "Open Sans";
|
|
436
488
|
readonly style: "Regular" | "Italic" | "Light";
|
|
@@ -456,6 +508,7 @@ interface ResolvedMapSheetMetadata {
|
|
|
456
508
|
};
|
|
457
509
|
readonly grid?: ResolvedMgrsGridMetadata;
|
|
458
510
|
readonly document?: MapSheetMetadata;
|
|
511
|
+
readonly rasterBasemap?: ResolvedMapSheetRasterBasemapMetadata;
|
|
459
512
|
}
|
|
460
513
|
interface TacticalMapSheetResult {
|
|
461
514
|
readonly svg: string;
|
|
@@ -467,4 +520,4 @@ interface TacticalMapSheetResult {
|
|
|
467
520
|
}
|
|
468
521
|
declare function createTacticalMapSheet(request: TacticalMapSheetRequest): TacticalMapSheetResult;
|
|
469
522
|
//#endregion
|
|
470
|
-
export {
|
|
523
|
+
export { SceneRectangle as $, PaperSizeName as A, ControlledVectorSymbol as B, ResolvedMapSheetRasterTileMetadata as C, ResolvedRegistrationMark as D, MapSheetRegistrationRequest as E, GraphicOutsideFrameWarning as F, VectorSymbolAnchor as G, MapSheetPointSymbol as H, GraphicRenderWarning as I, MgrsGridOptions as J, MapSheetMarginaliaOptions as K, MapSheetPortrayalDefaults as L, AuthoredMapSheetGraphic as M, GraphicClippedWarning as N, PAPER_SIZES as O, GraphicHiddenWarning as P, MapSheetPatternIdentity as Q, PointSymbolFallbackWarning as R, ResolvedMapSheetRasterLayerMetadata as S, AuthoredRegistrationMark as T, MapSheetPointSymbolCapability as U, MapSheetCustomSymbol as V, MapSheetSymbolSize as W, MgrsPrecision as X, ResolvedMgrsGridMetadata as Y, MgrsSnapInterval as Z, MapSheetRasterLayer as _, ResolvedMapSheetMetadata as a, Position as at, RasterDpiAnisotropyWarning as b, TacticalMapSheetResources as c, UpsProjection as d, GeographicPosition as et, UtmProjection as f, MapSheetRasterBasemap as g, MapSheetRasterAttribution as h, MapSheetPageInput as i, Polygon as it, resolvePaperSize as j, PaperOrientation as k, TacticalMapSheetResult as l, LowRasterDpiWarning as m, CrossZoneFootprintWarning as n, UpsCrs as nt, ScaleDeviationWarning as o, createTacticalMapSheet as p, MapSheetMetadata as q, MapSheetFontIdentity as r, UtmCrs as rt, TacticalMapSheetRequest as s, CrossProjectionFootprintWarning as t, GridCrs as tt, TacticalMapSheetWarning as u, MapSheetRasterTile as v, PrinterCalibrationRequest as w, ResolvedMapSheetRasterBasemapMetadata as x, MapSheetRasterWarning as y, TacticalMapSheetLayer as z };
|
package/dist/{create-tactical-map-sheet-Ch8io55k.mjs → create-tactical-map-sheet-D2G1PW2H.mjs}
RENAMED
|
@@ -434,11 +434,42 @@ function validateTextFitsPage(element, page) {
|
|
|
434
434
|
const bottom = top + element.heightMm;
|
|
435
435
|
if (left < 0 || right > page.widthMm || top < 0 || bottom > page.heightMm) throw new RangeError(`Marginalia text does not fit on the physical page: ${element.text}`);
|
|
436
436
|
}
|
|
437
|
+
function wrapRasterAttribution(entries, maximumWidthMm, color) {
|
|
438
|
+
if (entries.length === 0) return {
|
|
439
|
+
elements: [],
|
|
440
|
+
bandHeightMm: 0
|
|
441
|
+
};
|
|
442
|
+
const words = `ATTRIBUTION: ${entries.join(" · ")}`.split(" ");
|
|
443
|
+
const lines = [];
|
|
444
|
+
let line = "";
|
|
445
|
+
for (const word of words) {
|
|
446
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
447
|
+
if (text(candidate, 0, 0, 4, color, "start").widthMm <= maximumWidthMm) line = candidate;
|
|
448
|
+
else {
|
|
449
|
+
if (!line || text(word, 0, 0, 4, color, "start").widthMm > maximumWidthMm) throw new RangeError("Required raster attribution cannot fit without overlap");
|
|
450
|
+
lines.push(line);
|
|
451
|
+
line = word;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (line) lines.push(line);
|
|
455
|
+
const sample = text("Ag", 0, 0, 4, color, "start");
|
|
456
|
+
return {
|
|
457
|
+
bandHeightMm: lines.length * sample.lineHeightMm + .5,
|
|
458
|
+
elements: lines.map((value, index) => text(value, 0, .25 + sample.lineHeightMm * (index + .5), 4, color, "start"))
|
|
459
|
+
};
|
|
460
|
+
}
|
|
437
461
|
function renderMarginalia(options, context) {
|
|
438
|
-
const topSpace = context.frame.yMm;
|
|
439
|
-
const bottomSpace = context.page.heightMm - context.frame.yMm - context.frame.heightMm;
|
|
440
|
-
if (topSpace < 8 || bottomSpace < 8) throw new RangeError("Requested marginalia requires at least 8 mm of printable space above and below the map frame");
|
|
441
462
|
const color = options.color ?? "#111827";
|
|
463
|
+
const attributionLayout = wrapRasterAttribution(context.requiredRasterAttribution ?? [], context.frame.widthMm, color);
|
|
464
|
+
const attribution = attributionLayout.elements;
|
|
465
|
+
const attributionBandHeight = attributionLayout.bandHeightMm;
|
|
466
|
+
const layoutPageHeight = context.page.heightMm - attributionBandHeight;
|
|
467
|
+
const topSpace = context.frame.yMm;
|
|
468
|
+
const bottomSpace = layoutPageHeight - context.frame.yMm - context.frame.heightMm;
|
|
469
|
+
if (topSpace < 8 || bottomSpace < 8) {
|
|
470
|
+
if (attribution.length > 0) throw new RangeError("Required raster attribution cannot fit without overlap");
|
|
471
|
+
throw new RangeError("Requested marginalia requires at least 8 mm of printable space above and below the map frame");
|
|
472
|
+
}
|
|
442
473
|
const elements = [];
|
|
443
474
|
const topY = topSpace >= 8 ? topSpace / 2 : context.frame.yMm + context.frame.heightMm + 3;
|
|
444
475
|
const bottomY = context.frame.yMm + context.frame.heightMm + Math.max(2.5, bottomSpace / 2);
|
|
@@ -513,16 +544,27 @@ function renderMarginalia(options, context) {
|
|
|
513
544
|
"data-calibration-length-mm": calibrationLengthMm
|
|
514
545
|
}));
|
|
515
546
|
}
|
|
516
|
-
if (options.showFootprint !== false) elements.push(text(footprintLabel(context.footprintRing), centerX,
|
|
547
|
+
if (options.showFootprint !== false) elements.push(text(footprintLabel(context.footprintRing), centerX, layoutPageHeight - 1.5, 4, color));
|
|
517
548
|
if (context.metadata?.classification) elements.push(text(context.metadata.classification, centerX, 1.5, 5, color));
|
|
518
549
|
if (context.metadata?.preparedBy) elements.push(text(`PREPARED BY ${context.metadata.preparedBy}`, context.frame.xMm, bottomY + 1.5, 4, color, "start"));
|
|
550
|
+
elements.push(...attribution.map((element) => ({
|
|
551
|
+
...element,
|
|
552
|
+
position: {
|
|
553
|
+
xMm: context.frame.xMm,
|
|
554
|
+
yMm: layoutPageHeight + element.position.yMm
|
|
555
|
+
},
|
|
556
|
+
attributes: { "data-raster-attribution": "required" }
|
|
557
|
+
})));
|
|
519
558
|
for (const element of elements) if (element.kind === "text") validateTextFitsPage(element, context.page);
|
|
520
559
|
return {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
id: "marginalia",
|
|
524
|
-
|
|
525
|
-
|
|
560
|
+
reservedBottomMm: attributionBandHeight,
|
|
561
|
+
layer: {
|
|
562
|
+
id: "sheet-marginalia-0",
|
|
563
|
+
graphics: [{
|
|
564
|
+
id: "marginalia",
|
|
565
|
+
elements
|
|
566
|
+
}]
|
|
567
|
+
}
|
|
526
568
|
};
|
|
527
569
|
}
|
|
528
570
|
//#endregion
|
|
@@ -537,6 +579,21 @@ function formatDecimal(value) {
|
|
|
537
579
|
function escapeXml(value) {
|
|
538
580
|
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
539
581
|
}
|
|
582
|
+
function encodeBase64(bytes) {
|
|
583
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
584
|
+
let result = "";
|
|
585
|
+
for (let offset = 0; offset < bytes.length; offset += 3) {
|
|
586
|
+
const first = bytes[offset];
|
|
587
|
+
const second = bytes[offset + 1];
|
|
588
|
+
const third = bytes[offset + 2];
|
|
589
|
+
const value = first << 16 | (second ?? 0) << 8 | (third ?? 0);
|
|
590
|
+
result += alphabet[value >>> 18 & 63];
|
|
591
|
+
result += alphabet[value >>> 12 & 63];
|
|
592
|
+
result += second === void 0 ? "=" : alphabet[value >>> 6 & 63];
|
|
593
|
+
result += third === void 0 ? "=" : alphabet[value & 63];
|
|
594
|
+
}
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
540
597
|
function serializeAttributes(attributes) {
|
|
541
598
|
if (!attributes) return "";
|
|
542
599
|
return Object.entries(attributes).sort(([left], [right]) => left.localeCompare(right)).map(([name, value]) => ` ${name}="${escapeXml(String(value))}"`).join("");
|
|
@@ -598,6 +655,7 @@ function serializeText(text) {
|
|
|
598
655
|
return `${background}<text ${attributes.join(" ")}>${content}</text>`;
|
|
599
656
|
}
|
|
600
657
|
function serializeElement(element) {
|
|
658
|
+
if (element.kind === "raster-image") return `<image data-raster-resource="${escapeXml(element.resourceId)}" x="${formatDecimal(element.frame.xMm)}" y="${formatDecimal(element.frame.yMm)}" width="${formatDecimal(element.frame.widthMm)}" height="${formatDecimal(element.frame.heightMm)}" opacity="${formatDecimal(element.opacity)}" preserveAspectRatio="none" href="data:${element.mediaType};base64,${encodeBase64(element.data)}"/>`;
|
|
601
659
|
if (element.kind === "vector-symbol") {
|
|
602
660
|
const transform = [
|
|
603
661
|
`translate(${formatDecimal(element.position.xMm)} ${formatDecimal(element.position.yMm)})`,
|
|
@@ -1445,6 +1503,28 @@ function textCorners(text) {
|
|
|
1445
1503
|
yMm: text.position.yMm + x * sin + y * cos
|
|
1446
1504
|
}));
|
|
1447
1505
|
}
|
|
1506
|
+
function rectangleCorners(frame) {
|
|
1507
|
+
const right = frame.xMm + frame.widthMm;
|
|
1508
|
+
const bottom = frame.yMm + frame.heightMm;
|
|
1509
|
+
return [
|
|
1510
|
+
{
|
|
1511
|
+
xMm: frame.xMm,
|
|
1512
|
+
yMm: frame.yMm
|
|
1513
|
+
},
|
|
1514
|
+
{
|
|
1515
|
+
xMm: right,
|
|
1516
|
+
yMm: frame.yMm
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
xMm: right,
|
|
1520
|
+
yMm: bottom
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
xMm: frame.xMm,
|
|
1524
|
+
yMm: bottom
|
|
1525
|
+
}
|
|
1526
|
+
];
|
|
1527
|
+
}
|
|
1448
1528
|
function expandFrame(frame, deltaMm) {
|
|
1449
1529
|
if (deltaMm === 0) return frame;
|
|
1450
1530
|
return {
|
|
@@ -1455,7 +1535,7 @@ function expandFrame(frame, deltaMm) {
|
|
|
1455
1535
|
};
|
|
1456
1536
|
}
|
|
1457
1537
|
function visibilityOfElement(element, frame) {
|
|
1458
|
-
const points = element.kind === "path" ? element.points : element.kind === "text" ? textCorners(element) : vectorSymbolCorners(element);
|
|
1538
|
+
const points = element.kind === "path" ? element.points : element.kind === "text" ? textCorners(element) : element.kind === "vector-symbol" ? vectorSymbolCorners(element) : rectangleCorners(element.frame);
|
|
1459
1539
|
const inflation = element.kind === "path" ? element.strokeWidthMm / 2 : 0;
|
|
1460
1540
|
const innerFrame = expandFrame(frame, -inflation);
|
|
1461
1541
|
const outerFrame = expandFrame(frame, inflation);
|
|
@@ -1885,12 +1965,13 @@ const STROKE_WIDTH_MM = DEFAULT_LINE_STROKE_WIDTH_MM;
|
|
|
1885
1965
|
function line(points, attributes) {
|
|
1886
1966
|
return createSceneLine(points, "#000000", { attributes });
|
|
1887
1967
|
}
|
|
1888
|
-
function renderPrinterCalibration(request, page, frame) {
|
|
1968
|
+
function renderPrinterCalibration(request, page, frame, reservedBottomMm = 0) {
|
|
1889
1969
|
const lengthMm = request.lengthMm ?? DEFAULT_LENGTH_MM;
|
|
1890
1970
|
requirePositiveFinite(lengthMm, "Printer-calibration length");
|
|
1891
1971
|
const centerX = page.widthMm / 2;
|
|
1892
1972
|
const frameBottom = frame.yMm + frame.heightMm;
|
|
1893
|
-
const
|
|
1973
|
+
const layoutBottom = page.heightMm - reservedBottomMm;
|
|
1974
|
+
const yMm = frameBottom + (layoutBottom - frameBottom) / 2;
|
|
1894
1975
|
const startX = centerX - lengthMm / 2;
|
|
1895
1976
|
const endX = centerX + lengthMm / 2;
|
|
1896
1977
|
const label = createRegularSceneText({
|
|
@@ -1907,7 +1988,7 @@ function renderPrinterCalibration(request, page, frame) {
|
|
|
1907
1988
|
const labelRight = label.position.xMm + label.widthMm / 2;
|
|
1908
1989
|
const contentTop = Math.min(label.position.yMm - label.heightMm / 2, yMm - TICK_HALF_HEIGHT_MM - strokeEdge);
|
|
1909
1990
|
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 >
|
|
1991
|
+
if (!fitsHorizontally || labelLeft < 0 || labelRight > page.widthMm || contentTop < frameBottom || contentBottom > layoutBottom) throw new RangeError("The bottom page margin must fit the complete printer-calibration line, ticks, and label");
|
|
1911
1992
|
return {
|
|
1912
1993
|
id: "printer-calibration-0",
|
|
1913
1994
|
graphics: [{
|
|
@@ -1940,9 +2021,7 @@ function renderPrinterCalibration(request, page, frame) {
|
|
|
1940
2021
|
};
|
|
1941
2022
|
}
|
|
1942
2023
|
//#endregion
|
|
1943
|
-
//#region src/
|
|
1944
|
-
const FOOTPRINT_SEGMENTS_PER_EDGE = 16;
|
|
1945
|
-
const SCALE_SEARCH_BOUNDARY_DIVISIONS = 256;
|
|
2024
|
+
//#region src/raster-plan.ts
|
|
1946
2025
|
function resolvePage(input) {
|
|
1947
2026
|
requirePositiveFinite(input.widthMm, "Page width");
|
|
1948
2027
|
requirePositiveFinite(input.heightMm, "Page height");
|
|
@@ -1982,6 +2061,317 @@ function resolveFrame(request, page) {
|
|
|
1982
2061
|
heightMm
|
|
1983
2062
|
};
|
|
1984
2063
|
}
|
|
2064
|
+
function resolveProjection(requested, center) {
|
|
2065
|
+
const selected = geographicLibGridProjection.selectStandard(center);
|
|
2066
|
+
if (requested === void 0 || requested === "auto") return selected;
|
|
2067
|
+
if (requested.hemisphere !== selected.hemisphere) throw new RangeError("The explicit projection hemisphere must contain the sheet center");
|
|
2068
|
+
if (requested.kind === "ups" && selected.kind !== "ups") throw new RangeError("UPS can only be selected for a center in a polar UPS region");
|
|
2069
|
+
return requested;
|
|
2070
|
+
}
|
|
2071
|
+
function epsgFor(crs) {
|
|
2072
|
+
if (crs.kind === "ups") return crs.hemisphere === "north" ? 32661 : 32761;
|
|
2073
|
+
return (crs.hemisphere === "north" ? 32600 : 32700) + crs.zone;
|
|
2074
|
+
}
|
|
2075
|
+
function resolveMapSheetGeometry(request) {
|
|
2076
|
+
requirePositiveFinite(request.scaleDenominator, "Scale denominator");
|
|
2077
|
+
const page = resolvePage(request.page);
|
|
2078
|
+
const frame = resolveFrame(request, page);
|
|
2079
|
+
const crs = resolveProjection(request.projection, request.center);
|
|
2080
|
+
const center = geographicLibGridProjection.forward(crs, request.center);
|
|
2081
|
+
if (!(center.pointScale > 0)) throw new Error("Projection point scale must be positive");
|
|
2082
|
+
const projectedMetresPerMillimetre = request.scaleDenominator / 1e3 * center.pointScale;
|
|
2083
|
+
const projectedWidth = frame.widthMm * projectedMetresPerMillimetre;
|
|
2084
|
+
const projectedHeight = frame.heightMm * projectedMetresPerMillimetre;
|
|
2085
|
+
return {
|
|
2086
|
+
page,
|
|
2087
|
+
center,
|
|
2088
|
+
plan: {
|
|
2089
|
+
crs,
|
|
2090
|
+
epsg: epsgFor(crs),
|
|
2091
|
+
frame,
|
|
2092
|
+
projectedMetresPerMillimetre,
|
|
2093
|
+
projectedBounds: {
|
|
2094
|
+
left: center.easting - projectedWidth / 2,
|
|
2095
|
+
bottom: center.northing - projectedHeight / 2,
|
|
2096
|
+
right: center.easting + projectedWidth / 2,
|
|
2097
|
+
top: center.northing + projectedHeight / 2
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
function resolveMapSheetRasterPlan(request) {
|
|
2103
|
+
return resolveMapSheetGeometry(request).plan;
|
|
2104
|
+
}
|
|
2105
|
+
function mapSheetRasterPointToGeographic(plan, normalizedX, normalizedY) {
|
|
2106
|
+
if (!Number.isFinite(normalizedX) || normalizedX < 0 || normalizedX > 1 || !Number.isFinite(normalizedY) || normalizedY < 0 || normalizedY > 1) throw new RangeError("Normalized raster coordinates must be finite numbers from 0 through 1");
|
|
2107
|
+
const bounds = plan.projectedBounds;
|
|
2108
|
+
return geographicLibGridProjection.reverse(plan.crs, {
|
|
2109
|
+
easting: bounds.left + normalizedX * (bounds.right - bounds.left),
|
|
2110
|
+
northing: bounds.top - normalizedY * (bounds.top - bounds.bottom)
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
//#endregion
|
|
2114
|
+
//#region src/raster-basemap.ts
|
|
2115
|
+
const TESSELLATION_TOLERANCE_MM = 1e-6;
|
|
2116
|
+
const LOW_DPI_THRESHOLD = 150;
|
|
2117
|
+
const DPI_ANISOTROPY_THRESHOLD = .01;
|
|
2118
|
+
function renderRasterBasemapLayers(basemap) {
|
|
2119
|
+
return basemap.layers.map((layer) => ({
|
|
2120
|
+
id: `raster-basemap-${layer.id}`,
|
|
2121
|
+
attributes: {
|
|
2122
|
+
"data-raster-layer": layer.id,
|
|
2123
|
+
"data-opacity": layer.opacity
|
|
2124
|
+
},
|
|
2125
|
+
graphics: layer.tiles.map((tile) => ({
|
|
2126
|
+
id: tile.id,
|
|
2127
|
+
elements: [{
|
|
2128
|
+
kind: "raster-image",
|
|
2129
|
+
mediaType: tile.mediaType,
|
|
2130
|
+
data: tile.data,
|
|
2131
|
+
frame: { ...tile.frame },
|
|
2132
|
+
opacity: layer.opacity,
|
|
2133
|
+
resourceId: `${layer.id}/${tile.id}`
|
|
2134
|
+
}]
|
|
2135
|
+
}))
|
|
2136
|
+
}));
|
|
2137
|
+
}
|
|
2138
|
+
function requireStableId(value, label, ids) {
|
|
2139
|
+
if (typeof value !== "string" || value.trim() === "") throw new RangeError(`${label} must be a non-blank stable ID`);
|
|
2140
|
+
if (ids.has(value)) throw new RangeError(`Duplicate ${label.toLowerCase()}: ${value}`);
|
|
2141
|
+
ids.add(value);
|
|
2142
|
+
}
|
|
2143
|
+
function readUint32(bytes, offset, littleEndian = false) {
|
|
2144
|
+
if (offset < 0 || offset + 4 > bytes.length) throw new RangeError("Malformed image data");
|
|
2145
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(offset, littleEndian);
|
|
2146
|
+
}
|
|
2147
|
+
function readUint16(bytes, offset, littleEndian = false) {
|
|
2148
|
+
if (offset < 0 || offset + 2 > bytes.length) throw new RangeError("Malformed image data");
|
|
2149
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint16(offset, littleEndian);
|
|
2150
|
+
}
|
|
2151
|
+
function ascii(bytes, offset, length) {
|
|
2152
|
+
if (offset < 0 || offset + length > bytes.length) throw new RangeError("Malformed image data");
|
|
2153
|
+
return String.fromCharCode(...bytes.subarray(offset, offset + length));
|
|
2154
|
+
}
|
|
2155
|
+
function inspectPng(bytes) {
|
|
2156
|
+
if (bytes.length < 33 || [
|
|
2157
|
+
137,
|
|
2158
|
+
80,
|
|
2159
|
+
78,
|
|
2160
|
+
71,
|
|
2161
|
+
13,
|
|
2162
|
+
10,
|
|
2163
|
+
26,
|
|
2164
|
+
10
|
|
2165
|
+
].some((value, index) => bytes[index] !== value)) throw new RangeError("Malformed PNG image");
|
|
2166
|
+
if (readUint32(bytes, 8) !== 13 || ascii(bytes, 12, 4) !== "IHDR") throw new RangeError("Malformed PNG image: IHDR must be first");
|
|
2167
|
+
const width = readUint32(bytes, 16);
|
|
2168
|
+
const height = readUint32(bytes, 20);
|
|
2169
|
+
let offset = 8;
|
|
2170
|
+
let ended = false;
|
|
2171
|
+
while (offset + 12 <= bytes.length) {
|
|
2172
|
+
const length = readUint32(bytes, offset);
|
|
2173
|
+
const type = ascii(bytes, offset + 4, 4);
|
|
2174
|
+
const next = offset + 12 + length;
|
|
2175
|
+
if (next > bytes.length) throw new RangeError("Malformed PNG image chunk");
|
|
2176
|
+
if (type === "acTL") throw new RangeError("APNG images are not supported");
|
|
2177
|
+
offset = next;
|
|
2178
|
+
if (type === "IEND") {
|
|
2179
|
+
ended = true;
|
|
2180
|
+
break;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
if (!ended || offset !== bytes.length || width === 0 || height === 0) throw new RangeError("Malformed PNG image");
|
|
2184
|
+
return {
|
|
2185
|
+
width,
|
|
2186
|
+
height
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
function readExifOrientation(bytes, start, length) {
|
|
2190
|
+
if (length < 6 || ascii(bytes, start, 6) !== "Exif\0\0") return void 0;
|
|
2191
|
+
if (length < 14) throw new RangeError("Malformed JPEG EXIF data");
|
|
2192
|
+
const tiff = start + 6;
|
|
2193
|
+
const byteOrder = ascii(bytes, tiff, 2);
|
|
2194
|
+
if (byteOrder !== "II" && byteOrder !== "MM") throw new RangeError("Malformed JPEG EXIF data");
|
|
2195
|
+
const littleEndian = byteOrder === "II";
|
|
2196
|
+
if (readUint16(bytes, tiff + 2, littleEndian) !== 42) throw new RangeError("Malformed JPEG EXIF data");
|
|
2197
|
+
const ifd = tiff + readUint32(bytes, tiff + 4, littleEndian);
|
|
2198
|
+
if (ifd + 2 > start + length) throw new RangeError("Malformed JPEG EXIF data");
|
|
2199
|
+
const entries = readUint16(bytes, ifd, littleEndian);
|
|
2200
|
+
for (let index = 0; index < entries; index += 1) {
|
|
2201
|
+
const entry = ifd + 2 + index * 12;
|
|
2202
|
+
if (entry + 12 > start + length) throw new RangeError("Malformed JPEG EXIF data");
|
|
2203
|
+
if (readUint16(bytes, entry, littleEndian) === 274) {
|
|
2204
|
+
if (readUint16(bytes, entry + 2, littleEndian) !== 3 || readUint32(bytes, entry + 4, littleEndian) !== 1) throw new RangeError("Malformed JPEG EXIF orientation");
|
|
2205
|
+
return readUint16(bytes, entry + 8, littleEndian);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
function inspectJpeg(bytes) {
|
|
2210
|
+
if (bytes.length < 4 || bytes[0] !== 255 || bytes[1] !== 216 || bytes.at(-2) !== 255 || bytes.at(-1) !== 217) throw new RangeError("Malformed JPEG image");
|
|
2211
|
+
let offset = 2;
|
|
2212
|
+
let dimensions;
|
|
2213
|
+
let sawScan = false;
|
|
2214
|
+
while (offset < bytes.length - 2) {
|
|
2215
|
+
if (bytes[offset] !== 255) throw new RangeError("Malformed JPEG marker");
|
|
2216
|
+
while (bytes[offset] === 255) offset += 1;
|
|
2217
|
+
const marker = bytes[offset++];
|
|
2218
|
+
if (marker === 217) break;
|
|
2219
|
+
if (marker === 218) {
|
|
2220
|
+
const segmentLength = readUint16(bytes, offset);
|
|
2221
|
+
if (segmentLength < 2 || offset + segmentLength > bytes.length - 2) throw new RangeError("Malformed JPEG scan header");
|
|
2222
|
+
sawScan = true;
|
|
2223
|
+
break;
|
|
2224
|
+
}
|
|
2225
|
+
if (marker === 1 || marker >= 208 && marker <= 215) continue;
|
|
2226
|
+
const segmentLength = readUint16(bytes, offset);
|
|
2227
|
+
if (segmentLength < 2 || offset + segmentLength > bytes.length) throw new RangeError("Malformed JPEG segment");
|
|
2228
|
+
const dataStart = offset + 2;
|
|
2229
|
+
const dataLength = segmentLength - 2;
|
|
2230
|
+
if (marker === 225) {
|
|
2231
|
+
const orientation = readExifOrientation(bytes, dataStart, dataLength);
|
|
2232
|
+
if (orientation !== void 0 && orientation !== 1) throw new RangeError(`JPEG EXIF orientation ${orientation} is not supported`);
|
|
2233
|
+
}
|
|
2234
|
+
if (marker === 192 || marker === 194) {
|
|
2235
|
+
if (dataLength < 6) throw new RangeError("Malformed JPEG dimensions");
|
|
2236
|
+
dimensions = {
|
|
2237
|
+
height: readUint16(bytes, dataStart + 1),
|
|
2238
|
+
width: readUint16(bytes, dataStart + 3)
|
|
2239
|
+
};
|
|
2240
|
+
} else if (marker >= 192 && marker <= 207 && ![
|
|
2241
|
+
196,
|
|
2242
|
+
200,
|
|
2243
|
+
204
|
|
2244
|
+
].includes(marker)) throw new RangeError("Only baseline and progressive JPEG images are supported");
|
|
2245
|
+
offset += segmentLength;
|
|
2246
|
+
}
|
|
2247
|
+
if (!dimensions || dimensions.width === 0 || dimensions.height === 0 || !sawScan) throw new RangeError("Malformed JPEG image: dimensions are missing");
|
|
2248
|
+
return dimensions;
|
|
2249
|
+
}
|
|
2250
|
+
function inspectImage(tile) {
|
|
2251
|
+
if (!(tile.data instanceof Uint8Array) || tile.data.length === 0) throw new RangeError(`Raster tile ${tile.id} image bytes must be non-empty`);
|
|
2252
|
+
if (!Number.isInteger(tile.pixelWidth) || tile.pixelWidth <= 0 || !Number.isInteger(tile.pixelHeight) || tile.pixelHeight <= 0) throw new RangeError(`Raster tile ${tile.id} pixel dimensions must be positive integers`);
|
|
2253
|
+
const actual = tile.mediaType === "image/png" ? inspectPng(tile.data) : tile.mediaType === "image/jpeg" ? inspectJpeg(tile.data) : void 0;
|
|
2254
|
+
if (!actual) throw new RangeError(`Raster tile ${tile.id} media type is not supported`);
|
|
2255
|
+
if (actual.width !== tile.pixelWidth || actual.height !== tile.pixelHeight) {
|
|
2256
|
+
const format = tile.mediaType === "image/png" ? "PNG" : "JPEG";
|
|
2257
|
+
throw new RangeError(`Raster tile ${tile.id} declared dimensions ${tile.pixelWidth}x${tile.pixelHeight} do not match ${format} dimensions ${actual.width}x${actual.height}`);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
function clustered(values) {
|
|
2261
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
2262
|
+
const result = [];
|
|
2263
|
+
for (const value of sorted) if (result.length === 0 || value - result[result.length - 1] > TESSELLATION_TOLERANCE_MM) result.push(value);
|
|
2264
|
+
return result;
|
|
2265
|
+
}
|
|
2266
|
+
function validateTessellation(layer, frame) {
|
|
2267
|
+
const right = frame.xMm + frame.widthMm;
|
|
2268
|
+
const bottom = frame.yMm + frame.heightMm;
|
|
2269
|
+
for (const tile of layer.tiles) {
|
|
2270
|
+
const rectangle = tile.frame;
|
|
2271
|
+
if (!Number.isFinite(rectangle.xMm) || !Number.isFinite(rectangle.yMm) || !Number.isFinite(rectangle.widthMm) || !Number.isFinite(rectangle.heightMm) || rectangle.widthMm <= 0 || rectangle.heightMm <= 0 || rectangle.xMm < frame.xMm - TESSELLATION_TOLERANCE_MM || rectangle.yMm < frame.yMm - TESSELLATION_TOLERANCE_MM || rectangle.xMm + rectangle.widthMm > right + TESSELLATION_TOLERANCE_MM || rectangle.yMm + rectangle.heightMm > bottom + TESSELLATION_TOLERANCE_MM) throw new RangeError(`Raster layer ${layer.id} must tessellate the complete map frame`);
|
|
2272
|
+
}
|
|
2273
|
+
const xs = clustered([
|
|
2274
|
+
frame.xMm,
|
|
2275
|
+
right,
|
|
2276
|
+
...layer.tiles.flatMap((tile) => [tile.frame.xMm, tile.frame.xMm + tile.frame.widthMm])
|
|
2277
|
+
]);
|
|
2278
|
+
const ys = clustered([
|
|
2279
|
+
frame.yMm,
|
|
2280
|
+
bottom,
|
|
2281
|
+
...layer.tiles.flatMap((tile) => [tile.frame.yMm, tile.frame.yMm + tile.frame.heightMm])
|
|
2282
|
+
]);
|
|
2283
|
+
for (let xIndex = 0; xIndex < xs.length - 1; xIndex += 1) for (let yIndex = 0; yIndex < ys.length - 1; yIndex += 1) {
|
|
2284
|
+
const x = (xs[xIndex] + xs[xIndex + 1]) / 2;
|
|
2285
|
+
const y = (ys[yIndex] + ys[yIndex + 1]) / 2;
|
|
2286
|
+
if (x < frame.xMm || x > right || y < frame.yMm || y > bottom) continue;
|
|
2287
|
+
if (layer.tiles.filter((tile) => x >= tile.frame.xMm - TESSELLATION_TOLERANCE_MM && x <= tile.frame.xMm + tile.frame.widthMm + TESSELLATION_TOLERANCE_MM && y >= tile.frame.yMm - TESSELLATION_TOLERANCE_MM && y <= tile.frame.yMm + tile.frame.heightMm + TESSELLATION_TOLERANCE_MM).length !== 1) throw new RangeError(`Raster layer ${layer.id} must tessellate the complete map frame`);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
function validateAttribution(attribution) {
|
|
2291
|
+
return attribution.map((entry) => {
|
|
2292
|
+
if (typeof entry.text !== "string" || entry.text.trim() === "") throw new RangeError("Raster attribution text must be non-blank");
|
|
2293
|
+
if (typeof entry.required !== "boolean") throw new RangeError("Raster attribution requiredness must be explicit");
|
|
2294
|
+
return {
|
|
2295
|
+
text: entry.text,
|
|
2296
|
+
required: entry.required
|
|
2297
|
+
};
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
function resolveRasterBasemap(basemap, frame) {
|
|
2301
|
+
if (basemap.layers.length === 0) throw new RangeError("A raster basemap requires at least one layer");
|
|
2302
|
+
const layerIds = /* @__PURE__ */ new Set();
|
|
2303
|
+
const tileIds = /* @__PURE__ */ new Set();
|
|
2304
|
+
const attributionTexts = /* @__PURE__ */ new Set();
|
|
2305
|
+
const warnings = [];
|
|
2306
|
+
const allAttribution = [];
|
|
2307
|
+
const layers = basemap.layers.map((layer) => {
|
|
2308
|
+
requireStableId(layer.id, "Raster layer ID", layerIds);
|
|
2309
|
+
if (!Number.isFinite(layer.opacity) || layer.opacity < 0 || layer.opacity > 1) throw new RangeError(`Raster layer ${layer.id} opacity must be from 0 through 1`);
|
|
2310
|
+
if (layer.tiles.length === 0) throw new RangeError(`Raster layer ${layer.id} requires at least one tile`);
|
|
2311
|
+
const attribution = validateAttribution(layer.attribution).filter((entry) => {
|
|
2312
|
+
if (attributionTexts.has(entry.text)) return false;
|
|
2313
|
+
attributionTexts.add(entry.text);
|
|
2314
|
+
return true;
|
|
2315
|
+
});
|
|
2316
|
+
allAttribution.push(...attribution);
|
|
2317
|
+
for (const tile of layer.tiles) {
|
|
2318
|
+
requireStableId(tile.id, "Raster tile ID", tileIds);
|
|
2319
|
+
inspectImage(tile);
|
|
2320
|
+
}
|
|
2321
|
+
validateTessellation(layer, frame);
|
|
2322
|
+
const tiles = layer.tiles.map((tile) => {
|
|
2323
|
+
const effectiveHorizontalDpi = tile.pixelWidth * 25.4 / tile.frame.widthMm;
|
|
2324
|
+
const effectiveVerticalDpi = tile.pixelHeight * 25.4 / tile.frame.heightMm;
|
|
2325
|
+
if (effectiveHorizontalDpi < LOW_DPI_THRESHOLD || effectiveVerticalDpi < LOW_DPI_THRESHOLD) warnings.push({
|
|
2326
|
+
code: "low-raster-dpi",
|
|
2327
|
+
layerId: layer.id,
|
|
2328
|
+
tileId: tile.id,
|
|
2329
|
+
effectiveHorizontalDpi,
|
|
2330
|
+
effectiveVerticalDpi,
|
|
2331
|
+
thresholdDpi: LOW_DPI_THRESHOLD
|
|
2332
|
+
});
|
|
2333
|
+
const relativeDifference = Math.abs(effectiveHorizontalDpi - effectiveVerticalDpi) / Math.max(effectiveHorizontalDpi, effectiveVerticalDpi);
|
|
2334
|
+
if (relativeDifference > DPI_ANISOTROPY_THRESHOLD) warnings.push({
|
|
2335
|
+
code: "raster-dpi-anisotropy",
|
|
2336
|
+
layerId: layer.id,
|
|
2337
|
+
tileId: tile.id,
|
|
2338
|
+
relativeDifference
|
|
2339
|
+
});
|
|
2340
|
+
return {
|
|
2341
|
+
id: tile.id,
|
|
2342
|
+
mediaType: tile.mediaType,
|
|
2343
|
+
pixelWidth: tile.pixelWidth,
|
|
2344
|
+
pixelHeight: tile.pixelHeight,
|
|
2345
|
+
frame: { ...tile.frame },
|
|
2346
|
+
effectiveHorizontalDpi,
|
|
2347
|
+
effectiveVerticalDpi
|
|
2348
|
+
};
|
|
2349
|
+
});
|
|
2350
|
+
return {
|
|
2351
|
+
resolved: {
|
|
2352
|
+
id: layer.id,
|
|
2353
|
+
opacity: layer.opacity,
|
|
2354
|
+
tiles: [...layer.tiles]
|
|
2355
|
+
},
|
|
2356
|
+
metadata: {
|
|
2357
|
+
id: layer.id,
|
|
2358
|
+
opacity: layer.opacity,
|
|
2359
|
+
attribution,
|
|
2360
|
+
tiles
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
});
|
|
2364
|
+
return {
|
|
2365
|
+
layers: layers.map((layer) => layer.resolved),
|
|
2366
|
+
metadata: { layers: layers.map((layer) => layer.metadata) },
|
|
2367
|
+
warnings,
|
|
2368
|
+
attribution: allAttribution
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
2371
|
+
//#endregion
|
|
2372
|
+
//#region src/create-tactical-map-sheet.ts
|
|
2373
|
+
const FOOTPRINT_SEGMENTS_PER_EDGE = 16;
|
|
2374
|
+
const SCALE_SEARCH_BOUNDARY_DIVISIONS = 256;
|
|
1985
2375
|
function densifyFrameBoundary(center, projectedWidth, projectedHeight) {
|
|
1986
2376
|
const halfWidth = projectedWidth / 2;
|
|
1987
2377
|
const halfHeight = projectedHeight / 2;
|
|
@@ -2050,17 +2440,6 @@ function findMaximumFrameScaleDeviation(crs, center, projectedWidth, projectedHe
|
|
|
2050
2440
|
}
|
|
2051
2441
|
return maximum;
|
|
2052
2442
|
}
|
|
2053
|
-
function resolveProjection(requested, center) {
|
|
2054
|
-
const selected = geographicLibGridProjection.selectStandard(center);
|
|
2055
|
-
if (requested === void 0 || requested === "auto") return selected;
|
|
2056
|
-
if (requested.hemisphere !== selected.hemisphere) throw new RangeError("The explicit projection hemisphere must contain the sheet center");
|
|
2057
|
-
if (requested.kind === "ups" && selected.kind !== "ups") throw new RangeError("UPS can only be selected for a center in a polar UPS region");
|
|
2058
|
-
return requested;
|
|
2059
|
-
}
|
|
2060
|
-
function epsgFor(crs) {
|
|
2061
|
-
if (crs.kind === "ups") return crs.hemisphere === "north" ? 32661 : 32761;
|
|
2062
|
-
return (crs.hemisphere === "north" ? 32600 : 32700) + crs.zone;
|
|
2063
|
-
}
|
|
2064
2443
|
function mgrsGridZoneFor(crs, center) {
|
|
2065
2444
|
const longitude = center[0];
|
|
2066
2445
|
if (crs.kind === "ups") {
|
|
@@ -2079,13 +2458,11 @@ function scaleDeviationWarning(maximumRelativeDeviation) {
|
|
|
2079
2458
|
}] : [];
|
|
2080
2459
|
}
|
|
2081
2460
|
function createTacticalMapSheetDocument(request) {
|
|
2082
|
-
|
|
2083
|
-
const
|
|
2084
|
-
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
if (!(center.pointScale > 0)) throw new Error("Projection point scale must be positive");
|
|
2088
|
-
const projectedMetresPerMillimetre = request.scaleDenominator / 1e3 * center.pointScale;
|
|
2461
|
+
const { page, center, plan } = resolveMapSheetGeometry(request);
|
|
2462
|
+
const { frame: mapFrame, crs, epsg, projectedMetresPerMillimetre } = plan;
|
|
2463
|
+
if (request.registration && request.rasterBasemap) throw new RangeError("Registered overlays cannot include a raster basemap");
|
|
2464
|
+
const resolvedRasterBasemap = request.rasterBasemap && resolveRasterBasemap(request.rasterBasemap, mapFrame);
|
|
2465
|
+
if (resolvedRasterBasemap?.attribution.some((entry) => entry.required) && !request.marginalia) throw new RangeError("Required raster attribution needs enabled marginalia");
|
|
2089
2466
|
const projectedWidth = mapFrame.widthMm * projectedMetresPerMillimetre;
|
|
2090
2467
|
const projectedHeight = mapFrame.heightMm * projectedMetresPerMillimetre;
|
|
2091
2468
|
const footprintRing = densifyFrameBoundary(center, projectedWidth, projectedHeight).map((point) => geographicLibGridProjection.reverse(crs, point));
|
|
@@ -2113,7 +2490,6 @@ function createTacticalMapSheetDocument(request) {
|
|
|
2113
2490
|
projectedMetresPerMillimetre,
|
|
2114
2491
|
frame: mapFrame
|
|
2115
2492
|
});
|
|
2116
|
-
const epsg = epsgFor(crs);
|
|
2117
2493
|
const renderedMarginalia = request.marginalia && renderMarginalia(request.marginalia, {
|
|
2118
2494
|
page,
|
|
2119
2495
|
frame: mapFrame,
|
|
@@ -2123,20 +2499,22 @@ function createTacticalMapSheetDocument(request) {
|
|
|
2123
2499
|
mgrsGridZone: mgrsGridZoneFor(crs, request.center),
|
|
2124
2500
|
convergenceDegrees: center.convergenceDegrees,
|
|
2125
2501
|
footprintRing,
|
|
2126
|
-
metadata: request.metadata
|
|
2502
|
+
metadata: request.metadata,
|
|
2503
|
+
requiredRasterAttribution: resolvedRasterBasemap?.attribution.filter((entry) => entry.required).map((entry) => entry.text)
|
|
2127
2504
|
});
|
|
2128
|
-
const renderedPrinterCalibration = request.printerCalibration && renderPrinterCalibration(request.printerCalibration, page, mapFrame);
|
|
2505
|
+
const renderedPrinterCalibration = request.printerCalibration && renderPrinterCalibration(request.printerCalibration, page, mapFrame, renderedMarginalia ? renderedMarginalia.reservedBottomMm : void 0);
|
|
2129
2506
|
const fontStyles = [...renderedGrid || renderedMarginalia || renderedRegistration || renderedPrinterCalibration ? ["Regular"] : [], ...renderedLayers.fontStyles];
|
|
2130
2507
|
const scene = {
|
|
2131
2508
|
page,
|
|
2132
2509
|
mapFrame,
|
|
2133
2510
|
...request.page.background && request.page.background !== "transparent" ? { backgroundColor: request.page.background } : {},
|
|
2134
2511
|
layers: [
|
|
2512
|
+
...resolvedRasterBasemap ? renderRasterBasemapLayers(resolvedRasterBasemap) : [],
|
|
2135
2513
|
...renderedGrid ? [renderedGrid.layer] : [],
|
|
2136
2514
|
...renderedLayers.layers,
|
|
2137
2515
|
...renderedRegistration ? [renderedRegistration.layer] : []
|
|
2138
2516
|
],
|
|
2139
|
-
...renderedMarginalia || renderedPrinterCalibration ? { marginalia: [...renderedMarginalia ? [renderedMarginalia] : [], ...renderedPrinterCalibration ? [renderedPrinterCalibration] : []] } : {},
|
|
2517
|
+
...renderedMarginalia || renderedPrinterCalibration ? { marginalia: [...renderedMarginalia ? [renderedMarginalia.layer] : [], ...renderedPrinterCalibration ? [renderedPrinterCalibration] : []] } : {},
|
|
2140
2518
|
fonts: openSansResources(fontStyles),
|
|
2141
2519
|
patterns: renderedLayers.patterns
|
|
2142
2520
|
};
|
|
@@ -2151,6 +2529,7 @@ function createTacticalMapSheetDocument(request) {
|
|
|
2151
2529
|
warnings: [
|
|
2152
2530
|
...findCrossZoneWarning(crs, footprintRing),
|
|
2153
2531
|
...scaleDeviationWarning(maximumFrameScaleDeviation),
|
|
2532
|
+
...resolvedRasterBasemap?.warnings ?? [],
|
|
2154
2533
|
...renderedLayers.warnings
|
|
2155
2534
|
],
|
|
2156
2535
|
resources: {
|
|
@@ -2171,7 +2550,8 @@ function createTacticalMapSheetDocument(request) {
|
|
|
2171
2550
|
maximumFrameScaleDeviation
|
|
2172
2551
|
},
|
|
2173
2552
|
...renderedGrid ? { grid: renderedGrid.metadata } : {},
|
|
2174
|
-
...request.metadata ? { document: { ...request.metadata } } : {}
|
|
2553
|
+
...request.metadata ? { document: { ...request.metadata } } : {},
|
|
2554
|
+
...resolvedRasterBasemap ? { rasterBasemap: resolvedRasterBasemap.metadata } : {}
|
|
2175
2555
|
}
|
|
2176
2556
|
}
|
|
2177
2557
|
};
|
|
@@ -2180,4 +2560,4 @@ function createTacticalMapSheet(request) {
|
|
|
2180
2560
|
return createTacticalMapSheetDocument(request).result;
|
|
2181
2561
|
}
|
|
2182
2562
|
//#endregion
|
|
2183
|
-
export {
|
|
2563
|
+
export { geographicToMgrs as a, normalizeMgrsReference as c, snapGeographicToMgrs as d, snapProjectedToMgrs as f, openSansCentralBaselineOffset as g, POINTS_PER_MILLIMETRE as h, resolveMapSheetRasterPlan as i, parseMgrsReference as l, MILLIMETRES_PER_CSS_PIXEL as m, createTacticalMapSheetDocument as n, mgrsToGeographic as o, formatDecimal as p, mapSheetRasterPointToGeographic as r, mgrsToProjected as s, createTacticalMapSheet as t, projectedToMgrs as u };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { $ as SceneRectangle, A as PaperSizeName, B as ControlledVectorSymbol, C as ResolvedMapSheetRasterTileMetadata, D as ResolvedRegistrationMark, E as MapSheetRegistrationRequest, F as GraphicOutsideFrameWarning, G as VectorSymbolAnchor, H as MapSheetPointSymbol, I as GraphicRenderWarning, J as MgrsGridOptions, K as MapSheetMarginaliaOptions, L as MapSheetPortrayalDefaults, M as AuthoredMapSheetGraphic, N as GraphicClippedWarning, O as PAPER_SIZES, P as GraphicHiddenWarning, Q as MapSheetPatternIdentity, R as PointSymbolFallbackWarning, S as ResolvedMapSheetRasterLayerMetadata, T as AuthoredRegistrationMark, U as MapSheetPointSymbolCapability, V as MapSheetCustomSymbol, W as MapSheetSymbolSize, X as MgrsPrecision, Y as ResolvedMgrsGridMetadata, Z as MgrsSnapInterval, _ as MapSheetRasterLayer, a as ResolvedMapSheetMetadata, at as Position, b as RasterDpiAnisotropyWarning, c as TacticalMapSheetResources, d as UpsProjection, et as GeographicPosition, f as UtmProjection, g as MapSheetRasterBasemap, h as MapSheetRasterAttribution, i as MapSheetPageInput, j as resolvePaperSize, k as PaperOrientation, l as TacticalMapSheetResult, m as LowRasterDpiWarning, n as CrossZoneFootprintWarning, nt as UpsCrs, o as ScaleDeviationWarning, p as createTacticalMapSheet, q as MapSheetMetadata, r as MapSheetFontIdentity, rt as UtmCrs, s as TacticalMapSheetRequest, t as CrossProjectionFootprintWarning, tt as GridCrs, u as TacticalMapSheetWarning, v as MapSheetRasterTile, w as PrinterCalibrationRequest, x as ResolvedMapSheetRasterBasemapMetadata, y as MapSheetRasterWarning, z as TacticalMapSheetLayer } from "./create-tactical-map-sheet-Cydh9ghW.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/mgrs.d.ts
|
|
4
4
|
interface ProjectedGridCoordinate {
|
|
@@ -24,4 +24,22 @@ declare function geographicToMgrs(position: GeographicPosition, precision?: Mgrs
|
|
|
24
24
|
declare function snapProjectedToMgrs(coordinate: ProjectedGridCoordinate, interval: MgrsSnapInterval): ResolvedMgrsReference;
|
|
25
25
|
declare function snapGeographicToMgrs(position: GeographicPosition, interval: MgrsSnapInterval): ResolvedMgrsReference;
|
|
26
26
|
//#endregion
|
|
27
|
-
|
|
27
|
+
//#region src/raster-plan.d.ts
|
|
28
|
+
interface MapSheetProjectedBounds {
|
|
29
|
+
readonly left: number;
|
|
30
|
+
readonly bottom: number;
|
|
31
|
+
readonly right: number;
|
|
32
|
+
readonly top: number;
|
|
33
|
+
}
|
|
34
|
+
interface MapSheetRasterPlan {
|
|
35
|
+
readonly crs: GridCrs;
|
|
36
|
+
readonly epsg: number;
|
|
37
|
+
readonly frame: SceneRectangle;
|
|
38
|
+
readonly projectedBounds: MapSheetProjectedBounds;
|
|
39
|
+
readonly projectedMetresPerMillimetre: number;
|
|
40
|
+
}
|
|
41
|
+
type RasterPlanRequest = Pick<TacticalMapSheetRequest, "page" | "mapFrame" | "center" | "scaleDenominator" | "projection">;
|
|
42
|
+
declare function resolveMapSheetRasterPlan(request: RasterPlanRequest): MapSheetRasterPlan;
|
|
43
|
+
declare function mapSheetRasterPointToGeographic(plan: MapSheetRasterPlan, normalizedX: number, normalizedY: number): Position;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { type AuthoredMapSheetGraphic, type AuthoredRegistrationMark, type ControlledVectorSymbol, type CrossProjectionFootprintWarning, type CrossZoneFootprintWarning, type GraphicClippedWarning, type GraphicHiddenWarning, type GraphicOutsideFrameWarning, type GraphicRenderWarning, type GridCrs, type LowRasterDpiWarning, type MapSheetCustomSymbol, type MapSheetFontIdentity, type MapSheetMarginaliaOptions, type MapSheetMetadata, type MapSheetPageInput, type MapSheetPatternIdentity, type MapSheetPointSymbol, type MapSheetPointSymbolCapability, type MapSheetPortrayalDefaults, type MapSheetProjectedBounds, type MapSheetRasterAttribution, type MapSheetRasterBasemap, type MapSheetRasterLayer, type MapSheetRasterPlan, type MapSheetRasterTile, type MapSheetRasterWarning, type MapSheetRegistrationRequest, type MapSheetSymbolSize, type MgrsGridOptions, type MgrsPrecision, type MgrsSnapInterval, PAPER_SIZES, type PaperOrientation, type PaperSizeName, type PointSymbolFallbackWarning, type PrinterCalibrationRequest, type ProjectedGridCoordinate, type RasterDpiAnisotropyWarning, type ResolvedMapSheetMetadata, type ResolvedMapSheetRasterBasemapMetadata, type ResolvedMapSheetRasterLayerMetadata, type ResolvedMapSheetRasterTileMetadata, 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, mapSheetRasterPointToGeographic, mgrsToGeographic, mgrsToProjected, normalizeMgrsReference, parseMgrsReference, projectedToMgrs, resolveMapSheetRasterPlan, resolvePaperSize, snapGeographicToMgrs, snapProjectedToMgrs };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as geographicToMgrs, c as normalizeMgrsReference, d as snapGeographicToMgrs, f as snapProjectedToMgrs, i as resolveMapSheetRasterPlan, l as parseMgrsReference, o as mgrsToGeographic, r as mapSheetRasterPointToGeographic, s as mgrsToProjected, t as createTacticalMapSheet, u as projectedToMgrs } from "./create-tactical-map-sheet-D2G1PW2H.mjs";
|
|
2
2
|
//#region src/paper.ts
|
|
3
3
|
const A0 = Object.freeze({
|
|
4
4
|
widthMm: 841,
|
|
@@ -80,4 +80,4 @@ function resolvePaperSize(name, orientation = "portrait") {
|
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
//#endregion
|
|
83
|
-
export { PAPER_SIZES, createTacticalMapSheet, geographicToMgrs, mgrsToGeographic, mgrsToProjected, normalizeMgrsReference, parseMgrsReference, projectedToMgrs, resolvePaperSize, snapGeographicToMgrs, snapProjectedToMgrs };
|
|
83
|
+
export { PAPER_SIZES, createTacticalMapSheet, geographicToMgrs, mapSheetRasterPointToGeographic, mgrsToGeographic, mgrsToProjected, normalizeMgrsReference, parseMgrsReference, projectedToMgrs, resolveMapSheetRasterPlan, resolvePaperSize, snapGeographicToMgrs, snapProjectedToMgrs };
|
package/dist/pdf.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ResolvedMapSheetMetadata, c as TacticalMapSheetResources,
|
|
1
|
+
import { a as ResolvedMapSheetMetadata, c as TacticalMapSheetResources, it as Polygon, l as TacticalMapSheetResult, s as TacticalMapSheetRequest, u as TacticalMapSheetWarning } from "./create-tactical-map-sheet-Cydh9ghW.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/pdf.d.ts
|
|
4
4
|
interface TacticalMapSheetPdfResult {
|
package/dist/pdf.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { g as openSansCentralBaselineOffset, h as POINTS_PER_MILLIMETRE, m as MILLIMETRES_PER_CSS_PIXEL, n as createTacticalMapSheetDocument, p as formatDecimal } from "./create-tactical-map-sheet-D2G1PW2H.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");
|
|
@@ -512,6 +512,13 @@ function renderText(pdf, text) {
|
|
|
512
512
|
pdf.restoreGraphicsState();
|
|
513
513
|
}
|
|
514
514
|
function renderElement(pdf, element, formIds) {
|
|
515
|
+
if (element.kind === "raster-image") {
|
|
516
|
+
pdf.saveGraphicsState();
|
|
517
|
+
if (element.opacity !== 1) pdf.setGState(pdf.GState({ opacity: element.opacity }));
|
|
518
|
+
pdf.addImage(element.data, element.mediaType === "image/png" ? "PNG" : "JPEG", element.frame.xMm, element.frame.yMm, element.frame.widthMm, element.frame.heightMm, element.resourceId, "NONE");
|
|
519
|
+
pdf.restoreGraphicsState();
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
515
522
|
if (element.kind === "text") return renderText(pdf, element);
|
|
516
523
|
if (element.kind === "vector-symbol") {
|
|
517
524
|
pdf.saveGraphicsState();
|
|
@@ -624,7 +631,14 @@ function registerFont(pdf, font) {
|
|
|
624
631
|
pdf.addFont(fileName, "Open Sans", style, weight, "Identity-H");
|
|
625
632
|
}
|
|
626
633
|
function canonicalizeScene(scene) {
|
|
627
|
-
|
|
634
|
+
const rasterData = [];
|
|
635
|
+
return JSON.parse(JSON.stringify(scene, (_key, value) => {
|
|
636
|
+
if (value instanceof Uint8Array) return { rasterDataIndex: rasterData.push(value) - 1 };
|
|
637
|
+
return typeof value === "number" ? Number(formatDecimal(value)) : value;
|
|
638
|
+
}), (_key, value) => {
|
|
639
|
+
if (value && typeof value === "object" && Object.keys(value).length === 1 && "rasterDataIndex" in value && typeof value.rasterDataIndex === "number") return rasterData[value.rasterDataIndex];
|
|
640
|
+
return value;
|
|
641
|
+
});
|
|
628
642
|
}
|
|
629
643
|
function renderLayers(pdf, layers, formIds) {
|
|
630
644
|
for (const layer of layers) for (const graphic of layer.graphics) for (const element of graphic.elements) renderElement(pdf, element, formIds);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbat-mapper/tactical-map-sheet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Scale-accurate projected vector tactical map-sheet renderer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Orbat Mapper",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"jspdf": "4.2.1",
|
|
31
31
|
"opentype.js": "2.0.0",
|
|
32
32
|
"saxes": "6.0.0",
|
|
33
|
-
"@orbat-mapper/control-measures": "^0.
|
|
33
|
+
"@orbat-mapper/control-measures": "^0.11.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@napi-rs/canvas": "1.0.7",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"typescript": "~5.9.3",
|
|
45
45
|
"vite": "^8.0.16",
|
|
46
46
|
"vitest": "^4.1.8",
|
|
47
|
-
"@orbat-mapper/point-symbols": "0.1.
|
|
47
|
+
"@orbat-mapper/point-symbols": "0.1.10"
|
|
48
48
|
},
|
|
49
49
|
"inlinedDependencies": {
|
|
50
50
|
"@types/geojson": "7946.0.16"
|