@nika-js/onlymap 0.7.6 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +3 -1
  3. package/dist/{LercDecode.es-sRuTJq5Y.js → LercDecode.es-DKhLh4TO.js} +1 -1
  4. package/dist/{basemap-BCGMmc2F.js → basemap-BjXMRr_M.js} +538 -502
  5. package/dist/basemap.d.ts +23 -0
  6. package/dist/cartograph/api.d.ts +45 -0
  7. package/dist/cartograph/atlas.d.ts +50 -0
  8. package/dist/cartograph/core-loader.d.ts +35 -0
  9. package/dist/cartograph/cvd.d.ts +106 -0
  10. package/dist/cartograph/element-base.d.ts +26 -0
  11. package/dist/cartograph/elements/om-atlas.d.ts +47 -0
  12. package/dist/cartograph/elements/om-cartograph.d.ts +76 -0
  13. package/dist/cartograph/elements/om-frame.d.ts +121 -0
  14. package/dist/cartograph/elements/om-graticule.d.ts +24 -0
  15. package/dist/cartograph/elements/om-image.d.ts +12 -0
  16. package/dist/cartograph/elements/om-legend.d.ts +16 -0
  17. package/dist/cartograph/elements/om-north.d.ts +11 -0
  18. package/dist/cartograph/elements/om-scalebar.d.ts +13 -0
  19. package/dist/cartograph/elements/om-shape.d.ts +16 -0
  20. package/dist/cartograph/elements/om-text.d.ts +9 -0
  21. package/dist/cartograph/georef.d.ts +126 -0
  22. package/dist/cartograph/graticule.d.ts +70 -0
  23. package/dist/cartograph/html-data.d.ts +27 -0
  24. package/dist/cartograph/index.d.ts +1 -0
  25. package/dist/cartograph/layout.d.ts +23 -0
  26. package/dist/cartograph/legend.d.ts +75 -0
  27. package/dist/cartograph/live-frame.d.ts +83 -0
  28. package/dist/cartograph/paint.d.ts +54 -0
  29. package/dist/cartograph/refs.d.ts +33 -0
  30. package/dist/cartograph/render.d.ts +64 -0
  31. package/dist/cartograph/scalebar.d.ts +91 -0
  32. package/dist/cartograph/schema.d.ts +38 -0
  33. package/dist/cartograph/standalone.d.ts +1 -0
  34. package/dist/cartograph/textlayout.d.ts +45 -0
  35. package/dist/cartograph/tokens.d.ts +45 -0
  36. package/dist/cartograph/url-actions.d.ts +26 -0
  37. package/dist/cartograph/validate.d.ts +38 -0
  38. package/dist/cartograph/zip.d.ts +22 -0
  39. package/dist/cartograph.css +1 -0
  40. package/dist/cartograph.js +2797 -0
  41. package/dist/cartograph.standalone.js +7669 -0
  42. package/dist/crs-DsDQ4Q4i.js +71 -0
  43. package/dist/download.d.ts +6 -0
  44. package/dist/elements/om-map.d.ts +2 -0
  45. package/dist/feature-access.d.ts +14 -0
  46. package/dist/field-access.d.ts +1 -10
  47. package/dist/{geoparquet-CK9IjEjH.js → geoparquet-D9pYDrCk.js} +1 -1
  48. package/dist/{index-CN_tn36D.js → index-BGQidVp5.js} +1 -1
  49. package/dist/{index-CIEJyv5x.js → index-BdKNZcs-.js} +1 -1
  50. package/dist/index-CcLC9jE5.js +4798 -0
  51. package/dist/{index-C1ZQGJxa.js → index-CqtubBZ4.js} +1 -1
  52. package/dist/{index-CNRYpb0x.js → index-D6R4z3Zs.js} +6296 -6203
  53. package/dist/{index-CW3bdfHB.js → index-Zm8MxpvR.js} +2 -2
  54. package/dist/index.d.ts +1 -1
  55. package/dist/{lerc-Bb2JZ92v.js → lerc-BdHcL8pi.js} +2 -2
  56. package/dist/onlymap.standalone.js +10777 -10648
  57. package/dist/onlymapjs.js +12 -11
  58. package/dist/{raster-DpYssEe7.js → raster-B7rPpOTP.js} +3 -3
  59. package/dist/{raster-pipeline-BVCB-0yq.js → raster-pipeline-X2-mnFQx.js} +1 -1
  60. package/dist/runtime-core.d.ts +42 -0
  61. package/dist/snapshot.d.ts +17 -0
  62. package/dist/units.d.ts +9 -0
  63. package/dist/version.d.ts +1 -1
  64. package/dist/{zarr-DNb4iqbL.js → zarr-BF-buFRB.js} +2 -2
  65. package/docs/cartograph.md +396 -0
  66. package/llms.txt +1 -0
  67. package/onlymapjs.html-data.json +978 -0
  68. package/package.json +17 -6
  69. package/skills/onlymapjs/SKILL.md +2 -2
  70. package/skills/onlymapjs/references/syntax.md +27 -2
