@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 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 | 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
- | 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. |
18
- | 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. |
19
- | 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. |
20
- | 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. |
21
- | 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. |
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 in and deterministic vector document
24
- results out. Application stores, engine-rendered features, export UI, authorization, preview, and file
25
- saving remain application responsibilities.
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, physically registered overlays,
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.
@@ -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 { 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 };
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 };
@@ -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, context.page.heightMm - 1.5, 4, color));
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
- id: "sheet-marginalia-0",
522
- graphics: [{
523
- id: "marginalia",
524
- elements
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("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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 yMm = frameBottom + (page.heightMm - frameBottom) / 2;
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 > page.heightMm) throw new RangeError("The bottom page margin must fit the complete printer-calibration line, ticks, and label");
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/create-tactical-map-sheet.ts
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
- requirePositiveFinite(request.scaleDenominator, "Scale denominator");
2083
- const page = resolvePage(request.page);
2084
- const mapFrame = resolveFrame(request, page);
2085
- const crs = resolveProjection(request.projection, request.center);
2086
- const center = geographicLibGridProjection.forward(crs, request.center);
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 { 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 };
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 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";
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
- 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 };
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 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";
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, l as TacticalMapSheetResult, q as Polygon, s as TacticalMapSheetRequest, u as TacticalMapSheetWarning } from "./create-tactical-map-sheet-T2q1nHWO.mjs";
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 { 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";
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
- return JSON.parse(JSON.stringify(scene, (_key, value) => typeof value === "number" ? Number(formatDecimal(value)) : value));
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.2.2",
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.10.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.9"
47
+ "@orbat-mapper/point-symbols": "0.1.10"
48
48
  },
49
49
  "inlinedDependencies": {
50
50
  "@types/geojson": "7946.0.16"