@@ -0,0 +1,9 @@
1
+ import { CartographElementBase } from "../element-base";
2
+ import { type CartographPainter, type PaintEnv } from "../paint";
3
+ export declare class OmTextElement extends CartographElementBase implements CartographPainter {
4
+ static readonly observedAttributes: string[];
5
+ protected renderNow(): void;
6
+ /** Re-run token substitution from the authored templates. */
7
+ resolveTokens(): void;
8
+ paintCartograph(ctx: CanvasRenderingContext2D, env: PaintEnv): void;
9
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Frame georeferencing — the ONE interface every piece of furniture consumes
3
+ * (scale bar, north indicator, graticule, locator inset, tokens), so nothing
4
+ * else has to know whether a frame is a live map or a static raster.
5
+ *
6
+ * THE LOAD-BEARING DECISION: the projective homography is fitted in the
7
+ * frame's CRS PLANE, never in raw lon/lat.
8
+ *
9
+ * A map image is a projective (perspective) transform of its own projection
10
+ * plane — that is exactly what a pitched camera does to flat map coordinates,
11
+ * and what a north-up crop does trivially. It is NOT a projective transform of
12
+ * longitude/latitude: Web Mercator's y = R·ln(tan(π/4+φ/2)) is nonlinear in
13
+ * latitude, so a homography fitted to four lon/lat corners reproduces those
14
+ * four corners exactly and misplaces every interior point, with error growing
15
+ * with the frame's latitude span (a 40°-span frame is off by millimetres at
16
+ * the centre — a visibly wrong graticule). Fitting in the CRS plane makes the
17
+ * corner fit exact AND the interior correct:
18
+ *
19
+ * project(lngLat) = H · crsForward(lngLat)
20
+ *
21
+ * `crsForward` is closed-form for the two CRSs that need no dependency —
22
+ * EPSG:3857 (spherical Mercator) and EPSG:4326 (plate carrée, where the
23
+ * forward IS identity, which is why a naive lon/lat fit happens to be right
24
+ * for equirectangular rasters and wrong for everything else). Any other
25
+ * projected CRS resolves through the core's lazy proj4 (src/crs.ts) — the
26
+ * only reason this module ever awaits anything.
27
+ */
28
+ import type { LngLat } from "../geodesy";
29
+ /** A planar coordinate in a CRS (metres for projected CRSs, degrees for geographic). */
30
+ export type PlaneXY = [number, number];
31
+ /** Forward/inverse pair for a frame's CRS — the plane the homography is fitted in. */
32
+ export interface CrsForward {
33
+ code: string;
34
+ forward(lngLat: LngLat): PlaneXY;
35
+ inverse(xy: PlaneXY): LngLat;
36
+ }
37
+ export declare function mercatorForward([lng, lat]: LngLat): PlaneXY;
38
+ export declare function mercatorInverse([x, y]: PlaneXY): LngLat;
39
+ /** Normalize "3857" / "epsg:3857" / "EPSG::3857" to "EPSG:3857"; null when unparseable. */
40
+ export declare function normalizeCrs(raw: string | null | undefined): string | null;
41
+ /** The CRSs this module handles with no proj4 load. */
42
+ export declare function builtinCrsForward(crs: string | null | undefined): CrsForward | null;
43
+ /**
44
+ * Resolve any CRS to a forward/inverse pair, loading proj4 (lazily, through
45
+ * the core's own module) only for codes without a closed form. `crsDef` is an
46
+ * authored "+proj=…" string for CRSs the library does not bundle.
47
+ */
48
+ export declare function resolveCrsForward(crs: string | null | undefined, crsDef?: string | null): Promise<CrsForward | null>;
49
+ /** Formulaic UTM definition — the zones no regional table can enumerate. */
50
+ export declare function utmDef(zone: number, hemisphere: "N" | "S"): string | null;
51
+ /** Row-major 3×3: [a,b,c, d,e,f, g,h,i] mapping [x,y,1] → [u,v,w]. */
52
+ export type Mat3 = readonly [number, number, number, number, number, number, number, number, number];
53
+ /**
54
+ * The homography taking four source points to four destination points.
55
+ * Sources are normalized (centroid-shifted, isotropically scaled) before the
56
+ * fit — CRS-plane coordinates run to ±20 000 000 m, and an unnormalized 8×8
57
+ * DLT on those loses most of its precision.
58
+ */
59
+ export declare function homographyFromQuad(src: readonly PlaneXY[], dst: readonly PlaneXY[]): {
60
+ matrix: Mat3;
61
+ normalize: (p: PlaneXY) => PlaneXY;
62
+ denormalize: (p: PlaneXY) => PlaneXY;
63
+ } | null;
64
+ export declare function applyMat3(m: Mat3, [x, y]: PlaneXY): PlaneXY | null;
65
+ export declare function invertMat3(m: Mat3): Mat3 | null;
66
+ export interface FrameGeoref {
67
+ /** The frame's CRS, normalized (e.g. "EPSG:3857"). */
68
+ readonly crs: string;
69
+ /** The frame's four corners in lon/lat, order tl, tr, br, bl. */
70
+ readonly corners: readonly [LngLat, LngLat, LngLat, LngLat];
71
+ readonly widthMm: number;
72
+ readonly heightMm: number;
73
+ /**
74
+ * True when the corner quad could not produce a usable mapping (collinear
75
+ * corners, a near-horizon capture). Furniture must refuse to draw rather
76
+ * than draw something wrong; the validator surfaces it.
77
+ */
78
+ readonly degenerate: boolean;
79
+ /** Lon/lat → millimetres from the FRAME's top-left. Null when off the projective horizon. */
80
+ project(lngLat: LngLat): {
81
+ xMm: number;
82
+ yMm: number;
83
+ } | null;
84
+ /** Millimetres from the frame's top-left → lon/lat. Null when unmappable. */
85
+ unproject(xMm: number, yMm: number): LngLat | null;
86
+ /** Ground metres per page millimetre along the frame's horizontal centre line. */
87
+ groundMetersPerMm(): number;
88
+ }
89
+ export interface CreateGeorefOptions {
90
+ corners: readonly [LngLat, LngLat, LngLat, LngLat];
91
+ widthMm: number;
92
+ heightMm: number;
93
+ /** Resolved CRS plane; defaults to Web Mercator (what live frames always are). */
94
+ crsForward?: CrsForward | null;
95
+ }
96
+ export declare function createFrameGeoref({ corners, widthMm, heightMm, crsForward }: CreateGeorefOptions): FrameGeoref;
97
+ /**
98
+ * Parse a `corners=` attribute: JSON `[[lng,lat],[lng,lat],[lng,lat],[lng,lat]]`
99
+ * (tl, tr, br, bl) or a flat list of eight numbers in the same order. Returns
100
+ * null for anything else — the validator turns that into an error, because a
101
+ * frame placed from bad corners looks exactly like one placed from good ones.
102
+ */
103
+ export declare function parseCorners(raw: string | null | undefined): [LngLat, LngLat, LngLat, LngLat] | null;
104
+ /**
105
+ * Which way is north, in degrees clockwise from the frame's "up".
106
+ *
107
+ * Derived from the georeference rather than from a `bearing` attribute, so it
108
+ * is correct for every frame kind — a rotated capture, a projected static
109
+ * raster whose grid north differs from true north, a pitched view. Null when
110
+ * the frame has no usable mapping.
111
+ */
112
+ export declare function northBearingDegrees(georef: FrameGeoref): number | null;
113
+ /**
114
+ * Relative scale variation across a frame's width, as a fraction (0.02 = 2%).
115
+ *
116
+ * The quantity a scale bar's honesty depends on: in Mercator the ground
117
+ * distance per page millimetre changes with sec(latitude), so a bar is only
118
+ * true near its anchor. Measuring the actual variation is the right test —
119
+ * a 5° span at the equator is harmless (~0.4%) while 3° at 60°N is not (~9%).
120
+ */
121
+ export declare function scaleVariation(georef: FrameGeoref): number | null;
122
+ /**
123
+ * Corners for a north-up frame from a lon/lat bounding box — the shape a
124
+ * producer has when it exported a plain map view rather than a pitched one.
125
+ */
126
+ export declare function cornersFromBounds(west: number, south: number, east: number, north: number): [LngLat, LngLat, LngLat, LngLat];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Graticule generation — coordinate grid lines over a frame.
3
+ *
4
+ * Two grids, one algorithm. A GEOGRAPHIC graticule draws meridians and
5
+ * parallels at whole degrees; a PROJECTED grid (a UTM sheet's 1 km squares)
6
+ * draws lines at whole eastings and northings in the frame's projected CRS.
7
+ * Both are "walk a regular lattice in some coordinate system, project each
8
+ * point through the frame's georeference, clip to the frame" — so both come
9
+ * out of `buildGraticule` with the grid CRS supplying the lattice.
10
+ *
11
+ * Lines are DENSIFIED rather than drawn corner to corner: a straight line in
12
+ * one coordinate system is a curve in another, and a two-point meridian would
13
+ * visibly bow away from the truth on a wide or rotated frame.
14
+ */
15
+ import type { LngLat } from "../geodesy";
16
+ import type { FrameGeoref } from "./georef";
17
+ export interface GraticuleLine {
18
+ /** Points in frame millimetres. */
19
+ points: {
20
+ xMm: number;
21
+ yMm: number;
22
+ }[];
23
+ /** The coordinate value this line marks (degrees, or projected metres). */
24
+ value: number;
25
+ axis: "x" | "y";
26
+ }
27
+ export interface GraticuleLabel {
28
+ text: string;
29
+ xMm: number;
30
+ yMm: number;
31
+ /** Which frame edge the label hangs off. */
32
+ edge: "top" | "bottom" | "left" | "right";
33
+ axis: "x" | "y";
34
+ }
35
+ export interface Graticule {
36
+ lines: GraticuleLine[];
37
+ labels: GraticuleLabel[];
38
+ /** The interval actually used, after any automatic choice. */
39
+ intervalX: number;
40
+ intervalY: number;
41
+ }
42
+ /** A plane the lattice is walked in: identity for geographic, proj4 for projected. */
43
+ export interface GridPlane {
44
+ /** Grid coordinates → lon/lat, so the georeference can place them. */
45
+ toLngLat(x: number, y: number): LngLat;
46
+ /** Lon/lat → grid coordinates, for working out which lines are in view. */
47
+ fromLngLat(lngLat: LngLat): [number, number];
48
+ }
49
+ /** The geographic plane: grid coordinates ARE lon/lat. */
50
+ export declare const GEOGRAPHIC_PLANE: GridPlane;
51
+ /** A "nice" interval (1-2-5 × 10ⁿ) giving roughly `target` divisions across a span. */
52
+ export declare function niceInterval(span: number, target?: number): number;
53
+ export interface BuildGraticuleOptions {
54
+ georef: FrameGeoref;
55
+ plane?: GridPlane;
56
+ /** Grid spacing in the plane's units; omitted, a nice interval is chosen. */
57
+ interval?: number | null;
58
+ intervalX?: number | null;
59
+ intervalY?: number | null;
60
+ labels?: "none" | "inside" | "outside";
61
+ /** Formats a coordinate value for its label. */
62
+ format?: (value: number, axis: "x" | "y") => string;
63
+ }
64
+ /**
65
+ * Build the grid for a frame. Returns null when the frame has no usable
66
+ * georeference — furniture must then refuse to draw rather than guess.
67
+ */
68
+ export declare function buildGraticule({ georef, plane, interval, intervalX, intervalY, labels, format }: BuildGraticuleOptions): Graticule | null;
69
+ /** Degrees-minutes-seconds, for a geographic graticule that wants it. */
70
+ export declare function formatDms(value: number, axis: "x" | "y"): string;
@@ -0,0 +1,27 @@
1
+ export interface CartographAttrData {
2
+ name: string;
3
+ description: string;
4
+ values?: {
5
+ name: string;
6
+ }[];
7
+ }
8
+ export interface CartographTagData {
9
+ name: string;
10
+ description: string;
11
+ attributes: CartographAttrData[];
12
+ references?: {
13
+ name: string;
14
+ url: string;
15
+ }[];
16
+ }
17
+ /**
18
+ * Descriptions for the placement attributes every placed element carries
19
+ * (schema.ts PLACED_ATTR_NAMES). Millimetres from the page top-left.
20
+ */
21
+ export declare const PLACED_COMMON: CartographAttrData[];
22
+ /**
23
+ * The cartograph tags for `onlymapjs.html-data.json` — merged into
24
+ * buildHtmlCustomData()'s output by src/html-data.ts so ONE custom-data
25
+ * file ships for the whole package.
26
+ */
27
+ export declare function buildCartographHtmlData(): CartographTagData[];
@@ -0,0 +1 @@
1
+ export * from "./api";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Page geometry — the one place millimetres meet pixels.
3
+ *
4
+ * CSS defines 1mm ≡ 96/25.4 px, so elements positioned with mm CSS units
5
+ * agree between screen preview and print BY CONSTRUCTION; this constant
6
+ * exists only for canvas compositing (renderCartograph) and tests. Nothing
7
+ * else in src/cartograph/ multiplies by 3.78.
8
+ */
9
+ export declare const PX_PER_MM: number;
10
+ export interface PageSizeMm {
11
+ widthMm: number;
12
+ heightMm: number;
13
+ }
14
+ /** Page presets in PORTRAIT mm — orientation="landscape" swaps them. */
15
+ export declare const PAGE_PRESETS: Readonly<Record<string, PageSizeMm>>;
16
+ export declare const DEFAULT_PAGE_SIZE = "A4";
17
+ /**
18
+ * Resolve the page box from the authored attributes. Explicit width+height
19
+ * (custom size) win over the preset; an unknown preset falls back to A4 so
20
+ * the page still renders (the validator flags it — Phase 1). Orientation
21
+ * applies to presets only: custom dimensions are already oriented.
22
+ */
23
+ export declare function resolvePageSize(size: string | null, width: string | null, height: string | null, orientation: string | null): PageSizeMm;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Legend derivation.
3
+ *
4
+ * THE INVARIANT: a cartograph legend is never stored. Its rows are computed
5
+ * from the frame's map at render time, so the legend cannot drift out of
6
+ * agreement with the map it describes — the failure class that makes printed
7
+ * cartography untrustworthy, and the one QGIS still has.
8
+ *
9
+ * The source of truth is the mounted map's own IR (`getLayers()` →
10
+ * `meta.legend`, `meta.label`, `meta.color`), NOT a re-parse of the authored
11
+ * colour attributes. That matters: `meta.legend` is also populated by the
12
+ * `classify=` family (class breaks computed from the DATA, which no static
13
+ * read of the attributes could reproduce) and by rasters resolving their
14
+ * domain at load. Re-deriving from source would silently disagree with the
15
+ * map for exactly the layers whose symbology was computed rather than
16
+ * authored.
17
+ */
18
+ import type { LegendSpec } from "../legend-spec";
19
+ /** The swatch a row draws — the geometry a reader expects for that layer kind. */
20
+ export type SwatchShape = "polygon" | "line" | "point" | "ramp";
21
+ export interface LegendRow {
22
+ layerId: string;
23
+ label: string;
24
+ /** Primary colour for the row's swatch. */
25
+ color: string;
26
+ shape: SwatchShape;
27
+ /** Full symbology detail (ramp stops, class or category entries), when the layer has one. */
28
+ legend?: LegendSpec;
29
+ /** Sort key from an `order=` override; rows without one keep map order. */
30
+ order?: number;
31
+ }
32
+ /** The subset of a layer's IR the legend needs — structurally what `getLayers()` returns. */
33
+ export interface LegendLayerInput {
34
+ id: string;
35
+ type?: string;
36
+ meta?: {
37
+ label?: string;
38
+ color?: string;
39
+ legend?: LegendSpec;
40
+ };
41
+ props?: {
42
+ visible?: boolean;
43
+ [key: string]: unknown;
44
+ };
45
+ }
46
+ export declare function swatchShapeFor(layer: LegendLayerInput): SwatchShape;
47
+ /** The row swatch, matching the core legend widget's own rule exactly. */
48
+ export declare function rowColorFor(layer: LegendLayerInput): string;
49
+ /** Categories beyond this many collapse into a "+N more" row — a printed legend has finite space. */
50
+ export declare const MAX_CATEGORY_ENTRIES = 12;
51
+ export interface LegendRowOverride {
52
+ layer: string;
53
+ /** Which derived row to target: a category value or class label. Absent targets the layer's row. */
54
+ match?: string;
55
+ label?: string;
56
+ hide?: boolean;
57
+ order?: number;
58
+ color?: string;
59
+ shape?: SwatchShape;
60
+ }
61
+ /**
62
+ * Derive the rows for a legend from a frame's layers, applying overrides.
63
+ *
64
+ * Hidden layers are omitted (the core legend widget shows the layer row but
65
+ * not its symbology; on paper an invisible layer has no business appearing at
66
+ * all).
67
+ */
68
+ export declare function deriveLegendRows(layers: readonly LegendLayerInput[], overrides?: readonly LegendRowOverride[]): LegendRow[];
69
+ /** Read `<om-legend-row>` children into override records. */
70
+ export declare function readOverrides(legendEl: Element): LegendRowOverride[];
71
+ /**
72
+ * Literal rows for a legend that cannot derive — a static frame has no layers,
73
+ * so the producer writes the rows and marks the legend `derived="false"`.
74
+ */
75
+ export declare function readLiteralRows(legendEl: Element): LegendRow[];
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Mounting a live map inside a frame, and turning its camera into a
3
+ * georeference.
4
+ *
5
+ * Two jobs the frame element delegates here so its own file stays about
6
+ * placement and rendering:
7
+ *
8
+ * 1. SANITIZED ADOPTION of a map manifest fetched from `src=`. A cartograph
9
+ * may reference a map document from anywhere, and the fetched HTML is
10
+ * rebuilt element by element rather than assigned through innerHTML:
11
+ * scripts are dropped except the inert `application/json` blocks that
12
+ * carry inline layer data, and `on*` handler attributes never survive.
13
+ * An inline `<om-map>` CHILD is not sanitized — the author already owns
14
+ * that document.
15
+ * 2. CORNERS FROM THE CAMERA. A live frame's georeference comes from its own
16
+ * `center/zoom/bearing/pitch` and its pixel box, through the core's
17
+ * WebMercatorViewport — the same viewport class the renderer uses, so the
18
+ * furniture agrees with the pixels.
19
+ */
20
+ import type { LngLat } from "../geodesy";
21
+ import { type CoreModule } from "./core-loader";
22
+ export type Corners = [LngLat, LngLat, LngLat, LngLat];
23
+ /**
24
+ * Rebuild a fetched subtree as fresh elements, keeping only inert content.
25
+ * Returns null if the source has nothing to adopt.
26
+ */
27
+ export declare function sanitizeAdoptedMap(source: Element, doc: Document): Element | null;
28
+ /** Fetch a map document and return its `<om-map>`, sanitized and ready to mount. */
29
+ export declare function fetchMapElement(src: string, doc?: Document): Promise<Element>;
30
+ export interface FrameCamera {
31
+ center: LngLat;
32
+ zoom: number;
33
+ bearing: number;
34
+ pitch: number;
35
+ }
36
+ /** Read the frame's own camera; null when it has not authored one. */
37
+ export declare function readCamera(el: Element): FrameCamera | null;
38
+ /**
39
+ * The four ground corners a camera sees in a `widthPx × heightPx` box, in
40
+ * tl, tr, br, bl order.
41
+ *
42
+ * Null when the view cannot be georeferenced as a quad: past roughly 60° of
43
+ * pitch the top edge unprojects at or beyond the horizon, where "the ground
44
+ * under this pixel" has no finite answer. Returning null there — rather than
45
+ * a huge number that looks like a coordinate — is what lets the frame mark
46
+ * itself degenerate and the furniture refuse to draw.
47
+ */
48
+ /**
49
+ * The steepest pitch this frame can still georeference, in whole degrees.
50
+ *
51
+ * There is no constant to use here. The corners are unprojected through the
52
+ * frame's OWN box, so the ceiling moves with its aspect and the zoom: a wide
53
+ * frame tilts further than a tall one, and zooming in raises the limit again.
54
+ * A fixed 45° or 60° would be wrong in both directions — too strict on the
55
+ * frames that could tilt, too lax on the ones that cannot.
56
+ *
57
+ * Coarse then fine: the answer only needs to be whole degrees, and each probe
58
+ * is four unprojections, so this is a handful of microseconds rather than
59
+ * something worth caching.
60
+ */
61
+ export declare function maxGeoreferenceablePitch(core: CoreModule, camera: FrameCamera, widthPx: number, heightPx: number, ceiling?: number): number;
62
+ export declare function cornersFromCamera(core: CoreModule, camera: FrameCamera, widthPx: number, heightPx: number): Corners | null;
63
+ /** Load the core and build an `<om-map>` for a frame, from `src=` or an inline child. */
64
+ export declare function buildLiveMap(frame: Element): Promise<{
65
+ core: CoreModule;
66
+ map: Element;
67
+ }>;
68
+ /**
69
+ * Stamp the FRAME's camera onto the map. The frame's camera is authoritative
70
+ * by design: the same map document can appear twice on one page at different
71
+ * extents (the locator-inset pattern), so the document's own center/zoom are
72
+ * deliberately ignored.
73
+ *
74
+ * TWO steps, and the second is not optional. `<om-map>` reads center/zoom ONCE,
75
+ * as its initial view — its attribute observer deliberately does not watch the
76
+ * camera, so that a manifest edit never fights a user's pan. An inline
77
+ * `<om-map>` child has therefore already booted at ITS OWN camera by the time a
78
+ * frame mounts it, and writing the attributes alone leaves the map sitting
79
+ * wherever the document said (for a fresh document, [0,0] at zoom 1 — an empty
80
+ * ocean). So the attributes are written for the DOM's sake (serialization,
81
+ * agents, re-mounts) and the live camera is driven through the runtime.
82
+ */
83
+ export declare function applyCamera(map: Element, camera: FrameCamera): void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The canvas paint contract used by `renderCartograph`.
3
+ *
4
+ * Every placed element knows how to draw itself onto a 2D context at an
5
+ * arbitrary scale, so export is a walk over the page's own children rather
6
+ * than a second, drifting description of the page. Painters read their
7
+ * COMPUTED style — the browser has already resolved theme custom properties,
8
+ * attribute overrides and inherited values, so the exported pixels use exactly
9
+ * the values the screen and the printed page use.
10
+ */
11
+ export interface PaintEnv {
12
+ /** Canvas pixels per CSS pixel — dpi / 96. */
13
+ scale: number;
14
+ /** Canvas pixels per millimetre — dpi / 25.4. */
15
+ pxPerMm: number;
16
+ }
17
+ export declare function paintEnvForDpi(dpi: number): PaintEnv;
18
+ export interface CartographPainter {
19
+ /**
20
+ * Draw this element. The context is pre-translated to the element's own
21
+ * top-left corner and rotated by its `rotation`, so painters draw in their
22
+ * own local box starting at (0, 0), in CANVAS pixels.
23
+ */
24
+ paintCartograph(ctx: CanvasRenderingContext2D, env: PaintEnv): void;
25
+ /**
26
+ * Optional async pre-pass, awaited before ANY element paints. A live frame
27
+ * uses it to re-render its map at the export resolution; painting itself
28
+ * stays synchronous so the composite order is trivially deterministic.
29
+ */
30
+ prepareCartograph?(env: PaintEnv): Promise<void>;
31
+ /**
32
+ * Drop anything `prepareCartograph` produced, so a failed pre-pass paints
33
+ * nothing rather than the previous page's stale capture.
34
+ */
35
+ discardCartograph?(): void;
36
+ }
37
+ export declare function isPainter(node: unknown): node is Element & CartographPainter;
38
+ export interface BoxMm {
39
+ x: number;
40
+ y: number;
41
+ w: number;
42
+ h: number;
43
+ rotation: number;
44
+ z: number | null;
45
+ }
46
+ /** An element's authored box in millimetres — the placement source of truth. */
47
+ export declare function boxMm(el: Element): BoxMm;
48
+ /**
49
+ * Resolve a computed-style length (e.g. "12.5px") to a number, with a
50
+ * fallback for the values happy-dom and other partial DOMs leave empty.
51
+ */
52
+ export declare function styleLength(value: string | null | undefined, fallback?: number): number;
53
+ /** A CSS colour that actually paints — treats empty/transparent as "skip". */
54
+ export declare function paintableColor(value: string | null | undefined): string | null;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cross-element references (`for=`, `overview-of=`, atlas `for=`) and the
3
+ * shared "this reference goes nowhere" rendering.
4
+ *
5
+ * A dangling reference is a validation ERROR and renders a visible inline
6
+ * placeholder, never nothing: a scale bar that silently vanishes because its
7
+ * frame id has a typo is indistinguishable, on a printed page, from a scale
8
+ * bar the author chose not to include.
9
+ */
10
+ import type { OmFrameElement } from "./elements/om-frame";
11
+ /** The `<om-cartograph>` an element belongs to. */
12
+ export declare function ownerCartograph(el: Element): Element | null;
13
+ /** Resolve a frame id within the same cartograph. */
14
+ export declare function findFrame(el: Element, id: string | null | undefined): OmFrameElement | null;
15
+ /** Every frame on the page, in document order. */
16
+ export declare function allFrames(root: Element): OmFrameElement[];
17
+ /** The frame a `for=`-less furniture element implicitly binds to (only when there is exactly one). */
18
+ export declare function soleFrame(el: Element): OmFrameElement | null;
19
+ /**
20
+ * Resolve the frame this element draws for: an explicit `for=`, or the page's
21
+ * only frame when `for=` is absent.
22
+ */
23
+ export declare function referencedFrame(el: Element): OmFrameElement | null;
24
+ /** Show the inline "unlinked" note, replacing whatever the element rendered. */
25
+ export declare function showUnlinked(el: HTMLElement, message: string): void;
26
+ /**
27
+ * Drop the "unlinked" note once the element can render for real. Elements that
28
+ * rebuild their whole subtree get this for free; one that APPENDS its output
29
+ * (om-legend, which must preserve its authored config children) has to say so,
30
+ * or the page keeps a stale "waiting for its frame" line above a legend that
31
+ * is already showing rows.
32
+ */
33
+ export declare function clearUnlinked(el: Element): void;
@@ -0,0 +1,64 @@
1
+ import type { OmCartographElement } from "./elements/om-cartograph";
2
+ /**
3
+ * Conservative canvas ceilings. Browsers differ (Chrome allows ~2^28 px of
4
+ * area, iOS Safari far less), and exceeding them fails SILENTLY — a blank or
5
+ * null-blob export. Clamping to a documented budget and REPORTING the dpi
6
+ * actually delivered beats handing back an empty page.
7
+ */
8
+ export declare const MAX_CANVAS_DIMENSION = 16384;
9
+ export declare const MAX_CANVAS_AREA = 134217728;
10
+ export interface RenderCartographOptions {
11
+ /** Target resolution; defaults to the page's dpi= attribute, else 300. */
12
+ dpi?: number;
13
+ format?: "png" | "jpeg";
14
+ /** JPEG quality 0–1 (ignored for PNG). */
15
+ quality?: number;
16
+ /** Override the canvas ceilings (tests, hosts that know their environment). */
17
+ maxDimension?: number;
18
+ maxArea?: number;
19
+ /**
20
+ * Simulate a colour-vision deficiency in the exported raster
21
+ * (NikaGeospatial/onlymap-js#42). Default
22
+ * NONE — even when the page is being previewed under `cvd=`, because an
23
+ * export is a deliverable and a simulated deliverable helps nobody. Pass
24
+ * `"page"` to honour the page's own `cvd=`, or a type to force one.
25
+ */
26
+ cvd?: string | null;
27
+ }
28
+ export interface RenderCartographResult {
29
+ blob: Blob;
30
+ widthPx: number;
31
+ heightPx: number;
32
+ /** The resolution actually delivered — lower than requested when clamped. */
33
+ dpi: number;
34
+ /** Non-fatal problems worth surfacing to the user (clamping, skipped frames). */
35
+ warnings: string[];
36
+ }
37
+ export declare function renderCartograph(page?: OmCartographElement | null, options?: RenderCartographOptions): Promise<RenderCartographResult>;
38
+ export interface RenderAtlasOptions extends RenderCartographOptions {
39
+ /** Render only this page (0-based) — the host contract for `?export=&page=i`. */
40
+ page?: number;
41
+ /** Called after each page, for progress reporting on a long run. */
42
+ onPage?: (index: number, count: number) => void;
43
+ }
44
+ export interface RenderAtlasResult {
45
+ /** One ZIP of every page, or the single page's own image when `page` was given. */
46
+ blob: Blob;
47
+ filenames: string[];
48
+ pages: number;
49
+ dpi: number;
50
+ warnings: string[];
51
+ }
52
+ /**
53
+ * Render every page of an atlas.
54
+ *
55
+ * SEQUENTIALLY, and reusing the one live map: seeking moves the existing
56
+ * frames rather than cloning the page. Cloning would mount a fresh WebGL
57
+ * context per page, and browsers cap live contexts at roughly 8–16 — an atlas
58
+ * of any useful size would silently start printing blank pages. This way the
59
+ * ceiling is one map, whatever N is.
60
+ *
61
+ * The result is a single ZIP because browsers block or prompt on a loop of N
62
+ * downloads; a host wanting one page at a time passes `page`.
63
+ */
64
+ export declare function renderCartographAtlas(page?: OmCartographElement | null, options?: RenderAtlasOptions): Promise<RenderAtlasResult>;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Scale-bar maths — ground distance per page millimetre, and the "nice"
3
+ * segment lengths a printed bar is allowed to use.
4
+ *
5
+ * A scale bar whose segments read "0 · 347 · 694 m" is useless to the person
6
+ * holding a ruler over the page, so segment lengths snap to a 1-2-5 series
7
+ * (1, 2, 5, 10, 20, 50, …) in the bar's own unit system, and the bar's drawn
8
+ * width follows from that choice rather than the other way round.
9
+ */
10
+ import { type UnitSystem } from "../units";
11
+ export type ScalebarUnits = UnitSystem;
12
+ export { niceNumberBelow } from "../units";
13
+ export interface ScalebarSpec {
14
+ /** Ground metres spanned by ONE segment. */
15
+ segmentMeters: number;
16
+ /** Page millimetres spanned by one segment. */
17
+ segmentMm: number;
18
+ segments: number;
19
+ /** Total drawn width, mm (segments × segmentMm). */
20
+ totalMm: number;
21
+ /** Tick labels, one per boundary (segments + 1 of them), in `unitLabel` units. */
22
+ labels: string[];
23
+ unitLabel: string;
24
+ /** The representative fraction denominator — the "1:25 000" number. */
25
+ scaleDenominator: number;
26
+ }
27
+ export interface ScalebarOptions {
28
+ groundMetersPerMm: number;
29
+ /** The bar's authored box width in mm — the bar is drawn at or below this. */
30
+ maxWidthMm: number;
31
+ segments?: number;
32
+ units?: ScalebarUnits;
33
+ /** Ground metres per segment, overriding the automatic 1-2-5 choice. */
34
+ segmentLengthMeters?: number | null;
35
+ }
36
+ /**
37
+ * Build the bar. Null when there is no usable scale (a degenerate georef, a
38
+ * zero-width box) — furniture must render an inline "no scale" placeholder
39
+ * rather than a bar that lies.
40
+ */
41
+ export declare function buildScalebar({ groundMetersPerMm, maxWidthMm, segments, units, segmentLengthMeters }: ScalebarOptions): ScalebarSpec | null;
42
+ export type ScalebarStyle = "single" | "double" | "line" | "ticks";
43
+ export interface ScalebarDrawing {
44
+ /** Filled/hollow segment rectangles, in mm relative to the element's box. */
45
+ bars: {
46
+ x: number;
47
+ y: number;
48
+ w: number;
49
+ h: number;
50
+ filled: boolean;
51
+ }[];
52
+ /** Rules and ticks, in mm. */
53
+ strokes: {
54
+ x1: number;
55
+ y1: number;
56
+ x2: number;
57
+ y2: number;
58
+ }[];
59
+ /** Tick labels — `y` is the text BASELINE, in mm. */
60
+ labels: {
61
+ text: string;
62
+ x: number;
63
+ y: number;
64
+ anchor: "start" | "middle" | "end";
65
+ }[];
66
+ fontMm: number;
67
+ strokeMm: number;
68
+ /** Ground metres one segment spans — carried here so a host drawing the bar needs nothing else. */
69
+ segmentMeters: number;
70
+ /** Unit the labels are in ("m", "km", "mi", "nmi"). */
71
+ unitLabel: string;
72
+ /** Representative-fraction denominator at this scale. */
73
+ scaleDenominator: number;
74
+ }
75
+ /**
76
+ * Turn a computed bar into geometry, ONCE. The SVG renderer and the canvas
77
+ * exporter both consume this, so an exported PNG and the printed page cannot
78
+ * disagree about where the ticks are.
79
+ */
80
+ export declare function buildScalebarDrawing(spec: ScalebarSpec, style: ScalebarStyle, boxHeightMm: number): ScalebarDrawing;
81
+ /**
82
+ * Digit-group separator for printed scale text: U+202F NARROW NO-BREAK SPACE.
83
+ * ISO 31-0 / SI group digits with a thin space rather than a comma or point
84
+ * (which mean different things either side of the Atlantic), and the NO-BREAK
85
+ * variant is the one a printed page needs — "1:25 000" must never wrap across
86
+ * a line. Pinned by codepoint in the tests so it cannot drift to an ASCII
87
+ * space during a copy-edit.
88
+ */
89
+ export declare const SCALE_GROUP_SEPARATOR = "\u202F";
90
+ /** "1:25 000" — the representative fraction, grouped for print. */
91
+ export declare function formatScaleDenominator(denominator: number): string;