@nika-js/onlymap 0.7.6 → 0.8.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/CHANGELOG.md +10 -0
- package/README.md +3 -1
- package/dist/{LercDecode.es-sRuTJq5Y.js → LercDecode.es--DH6_OFE.js} +1 -1
- package/dist/{basemap-BCGMmc2F.js → basemap-2ZZ_9gFn.js} +528 -502
- package/dist/basemap.d.ts +15 -0
- package/dist/cartograph/api.d.ts +45 -0
- package/dist/cartograph/atlas.d.ts +50 -0
- package/dist/cartograph/core-loader.d.ts +35 -0
- package/dist/cartograph/cvd.d.ts +106 -0
- package/dist/cartograph/element-base.d.ts +26 -0
- package/dist/cartograph/elements/om-atlas.d.ts +47 -0
- package/dist/cartograph/elements/om-cartograph.d.ts +76 -0
- package/dist/cartograph/elements/om-frame.d.ts +111 -0
- package/dist/cartograph/elements/om-graticule.d.ts +24 -0
- package/dist/cartograph/elements/om-image.d.ts +12 -0
- package/dist/cartograph/elements/om-legend.d.ts +16 -0
- package/dist/cartograph/elements/om-north.d.ts +11 -0
- package/dist/cartograph/elements/om-scalebar.d.ts +13 -0
- package/dist/cartograph/elements/om-shape.d.ts +16 -0
- package/dist/cartograph/elements/om-text.d.ts +9 -0
- package/dist/cartograph/georef.d.ts +126 -0
- package/dist/cartograph/graticule.d.ts +70 -0
- package/dist/cartograph/html-data.d.ts +27 -0
- package/dist/cartograph/index.d.ts +1 -0
- package/dist/cartograph/layout.d.ts +23 -0
- package/dist/cartograph/legend.d.ts +75 -0
- package/dist/cartograph/live-frame.d.ts +69 -0
- package/dist/cartograph/paint.d.ts +54 -0
- package/dist/cartograph/refs.d.ts +33 -0
- package/dist/cartograph/render.d.ts +64 -0
- package/dist/cartograph/scalebar.d.ts +91 -0
- package/dist/cartograph/schema.d.ts +38 -0
- package/dist/cartograph/standalone.d.ts +1 -0
- package/dist/cartograph/textlayout.d.ts +45 -0
- package/dist/cartograph/tokens.d.ts +45 -0
- package/dist/cartograph/url-actions.d.ts +26 -0
- package/dist/cartograph/validate.d.ts +38 -0
- package/dist/cartograph/zip.d.ts +22 -0
- package/dist/cartograph.css +1 -0
- package/dist/cartograph.js +2768 -0
- package/dist/cartograph.standalone.js +7640 -0
- package/dist/crs-DsDQ4Q4i.js +71 -0
- package/dist/download.d.ts +6 -0
- package/dist/feature-access.d.ts +14 -0
- package/dist/field-access.d.ts +1 -10
- package/dist/{geoparquet-CK9IjEjH.js → geoparquet-DDZzee5Z.js} +1 -1
- package/dist/{index-CNRYpb0x.js → index-BGOblzWN.js} +5134 -5070
- package/dist/index-CcLC9jE5.js +4798 -0
- package/dist/{index-CIEJyv5x.js → index-CeoVzd6t.js} +1 -1
- package/dist/{index-CW3bdfHB.js → index-Cz1VsWzO.js} +2 -2
- package/dist/{index-CN_tn36D.js → index-DkxFFflv.js} +1 -1
- package/dist/{index-C1ZQGJxa.js → index-DphcuoPv.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/{lerc-Bb2JZ92v.js → lerc-D7TfTy6l.js} +2 -2
- package/dist/onlymap.standalone.js +8820 -8730
- package/dist/onlymapjs.js +12 -11
- package/dist/{raster-DpYssEe7.js → raster-D_3YKFZz.js} +3 -3
- package/dist/{raster-pipeline-BVCB-0yq.js → raster-pipeline-D1yeoJo2.js} +1 -1
- package/dist/runtime-core.d.ts +30 -0
- package/dist/snapshot.d.ts +17 -0
- package/dist/units.d.ts +9 -0
- package/dist/version.d.ts +1 -1
- package/dist/{zarr-DNb4iqbL.js → zarr-DH5Ntt5U.js} +2 -2
- package/docs/cartograph.md +396 -0
- package/llms.txt +1 -0
- package/onlymapjs.html-data.json +974 -0
- package/package.json +13 -2
- package/skills/onlymapjs/SKILL.md +2 -2
- package/skills/onlymapjs/references/syntax.md +26 -2
package/dist/onlymapjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { aC as e, aD as r, aE as t, aF as o, aG as b, aH as i, ap as l, aI as n, aJ as S, aK as c, aL as E, aM as L, aN as g, aO as T, aP as _, aQ as
|
|
1
|
+
import { aC as e, aD as r, aE as t, aF as o, aG as b, aH as i, ap as l, aI as n, aJ as S, aK as c, aL as E, aM as L, aN as g, aO as T, aP as _, aQ as d, aR as A, aS as p, aT as I, aU as m, aV as D, aW as u, aX as M, aY as y, aZ as O, a_ as f, ao as R, a$ as N, aw as P, b0 as F, b1 as h, b2 as C, W as B, b3 as v, b4 as x, b5 as G, b6 as U, b7 as W, b8 as w, b9 as Y, ba as X, bb as H, bc as k, bd as J, be as K, bf as V, bg as z, bh as Q, bi as Z, bj as $, bk as j, bl as q, bm as aa, bn as sa, bo as ea, bp as ra, bq as ta, br as oa, bs as ba, bt as ia, bu as la, bv as na, bw as Sa, bx as ca, by as Ea, bz as La, bA as ga, bB as Ta, bC as _a, bD as da, bE as Aa, bF as pa, bG as Ia, bH as ma, bI as Da, bJ as ua, bK as Ma, bL as ya, bM as Oa, bN as fa, bO as Ra, bP as Na, bQ as Pa, bR as Fa, bS as ha, bT as Ca, bU as Ba, bV as va, bW as xa, bX as Ga, bY as Ua, bZ as Wa, b_ as wa, b$ as Ya, c0 as Xa, c1 as Ha, c2 as ka, c3 as Ja, c4 as Ka, c5 as Va, c6 as za, c7 as Qa, c8 as Za, c9 as $a, ca as ja, cb as qa, cc as as, cd as ss } from "./index-BGOblzWN.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ALL_POSITION_VALUES,
|
|
4
4
|
r as AUDIT_EXEMPTIONS,
|
|
@@ -15,14 +15,14 @@ export {
|
|
|
15
15
|
g as FOLD_HYSTERESIS_PX,
|
|
16
16
|
T as FOLD_SIDES,
|
|
17
17
|
_ as FOLD_SINGLE_SIDED_RATIO,
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
d as GeoJsonLayer,
|
|
19
|
+
A as IconLayer,
|
|
20
20
|
p as LEGACY_POSITION_ALIASES,
|
|
21
21
|
I as LIGHTING_PRESET_NAMES,
|
|
22
22
|
m as Layer,
|
|
23
23
|
D as LayerExtension,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
u as MANAGED_SLOTS,
|
|
25
|
+
M as MapController,
|
|
26
26
|
y as OmMap,
|
|
27
27
|
O as ScatterplotLayer,
|
|
28
28
|
f as ScenegraphLayer,
|
|
@@ -69,14 +69,14 @@ export {
|
|
|
69
69
|
ga as lineStringLengthMeters,
|
|
70
70
|
Ta as loadIfc,
|
|
71
71
|
_a as loadRasterModule,
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
da as midpoint,
|
|
73
|
+
Aa as mountForTest,
|
|
74
74
|
pa as niceNumber,
|
|
75
75
|
Ia as normalizeData,
|
|
76
76
|
ma as parseDurationMs,
|
|
77
77
|
Da as parseTerrainAttrs,
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
ua as parseUnitSystem,
|
|
79
|
+
Ma as parseWidgetStyle,
|
|
80
80
|
ya as pathLengthMeters,
|
|
81
81
|
Oa as registerAction,
|
|
82
82
|
fa as registerBasemap,
|
|
@@ -105,6 +105,7 @@ export {
|
|
|
105
105
|
Za as snapshotDescriptorIR,
|
|
106
106
|
$a as snapshotIR,
|
|
107
107
|
ja as solarAzElDegrees,
|
|
108
|
-
qa as
|
|
109
|
-
as as
|
|
108
|
+
qa as subscribeLicense,
|
|
109
|
+
as as validateManifest,
|
|
110
|
+
ss as validateManifestString
|
|
110
111
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as se, t as ze, i as wt, a as Be, s as St, C as Pt, b as bt, F as Ct, A as Lt, d as It, R as fe, e as Gt, p as vt, m as Et, f as Ft, g as At, r as _e, h as jt, j as Rt, k as Ut, l as Mt, n as Ot } from "./raster-pipeline-
|
|
2
|
-
import { ax as Kt, ay as xt } from "./index-
|
|
1
|
+
import { c as se, t as ze, i as wt, a as Be, s as St, C as Pt, b as bt, F as Ct, A as Lt, d as It, R as fe, e as Gt, p as vt, m as Et, f as Ft, g as At, r as _e, h as jt, j as Rt, k as Ut, l as Mt, n as Ot } from "./raster-pipeline-D1yeoJo2.js";
|
|
2
|
+
import { ax as Kt, ay as xt } from "./index-BGOblzWN.js";
|
|
3
3
|
import ye from "./index-CW1n5LdO.js";
|
|
4
4
|
function Dt(e, t) {
|
|
5
5
|
const n = e.length / 3, r = new Uint8ClampedArray(n * 4), o = 0, i = n, a = n * 2;
|
|
@@ -1070,7 +1070,7 @@ j.set(f.Zstd, () => import("./zstd-jXobGRcq.js").then((e) => e.decode));
|
|
|
1070
1070
|
j.set(f.Jpeg, () => Promise.resolve(le));
|
|
1071
1071
|
j.set(f.Jpeg6, () => Promise.resolve(le));
|
|
1072
1072
|
j.set(f.Webp, () => Promise.resolve(le));
|
|
1073
|
-
j.set(f.Lerc, () => import("./lerc-
|
|
1073
|
+
j.set(f.Lerc, () => import("./lerc-D7TfTy6l.js").then((e) => e.l).then((e) => e.decode));
|
|
1074
1074
|
async function ce(e, t, n) {
|
|
1075
1075
|
const r = j.get(t);
|
|
1076
1076
|
if (!r)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { w as de } from "./mgrs-BY9bIvp4.js";
|
|
2
|
-
import { am as he, an as fe, ao as se, ap as oe, aq as me, b as pe, ar as ge, l as Z, as as ve, d as be, at as xe, au as Pe, av as ye, aw as ie } from "./index-
|
|
2
|
+
import { am as he, an as fe, ao as se, ap as oe, aq as me, b as pe, ar as ge, l as Z, as as ve, d as be, at as xe, au as Pe, av as ye, aw as ie } from "./index-BGOblzWN.js";
|
|
3
3
|
function Te(r, e, t) {
|
|
4
4
|
const { projectedCorners: n } = e, { topLeft: s, topRight: o, bottomRight: i, bottomLeft: a } = n, c = t(s[0], s[1]), u = t(o[0], o[1]), l = t(i[0], i[1]), h = t(a[0], a[1]), f = [
|
|
5
5
|
c,
|
package/dist/runtime-core.d.ts
CHANGED
|
@@ -592,6 +592,36 @@ export declare class RuntimeCore {
|
|
|
592
592
|
}>;
|
|
593
593
|
/** Force one renderer draw without capturing (issue #37 — whenSettled's completed-render guarantee; a fresh page's first capture otherwise misses content that has never drawn). */
|
|
594
594
|
forceDraw(): void;
|
|
595
|
+
/**
|
|
596
|
+
* Serializes capture-resolution overrides: one at a time per core, because
|
|
597
|
+
* the override mutates shared renderer state and two concurrent exports
|
|
598
|
+
* would each restore the other's ratio.
|
|
599
|
+
*/
|
|
600
|
+
private captureRatioQueue;
|
|
601
|
+
/**
|
|
602
|
+
* Run `body` with the renderer temporarily rasterizing at `scale` captured
|
|
603
|
+
* pixels per CSS pixel (spec: "Snapshot API" — the `snapshot({scale})`
|
|
604
|
+
* seam). Restores the display resolution afterwards, always.
|
|
605
|
+
*
|
|
606
|
+
* Three things make this correct rather than merely plausible, each learned
|
|
607
|
+
* from the machinery it touches:
|
|
608
|
+
*
|
|
609
|
+
* 1. It is a SCOPED override, not a per-capture argument, because
|
|
610
|
+
* `captureStable()` calls `snapshot()` repeatedly in its own loop —
|
|
611
|
+
* a per-call parameter could never reach those inner captures.
|
|
612
|
+
* 2. It re-awaits `whenSettled()` after changing the ratio. The basemap
|
|
613
|
+
* path captures inside a single `map.once("render")`, which fires long
|
|
614
|
+
* before a re-rasterized style has redrawn its tiles and glyphs.
|
|
615
|
+
* 3. The adapter suppresses the camera-move events MapLibre's resize()
|
|
616
|
+
* emits, so a host app never sees a phantom pan mid-export.
|
|
617
|
+
*
|
|
618
|
+
* Deliberately NOT a DOM resize of the map element: that would change the
|
|
619
|
+
* camera's viewport, re-trigger widget fold, and alter which tiles load —
|
|
620
|
+
* a different picture, not a sharper one.
|
|
621
|
+
*/
|
|
622
|
+
withCapturePixelRatio<T>(scale: number, body: () => Promise<T>, opts?: {
|
|
623
|
+
timeout?: number;
|
|
624
|
+
}): Promise<T>;
|
|
595
625
|
snapshot(): Promise<HTMLCanvasElement>;
|
|
596
626
|
/**
|
|
597
627
|
* Live basemap change (spec: "Basemap presets & switching"), both paths:
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -10,6 +10,23 @@ export interface SnapshotOptions {
|
|
|
10
10
|
quality?: number;
|
|
11
11
|
/** Output shape — default "dataURL" (what print/export pipelines embed directly); "blob" for uploads/files. */
|
|
12
12
|
as?: "dataURL" | "blob";
|
|
13
|
+
/**
|
|
14
|
+
* Capture resolution as ABSOLUTE captured-bitmap pixels per CSS pixel of the
|
|
15
|
+
* map's layout box — not a multiplier on the current device ratio. A 190mm
|
|
16
|
+
* frame printed at 300 dpi wants `scale: 300/96`, whatever screen it was
|
|
17
|
+
* composed on.
|
|
18
|
+
*
|
|
19
|
+
* Omitted (the default) the capture happens at whatever the renderer is
|
|
20
|
+
* already drawing at, byte-for-byte the behaviour every existing caller
|
|
21
|
+
* gets. Supplied, the renderer is temporarily re-rasterized at that ratio,
|
|
22
|
+
* allowed to reconverge, captured, and restored — so a print export is a
|
|
23
|
+
* genuine high-resolution render, not an upscaled screenshot.
|
|
24
|
+
*
|
|
25
|
+
* Real ceilings apply (GPU max texture size, canvas area); the renderer
|
|
26
|
+
* clamps and the resulting capture is simply smaller than asked for, which
|
|
27
|
+
* `renderCartograph` surfaces as an effective-dpi warning.
|
|
28
|
+
*/
|
|
29
|
+
scale?: number;
|
|
13
30
|
}
|
|
14
31
|
export declare function serializeSnapshot(canvas: HTMLCanvasElement, opts: SnapshotOptions): Promise<string | Blob>;
|
|
15
32
|
/**
|
package/dist/units.d.ts
CHANGED
|
@@ -55,6 +55,15 @@ export declare function densityToKgM3(raw: number, system: UnitSystem): number;
|
|
|
55
55
|
* correct for sub-unit spans (`value < 1`) where a digit-count rule breaks.
|
|
56
56
|
*/
|
|
57
57
|
export declare function niceNumber(value: number): number;
|
|
58
|
+
/**
|
|
59
|
+
* The largest 1/2/5 ×10ⁿ value ≤ `value`.
|
|
60
|
+
*
|
|
61
|
+
* Printed scale bars and coordinate grids use the stricter cartographic
|
|
62
|
+
* series; the interactive measure widget keeps {@link niceNumber}'s denser
|
|
63
|
+
* 1/2/3/5 series. Keeping both policies here makes that distinction explicit
|
|
64
|
+
* without duplicating the logarithmic snapping maths.
|
|
65
|
+
*/
|
|
66
|
+
export declare function niceNumberBelow(value: number): number;
|
|
58
67
|
/**
|
|
59
68
|
* Scale-bar helper: given the max distance (meters) that fits the bar's pixel
|
|
60
69
|
* budget, return the nice round distance to actually draw and its label, in the
|
package/dist/version.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as ur, d as lr, R as zt, e as fr, m as dr, p as St, f as hr, j as pr, k as mr } from "./raster-pipeline-
|
|
2
|
-
import { ap as gr } from "./index-
|
|
1
|
+
import { A as ur, d as lr, R as zt, e as fr, m as dr, p as St, f as hr, j as pr, k as mr } from "./raster-pipeline-D1yeoJo2.js";
|
|
2
|
+
import { ap as gr } from "./index-BGOblzWN.js";
|
|
3
3
|
import $t from "./index-CW1n5LdO.js";
|
|
4
4
|
var Et;
|
|
5
5
|
function h(e, t, n) {
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# Cartographs — print-composition pages
|
|
2
|
+
|
|
3
|
+
> **Available in 0.8.0.** This document is the
|
|
4
|
+
> normative element/attribute reference for the `@nika-js/onlymap/cartograph` entry
|
|
5
|
+
> (issue [onlymap-js#41](https://github.com/NikaGeospatial/onlymap-js/issues/41)).
|
|
6
|
+
> **Phases 0–4 are implemented**: the page, static AND live georeferenced frames, the
|
|
7
|
+
> locator inset, scale bar, north indicator, derived legend, graticule, colour-vision
|
|
8
|
+
> simulation, shapes, images, text with tokens, atlas iteration, the validator, print-DPI
|
|
9
|
+
> export and printing, and the URL triggers. Consumers:
|
|
10
|
+
> [nika-agent#68](https://github.com/NikaGeospatial/nika-agent/issues/68) (whose §2 defers
|
|
11
|
+
> to this document) and qgis2carto.
|
|
12
|
+
|
|
13
|
+
A **cartograph** is a standalone HTML file describing a printed page of maps: a page in
|
|
14
|
+
millimetres carrying placed map frames (live `<om-map>` mounts or georeferenced rasters)
|
|
15
|
+
and cartographic furniture — legend, scale bar, north indicator, graticule, text, shapes,
|
|
16
|
+
images — plus atlas iteration. It prints
|
|
17
|
+
to PDF at true page size via CSS `@page` and exports PNG/JPEG at print DPI through
|
|
18
|
+
`renderCartograph()`.
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<!doctype html>
|
|
22
|
+
<html>
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="utf-8">
|
|
25
|
+
<script type="module" src="https://unpkg.com/@nika-js/onlymap@0.8.0/dist/cartograph.standalone.js"></script>
|
|
26
|
+
<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.8.0/dist/cartograph.css">
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<om-cartograph cartograph-id="cg-lot-12" format="cartograph/2" size="A4" theme="minimal"
|
|
30
|
+
title="Lot 12 — parcel survey" attribution="© Survey Dept 2026">
|
|
31
|
+
|
|
32
|
+
<om-text id="title" kind="title" x="10" y="8" w="150" h="12">{{title}}</om-text>
|
|
33
|
+
|
|
34
|
+
<!-- A georeferenced raster. The CORNERS place it, not the pixels. -->
|
|
35
|
+
<om-frame id="main" mode="static" x="10" y="24" w="190" h="190"
|
|
36
|
+
crs="EPSG:3857" corners="[[103.80,1.32],[103.82,1.32],[103.82,1.30],[103.80,1.30]]"
|
|
37
|
+
style="left:10mm;top:24mm;width:190mm;height:190mm">
|
|
38
|
+
<img src="lot-12.png" alt="">
|
|
39
|
+
</om-frame>
|
|
40
|
+
|
|
41
|
+
<om-scalebar id="bar" for="main" x="10" y="222" w="70" h="9" kind="double" units="metric"></om-scalebar>
|
|
42
|
+
<om-north id="north" for="main" x="186" y="220" w="14" h="18" kind="rose"></om-north>
|
|
43
|
+
<om-text id="credit" kind="attribution" x="10" y="240" w="190" h="6">
|
|
44
|
+
1:{{frame.main.scale}} · {{frame.main.crs}} · {{attribution}}
|
|
45
|
+
</om-text>
|
|
46
|
+
</om-cartograph>
|
|
47
|
+
</body>
|
|
48
|
+
</html>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Ground rules
|
|
52
|
+
|
|
53
|
+
- **Millimetres everywhere.** Every placed element carries `x y w h` in mm from the page's
|
|
54
|
+
top-left, plus `rotation` (degrees clockwise), `z` (stacking override — document order is
|
|
55
|
+
z-order by default), `locked` (editor-only, inert), `hidden` (skipped in render and print).
|
|
56
|
+
CSS defines 1mm ≡ 96/25.4 px, so screen preview and print agree by construction.
|
|
57
|
+
- **Attributes are the source of truth.** The runtime re-asserts inline mm styles from the
|
|
58
|
+
placement attributes on every change. Producers should ALSO write those inline styles
|
|
59
|
+
(as in the example above) so the page positions correctly with JavaScript disabled; the
|
|
60
|
+
validator raises an **error** if the two ever disagree.
|
|
61
|
+
- **`kind`, never `style`.** Every discriminator — text role, shape type, scale-bar style,
|
|
62
|
+
north-indicator style, graticule style — is spelled `kind`. `style` is the global HTML
|
|
63
|
+
attribute the runtime writes placement into, so a discriminator living there would be
|
|
64
|
+
erased on the first render.
|
|
65
|
+
- **One package, zero cost to maps.** The cartograph is a separate build entry; the core map
|
|
66
|
+
bundle does not grow, and a cartograph whose frames are all static never loads the map
|
|
67
|
+
runtime at all (CI-enforced by `dev/assert-bundle-cdn-safe.mjs`).
|
|
68
|
+
- **Themes are CSS.** `theme=` selects a set of `--om-carto-*` custom properties (`default`,
|
|
69
|
+
`dark`, `sepia`, `blueprint`, `minimal`); a user theme is a `<style>` block overriding the
|
|
70
|
+
same properties. Furniture reads its **computed** colour, so an exported PNG matches the
|
|
71
|
+
screen without a second theme table.
|
|
72
|
+
|
|
73
|
+
## Elements
|
|
74
|
+
|
|
75
|
+
The authoritative attribute lists (with editor IntelliSense) are generated from
|
|
76
|
+
`src/cartograph/schema.ts` into `onlymapjs.html-data.json`.
|
|
77
|
+
|
|
78
|
+
| Element | Purpose | Status |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `<om-cartograph>` | Page root: `cartograph-id`, `format="cartograph/2"`, `size` (A5–A0/Letter/Legal) or `width`/`height` (mm), `orientation`, `theme`, `title`, `attribution`; print controls `bleed`, `crop-marks`, `safe-zone`, `dpi`, `flatten`, `color-profile`; `cvd`; `allow-url-actions` | implemented |
|
|
81
|
+
| `<om-frame>` | A placed map. `mode="static"`: a georeferenced `<img>` child placed by `crs` + `corners`. `mode="live"` (the default): mounts `<om-map>` from `src=` or an inline child with the frame's own `center/zoom/bearing/pitch`; `overview-of` draws another frame's footprint | implemented |
|
|
82
|
+
| `<om-text>` | Text block — `kind` (title/text/attribution), `font-size` (pt), `weight`, `align`, `font`, `color`, `bg`; body may use `{{tokens}}` | implemented |
|
|
83
|
+
| `<om-scalebar>` | Scale bar — `for`, `units` (metric/imperial/nautical), `segments`, `segment-length`, `kind` (single/double/line/ticks) | implemented |
|
|
84
|
+
| `<om-north>` | North indicator — `for`, `kind` (arrow/rose). Direction is derived from the frame's georeference | implemented |
|
|
85
|
+
| `<om-shape>` | `kind` (rect/ellipse/line/arrow), `fill`, `fill-opacity`, `stroke`, `stroke-width`, `radius` | implemented |
|
|
86
|
+
| `<om-image>` | Logo or photo — `src`, `fit` (contain/cover/fill) | implemented |
|
|
87
|
+
| `<om-legend>` / `<om-legend-row>` | Derived legend — `for`, `title`, `columns`, `derived`; rows computed from the frame's layers, never stored. Children are overrides (`layer`, `match`, `label`, `hide`, `order`, `color`, `shape`), or literal rows when `derived="false"` | implemented |
|
|
88
|
+
| `<om-graticule>` | Coordinate grid — `for`, `crs`, `interval`/`-x`/`-y`, `kind` (solid/cross/markers), `labels`, `format` (`dms`). Geographic by default; a projected `crs` draws that projection's grid | implemented |
|
|
89
|
+
| `<om-atlas>` | One page per feature — `for`, `layer`, `filter`, `sort`, `filename`, `page-name`. Frames follow with `atlas-fit="feature"` + `atlas-margin` | implemented |
|
|
90
|
+
|
|
91
|
+
## Georeferencing
|
|
92
|
+
|
|
93
|
+
Both frame modes expose one interface, so no piece of furniture needs to know which kind of
|
|
94
|
+
frame it is attached to:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface FrameGeoref {
|
|
98
|
+
crs: string;
|
|
99
|
+
corners: [LngLat, LngLat, LngLat, LngLat]; // tl, tr, br, bl — always lon/lat
|
|
100
|
+
widthMm: number; heightMm: number;
|
|
101
|
+
degenerate: boolean;
|
|
102
|
+
project(lngLat): { xMm, yMm } | null;
|
|
103
|
+
unproject(xMm, yMm): LngLat | null;
|
|
104
|
+
groundMetersPerMm(): number;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**The homography is fitted in the frame's CRS plane, never in raw lon/lat.** A map image is
|
|
109
|
+
a projective transform of its own projection plane; it is *not* a projective transform of
|
|
110
|
+
longitude/latitude, because Mercator's `y = R·ln(tan(π/4+φ/2))` is nonlinear in latitude. A
|
|
111
|
+
fit in degrees reproduces the four corners exactly and misplaces everything between them —
|
|
112
|
+
on a frame spanning 40° of latitude, the centre lands ~11 mm out on an A4 page. So:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
project(lngLat) = H · crsForward(lngLat)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`crsForward` is closed-form for EPSG:3857 (spherical Mercator) and EPSG:4326 (plate carrée,
|
|
119
|
+
where the forward *is* identity — which is why a naive lon/lat fit happens to be correct for
|
|
120
|
+
equirectangular rasters and wrong for everything else). Any other projected CRS resolves
|
|
121
|
+
through the core's lazy proj4; `crs-def="+proj=…"` covers CRSs the library does not bundle.
|
|
122
|
+
|
|
123
|
+
`corners` are authored in lon/lat in tl, tr, br, bl order, so a producer never needs proj4
|
|
124
|
+
to place a frame. Longitudes are unwrapped around the first corner, so an
|
|
125
|
+
antimeridian-crossing frame fits a continuous plane. A collinear or coincident quad is
|
|
126
|
+
`degenerate`: furniture refuses to draw rather than drawing something wrong, and the
|
|
127
|
+
validator raises an error.
|
|
128
|
+
|
|
129
|
+
## Live frames
|
|
130
|
+
|
|
131
|
+
A live frame mounts the real map runtime with the FRAME's own camera:
|
|
132
|
+
|
|
133
|
+
```html
|
|
134
|
+
<om-frame id="main" x="10" y="24" w="150" h="150"
|
|
135
|
+
center="[103.81, 1.31]" zoom="14" bearing="0" pitch="0"
|
|
136
|
+
src="../maps/map-abc.html"></om-frame>
|
|
137
|
+
|
|
138
|
+
<!-- A second frame on the same map, wider, drawing the first one's footprint. -->
|
|
139
|
+
<om-frame id="locator" x="165" y="24" w="35" h="28"
|
|
140
|
+
center="[103.81, 1.31]" zoom="9" src="../maps/map-abc.html"
|
|
141
|
+
overview-of="main" overview-stroke="#e63946"></om-frame>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **The frame's camera wins.** The referenced document's own `center`/`zoom` are ignored,
|
|
145
|
+
which is what lets the same map appear twice on one page at different extents.
|
|
146
|
+
- **Corners come from the camera**, through the same `WebMercatorViewport` the renderer
|
|
147
|
+
uses, so the furniture agrees with the pixels. A view pitched past roughly 60° cannot be
|
|
148
|
+
georeferenced as a quad — its top edge reaches the horizon — and the frame reports itself
|
|
149
|
+
`degenerate` rather than inventing coordinates.
|
|
150
|
+
- **`src=` documents are adopted, not injected.** The fetched subtree is rebuilt element by
|
|
151
|
+
element: `<script>` blocks are dropped except the inert `application/json` ones that carry
|
|
152
|
+
inline layer data, and `on*` handler attributes never survive. An inline `<om-map>` child
|
|
153
|
+
is *not* sanitized — the author already owns that document. Cross-origin `src=` needs CORS.
|
|
154
|
+
- **The frame's keyline is an inset shadow, not a border**, so `w`/`h` is exactly the mapped
|
|
155
|
+
extent. (A border would shrink the content box and put a silent ~0.3% error into
|
|
156
|
+
everything derived from the georeference.)
|
|
157
|
+
- Frames expose `ready` (settling on success *or* failure) and fire `om-frame-ready` /
|
|
158
|
+
`om-frame-error`; a failed frame shows a visible inline placeholder rather than an empty
|
|
159
|
+
box, which on a printed page is indistinguishable from a design choice.
|
|
160
|
+
|
|
161
|
+
### Export and print resolution
|
|
162
|
+
|
|
163
|
+
`renderCartograph()` and `print()` capture each live frame through the core's
|
|
164
|
+
`snapshot({ scale })` — a genuine re-render at the target resolution, not an upscaled
|
|
165
|
+
screenshot. `scale` is absolute captured pixels per CSS pixel (300 dpi is `300/96`), so an
|
|
166
|
+
export is identical whatever screen composed it. The renderer is restored to display
|
|
167
|
+
resolution before the call resolves.
|
|
168
|
+
|
|
169
|
+
Printing goes through `cartographEl.print({ dpi })` (or `?print=1`), which swaps each live
|
|
170
|
+
canvas for a print-resolution `<img>` first and restores it on `afterprint`. This is
|
|
171
|
+
necessary, not decorative: a WebGL canvas prints blank or at screen resolution, because its
|
|
172
|
+
drawing buffer is not preserved and the print rasterizer never re-renders it. An unassisted
|
|
173
|
+
Ctrl+P therefore cannot produce a high-DPI page — the print dialog opens synchronously and
|
|
174
|
+
captures are asynchronous.
|
|
175
|
+
|
|
176
|
+
## The derived legend
|
|
177
|
+
|
|
178
|
+
`<om-legend for="main">` computes its rows from the frame's map at render time and never
|
|
179
|
+
stores them, so the legend cannot drift out of agreement with the map it describes.
|
|
180
|
+
|
|
181
|
+
Rows come from the mounted map's own IR (`getLayers()` → `meta.legend`, `meta.label`,
|
|
182
|
+
`meta.color`) — the same values the core's legend widget reads, so both legends describing
|
|
183
|
+
one map say the same thing. That is also why rows are NOT re-derived from the authored
|
|
184
|
+
colour attributes: `classify=` computes its class breaks from the *data*, and rasters
|
|
185
|
+
resolve their domain at load, so a static re-read would silently disagree for exactly the
|
|
186
|
+
layers whose symbology was computed rather than authored.
|
|
187
|
+
|
|
188
|
+
Swatch geometry carries meaning and follows the layer type: an area chip for polygons, a
|
|
189
|
+
bar for lines, a dot for points, a gradient for rasters. `<om-legend-row>` children are
|
|
190
|
+
overrides:
|
|
191
|
+
|
|
192
|
+
```html
|
|
193
|
+
<om-legend for="main" title="Legend" x="266" y="70" w="50" h="80">
|
|
194
|
+
<om-legend-row layer="parcels" label="Zoning"></om-legend-row>
|
|
195
|
+
<om-legend-row layer="parcels" match="residential" label="Housing"></om-legend-row>
|
|
196
|
+
<om-legend-row layer="basemap-labels" hide></om-legend-row>
|
|
197
|
+
</om-legend>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`match=` targets one category or class entry, keyed on its **original** label — so
|
|
201
|
+
relabelling an entry does not break the reference to it. Categories beyond 12 collapse into
|
|
202
|
+
a "+N more" row. A static frame has no layers to derive from: there the producer writes
|
|
203
|
+
literal rows and sets `derived="false"` (the validator warns if a derived legend points at
|
|
204
|
+
a static frame).
|
|
205
|
+
|
|
206
|
+
## Graticules
|
|
207
|
+
|
|
208
|
+
`<om-graticule for="main">` draws a coordinate grid. With no `crs` it draws meridians and
|
|
209
|
+
parallels at whole degrees; give it a projected `crs` and it draws that projection's grid —
|
|
210
|
+
a UTM sheet's kilometre squares — resolving proj4 lazily, so a page that only wants degrees
|
|
211
|
+
never loads it. `interval` is in the grid's own units (degrees or metres), and an omitted
|
|
212
|
+
interval is chosen automatically on a 1-2-5 series.
|
|
213
|
+
|
|
214
|
+
Lines are **densified**, not drawn corner to corner: a straight line in one coordinate
|
|
215
|
+
system is a curve in another, and a two-point meridian would visibly bow away from the truth
|
|
216
|
+
on a wide or rotated frame. `kind` selects solid lines, crosses at the intersections, or
|
|
217
|
+
edge markers; `labels` places them outside (the default), inside, or not at all; and
|
|
218
|
+
`format="dms"` prints degrees-minutes-seconds.
|
|
219
|
+
|
|
220
|
+
## Colour-vision simulation
|
|
221
|
+
|
|
222
|
+
`cvd="deuteranopia"` (or `?cvd=`, boot-only) renders the whole page as a reader with that
|
|
223
|
+
condition sees it — a review aid for the moment before printing, when the palette can still
|
|
224
|
+
change.
|
|
225
|
+
|
|
226
|
+
`lintLegendColours(entries)` answers the question that matters: would any two legend entries
|
|
227
|
+
become indistinguishable? It simulates each of the four conditions and reports pairs whose
|
|
228
|
+
CIEDE2000 distance falls below 10, naming the worst condition. Pairs already too close for
|
|
229
|
+
normal vision are skipped — that is a different palette bug, and reporting it here would
|
|
230
|
+
send the author looking in the wrong place. The validator runs this over literal legend rows
|
|
231
|
+
automatically.
|
|
232
|
+
|
|
233
|
+
The matrices are Viénot, Brettel & Mollon (1999) applied in **linear** RGB. That combination
|
|
234
|
+
is load-bearing: the widely-copied matrices that operate on sRGB directly are invertible, so
|
|
235
|
+
no two colours ever map to the same output and a lint built on them can never report a
|
|
236
|
+
collapse at all.
|
|
237
|
+
|
|
238
|
+
## Atlas — one page per feature
|
|
239
|
+
|
|
240
|
+
The labour multiplier the format exists for: one sheet per parcel, per corridor segment,
|
|
241
|
+
per site, from a single authored page.
|
|
242
|
+
|
|
243
|
+
```html
|
|
244
|
+
<om-frame id="main" x="8" y="20" w="132" h="120"
|
|
245
|
+
center="[103.81, 1.31]" zoom="13"
|
|
246
|
+
atlas-fit="feature" atlas-margin="0.25"> … </om-frame>
|
|
247
|
+
|
|
248
|
+
<om-atlas for="main" layer="parcels" sort="lot" filter="$area > 500"
|
|
249
|
+
filename="lot-{{atlas.lot}}-{{atlas.name}}" page-name="{{atlas.name}}"></om-atlas>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Seeking a page refits every frame with `atlas-fit="feature"` to that feature's bounds (padded
|
|
253
|
+
by `atlas-margin`), re-resolves `{{atlas.<field>}}` tokens, re-derives every scale bar — the
|
|
254
|
+
scale genuinely changes per page, so a bar sized for one sheet would lie on the next — and
|
|
255
|
+
fires `om-atlas-page`. The page element exposes `cartographEl.atlas` with `count`, `index`,
|
|
256
|
+
`current`, `seek(i)`, `next()`, `filenames()` and `pageName(i)`.
|
|
257
|
+
|
|
258
|
+
Three rules make an unattended N-page export trustworthy:
|
|
259
|
+
|
|
260
|
+
- **The map's own filtering applies.** A layer's declarative `filter`/`categoryFilter` hide
|
|
261
|
+
features from the map, so paging over the raw rows would print sheets for features the map
|
|
262
|
+
is not drawing. `filter=` on the atlas is an additional predicate in the **core expression
|
|
263
|
+
language** — the same syntax `om-layer` filters use.
|
|
264
|
+
- **Pages are snapshotted when iteration starts**, so a streaming or polling layer cannot
|
|
265
|
+
change the page count halfway through an export.
|
|
266
|
+
- **A layer with no local rows cannot drive an atlas.** Tiled and asset-backed layers keep
|
|
267
|
+
their features on a server; that is a validation error, not an empty run.
|
|
268
|
+
|
|
269
|
+
`sort="lot"` / `"-lot"` (comma-separated for several keys) orders the pages, with features
|
|
270
|
+
missing the key last in **both** directions. `filename=` is a token pattern, sanitized for
|
|
271
|
+
the filesystem, and duplicates are suffixed so two features with the same name cannot
|
|
272
|
+
silently overwrite each other.
|
|
273
|
+
|
|
274
|
+
### Exporting an atlas
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const { blob, filenames, pages } = await renderCartographAtlas(page, { dpi: 300 });
|
|
278
|
+
// blob is ONE zip; pass { page: 2 } for a single sheet (the ?page= host contract).
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Pages render **sequentially through the one live map** — seeking moves the existing frames
|
|
282
|
+
rather than cloning the page. Cloning would mount a WebGL context per page, and browsers cap
|
|
283
|
+
live contexts at roughly 8–16, so an atlas of any real size would quietly start emitting
|
|
284
|
+
blank sheets. The result is a single ZIP because browsers block or prompt on a loop of N
|
|
285
|
+
downloads; entries are stored uncompressed, since PNGs are already compressed and a
|
|
286
|
+
compression library has no business in a bundle this size.
|
|
287
|
+
|
|
288
|
+
## Programmatic surface
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import {
|
|
292
|
+
renderCartograph, renderCartographAtlas,
|
|
293
|
+
validateCartograph, validateCartographString,
|
|
294
|
+
createFrameGeoref, buildScalebar, lintLegendColours, applyUrlActions,
|
|
295
|
+
} from "@nika-js/onlymap/cartograph";
|
|
296
|
+
|
|
297
|
+
// Export at print resolution. Static-only pages need no map runtime.
|
|
298
|
+
const { blob, widthPx, heightPx, dpi, warnings } = await renderCartograph(page, { dpi: 300 });
|
|
299
|
+
|
|
300
|
+
// Validate — the core's ValidationResult shape, so a host folds cartograph
|
|
301
|
+
// diagnostics into agent tool results with the code it already has for maps.
|
|
302
|
+
const { valid, errors, warnings: warns } = validateCartographString(html);
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
`renderCartograph` clamps to the browser's canvas budget rather than failing silently, and
|
|
306
|
+
reports the resolution it actually delivered in `dpi` plus a message in `warnings`. Rendering
|
|
307
|
+
the same static page twice produces byte-identical output on the same browser build
|
|
308
|
+
(cross-browser identity is not claimed — canvas PNG encoding is unspecified).
|
|
309
|
+
|
|
310
|
+
**Validation** is synchronous and headless (it needs a DOM implementation for `DOMParser`
|
|
311
|
+
in node — happy-dom or jsdom, exactly like the core's `validateManifestString`). Inline
|
|
312
|
+
`<om-map>` manifests are deliberately **not** deep-validated there, because delegating would
|
|
313
|
+
load the whole map runtime just to check a document. Hosts that already have the core loaded
|
|
314
|
+
use `await validateCartographDeep(root)`, which adds the inline manifests' own entries
|
|
315
|
+
prefixed with their frame (`om-frame#main > om-layer#roads: …`).
|
|
316
|
+
|
|
317
|
+
## Tokens
|
|
318
|
+
|
|
319
|
+
Resolved at render time, in order: atlas feature → frame → page.
|
|
320
|
+
|
|
321
|
+
| Token | Resolves to |
|
|
322
|
+
|---|---|
|
|
323
|
+
| `{{title}}` `{{attribution}}` `{{notes}}` | the matching `<om-cartograph>` attribute |
|
|
324
|
+
| `{{date}}` | today, ISO (`2026-09-08`) |
|
|
325
|
+
| `{{frame.<id>.scale}}` | that frame's representative-fraction denominator, e.g. `25 000` |
|
|
326
|
+
| `{{frame.<id>.crs}}` | that frame's CRS |
|
|
327
|
+
| `{{scale}}` `{{crs}}` | the sole frame's values — ambiguous (and warned) when the page has more than one frame |
|
|
328
|
+
| `{{atlas.<field>}}` | the current atlas feature's field, plus `{{atlas.index}}` and `{{atlas.count}}` |
|
|
329
|
+
|
|
330
|
+
Substitution is **text-node only**: a token value containing markup lands as text, never as
|
|
331
|
+
live DOM. Unknown tokens render literally and are reported by the validator.
|
|
332
|
+
|
|
333
|
+
## Host triggers (URL actions)
|
|
334
|
+
|
|
335
|
+
For hosts with no embedded browser (the QGIS plugin), the query string is the automation
|
|
336
|
+
surface. They are **opt-in**: a cartograph acts on them only when its root carries
|
|
337
|
+
`allow-url-actions`, so hosting a cartograph file does not expose drive-by print or download.
|
|
338
|
+
|
|
339
|
+
| Parameter | Effect |
|
|
340
|
+
|---|---|
|
|
341
|
+
| `?export=png&dpi=N` | waits for frames + fonts, renders, downloads. The fully automatable path. `format` may also be `jpeg`. |
|
|
342
|
+
| `?export=png&page=N` | one sheet of an atlas, **numbered from 1** like `{{atlas.index}}` and the page filenames. Errors on a page the atlas does not have, or on a cartograph with no `<om-atlas>` — a plugin driving a run sheet by sheet must not receive sheet 1 five times. |
|
|
343
|
+
| `?print=1` | waits, then opens the print dialog. Human-in-the-loop by nature — no host can drive a print dialog to a saved PDF unattended. |
|
|
344
|
+
| `?cvd=<type>` | boot-only colour-vision simulation; never persisted by an editor. |
|
|
345
|
+
|
|
346
|
+
All of them are ignored when the embedding page sets `data-om-host="editor"` on `<html>`.
|
|
347
|
+
|
|
348
|
+
## Printing
|
|
349
|
+
|
|
350
|
+
`<om-cartograph>` injects `@page { size: <w>mm <h>mm; margin: 0 }` from its own page box
|
|
351
|
+
(plus `bleed`, when set). **Chromium honors `@page size`, so print-to-PDF is true physical
|
|
352
|
+
size.** Firefox and Safari do not support the `size` descriptor and print to the selected
|
|
353
|
+
paper with scaling — a documented limitation, not a bug to work around. Furniture is SVG and
|
|
354
|
+
text in the DOM, so PDF text stays selectable text.
|
|
355
|
+
|
|
356
|
+
## Print-shop attributes
|
|
357
|
+
|
|
358
|
+
| Attribute | Effect |
|
|
359
|
+
|---|---|
|
|
360
|
+
| `bleed="3"` | Grows the printed sheet 3mm on every side. The page box stays the TRIM size, so a placed element's `x`/`y` are still measured from the trim corner; content is simply allowed to spill into the bleed, which is the point of it. |
|
|
361
|
+
| `crop-marks` | Trim marks at the four corners, drawn **in the bleed** — so it needs `bleed` to have anywhere to go (the validator says so). |
|
|
362
|
+
| `safe-zone="8"` | An on-screen guide showing where content risks being trimmed. **Never printed and never exported** — a guide on the deliverable is worse than no guide. |
|
|
363
|
+
| `flatten` | Print one raster instead of a page of text, SVG and images. Loses selectable text, so it is opt-in — but some print RIPs mishandle SVG or CSS filters, and a flattened sheet that prints correctly beats a vector one that does not. |
|
|
364
|
+
| `color-profile` | Informational tag only. The runtime never converts colour. |
|
|
365
|
+
|
|
366
|
+
## No-JS fallback
|
|
367
|
+
|
|
368
|
+
A static-only cartograph reads correctly with JavaScript disabled — iOS QuickLook, mail and
|
|
369
|
+
chat previews, file managers. `om-cartograph:not(:defined)` renders the page-shaped block;
|
|
370
|
+
static frames' `<img>` children, text and shapes show at the producer-written inline mm
|
|
371
|
+
positions; live-frame internals stay hidden. This mirrors `src/fallback.css`, including the
|
|
372
|
+
`@media (scripting:)` branches that stop a banner flashing during a slow load.
|
|
373
|
+
|
|
374
|
+
## Licensing and telemetry
|
|
375
|
+
|
|
376
|
+
On the free plan every cartograph carries a small foot credit — "Built with
|
|
377
|
+
OnlyMap by NIKA · free for non-commercial use" — injected at the page's
|
|
378
|
+
bottom-left. It is part of the free license's attribution condition, so it is
|
|
379
|
+
included in print and PNG export. Two ways it lifts:
|
|
380
|
+
|
|
381
|
+
- **Author your own credit.** Any `<om-text>` whose text mentions OnlyMap
|
|
382
|
+
(e.g. `rendered with @nika-js/onlymap/cartograph` in an attribution line)
|
|
383
|
+
satisfies the condition and suppresses the injected one — style it however
|
|
384
|
+
the sheet needs.
|
|
385
|
+
- **A paid plan.** When a live frame's map runtime verifies a paid key, the
|
|
386
|
+
credit is removed (a static-only page loads no runtime, so the credit
|
|
387
|
+
stays; the map inside a live frame is what carries `license-key`).
|
|
388
|
+
|
|
389
|
+
Telemetry: a live frame's map beacon includes `cartograph` in its feature
|
|
390
|
+
census — same endpoint, nothing new sent. Static-only pages load no runtime
|
|
391
|
+
and send nothing.
|
|
392
|
+
|
|
393
|
+
## Non-goals
|
|
394
|
+
|
|
395
|
+
SVG export; multi-page non-atlas documents (`om-page` is reserved-inert); projections other
|
|
396
|
+
than Web Mercator on **live** frames; automatic label placement.
|
package/llms.txt
CHANGED
|
@@ -43,6 +43,7 @@ Programmatic/native bridge rule: `MapController.setLayers()` accepts normal func
|
|
|
43
43
|
- `<om-behavior on="click|hover|drag|load|data-loaded" layer="..." action="...">` — declarative interaction. Built-in actions: `show-overlay`, `hide-overlay`, `show-tooltip`, `hide-tooltip`, `toggle-layer`, `set-pickable` (`{layer, pickable: true|false|"3d"}` — runtime picking/popup toggle, story-capturable; the per-layer popup on/off switch a viewer-facing export needs), `filter-layer`, `highlight-feature`, `zoom-to-feature`, `set-basemap`, `undo`, `redo`; scene/tool actions `set-lighting`, `set-terrain`, `set-clip-box` (`{min,max,invert?,highlight?}` / `{clear:true}`), `clip-box-edit` (`{editing}`), `export-region-3d` (`{target?, format?:"glb"|"b3dm"}` — what the draw widget's `export-3d` button emits), the draw actions `draw-mode` (`{target?:"sketch", mode:"point"|"line"|"polygon"|null}` — null exits drawing), `draw-commit`, `draw-cancel`, `draw-delete` (removes the last shape), `draw-clear`, `draw-config` (`{target?, autosave?, fillColor?, lineColor?}`) and `draw-save` (`{target?, format?:"download"|"file-system"|"both"}`) — every one of these drives the same store a `data="draw:<target>"` layer reads, so a custom toolbar can replace `<om-widget type="draw">` entirely; and the measure actions `measure-mode` (`{mode:"distance"|"area"|"volume"|null}`), `measure-units`, `measure-clear`, `measure-config` (`{profile?, baseSurface?, density?, swell?, shrink?, deadband?}`), `measure-flat-target-plane` (`{flat}`). One payload contract everywhere: `{ layer, target, feature, featureId, coordinate }`.
|
|
44
44
|
- Undo/redo is built in: user-facing manifest changes (layer toggles, filter changes, basemap switches, element add/remove, drawn sketches) are recorded automatically — the manifest is the state. `<om-widget type="undo-redo">` renders the buttons; Cmd/Ctrl-Z, Shift-Cmd/Ctrl-Z, and Ctrl-Y work on any map (text inputs keep their native undo). Camera moves, hover effects, and story playback are deliberately NOT undo steps. Widget scripts: `ctx.history.canUndo/canRedo` with watch token `history`; `ctx.emit("undo")`/`ctx.emit("redo")`.
|
|
45
45
|
- `<om-fallback>` — static no-JS fallback, direct child of `<om-map>` (one per map, no attributes, plain HTML content — links allowed). Shown ONLY where scripts never run (chat-app/email file previews — iOS QuickLook renders HTML attachments with JS off — file managers, sandboxed webviews); hidden automatically once the map boots. GOOD PRACTICE: include one on every complete page, especially pages that may be shared as a file. To PRODUCE a shareable single file, `npx @nika-js/onlymap export map.html` embeds every relative data/src/scenegraph URL as a data: URL (format detection preserved — the original extension rides a #name.ext fragment), pins relative library refs to the CDN, adds a generic om-fallback when missing, and warns when embedded data is heavy (>1 MB per file / >5 MB total) or when a source cannot be embedded (shapefile sidecars, COG Range streams, Zarr directories, {z}/{x}/{y} templates — those need hosting) ("This interactive map requires JavaScript — open this file in a web browser", plus a hosted-version link when one exists). Without one, the stylesheet shows a generic text-only banner. The gate is pure CSS (`om-map:not(:defined)` in onlymapjs.css) refined by the `scripting` media feature on 2023+ engines: with scripting DISABLED the fallback shows instantly; with scripting ENABLED it never flashes during a slow load and only surfaces after a ~4s grace with load-failure wording (blocked/unreachable bundle). The CSS must load without JS — a real `<link rel="stylesheet">` or inlined `<style>` on no-build pages; a bundler-emitted stylesheet is fine in npm projects.
|
|
46
|
+
- PRINT LAYOUTS (`@nika-js/onlymap/cartograph`, separate lazy entry — the core bundle does not grow): `<om-cartograph size="A3" orientation="landscape">` is a standalone page measured in MILLIMETRES carrying `<om-frame>`s (live `<om-map>` with the frame's own camera, or `mode="static"` georeferenced rasters placed by `crs`+`corners`) plus cartographic furniture: `<om-legend for="...">` (rows DERIVED from the live map's own symbology incl. classified breaks; author literal `<om-legend-row>`s with `derived="false"`), `<om-scalebar>` (metric|imperial|nautical), `<om-north>`, `<om-graticule>` (4326/3857/UTM), `<om-shape>`, `<om-image>`, `<om-text>` with `{{title}}`/`{{date}}`/`{{frame.<id>.scale}}`/`{{atlas.<field>}}` tokens. `overview-of="<frame-id>"` draws a locator inset's footprint. `<om-atlas for="<frame-id>" layer="<layer-id>">` renders one page per feature (sequentially through ONE live map — WebGL context limits make cloned pages fail at scale) and exports a ZIP. Print via `page.print()` or `?print=1` (true page size via @page; live frames swap to print-DPI captures first), PNG via `renderCartograph(page, {dpi})` or `?export=png&dpi=300`. `bleed`/`crop-marks`/`safe-zone` (screen-only guide)/`flatten` are press-shop attributes; `cvd="deuteranopia|protanopia|tritanopia|achromatopsia"` simulates colour-vision deficiencies page-wide and `lintLegendColours()` flags collapsing palettes. Free plan: a small foot credit is injected — suppressed by any authored `om-text` crediting OnlyMap, or by a paid key on a live frame's map. Deep reference: docs/cartograph.md.
|
|
46
47
|
- Animation: `transition="get-fill-color 800ms, get-radius 400ms"` on a layer GPU-animates prop changes (also smooths streaming updates via `get-position`). Camera: the `fly-to` action takes `center`/`zoom`/`pitch`/`bearing`/`duration` (e.g. `duration="2s"`) — use it in behaviors or `data-emit` buttons; `zoom-to-feature` also accepts `duration`.
|
|
47
48
|
- Pull-model frame rendering (external video frameworks such as Remotion): mark the map `data-om-recording` (the determinism switch — instant camera, no transitions/gesture interrupts, byte-stable `snapshot()`), then per frame `story.seek(t, {interpolateCamera: true})` + `await mapEl.whenSettled()` + capture; the same frame is byte-identical across processes and orderings. Story effect verbs (fade/pulse/trace/populate) evaluate as pure functions of story time under the switch — a seek landing mid-trace renders the half-drawn outline; only effects dispatched OUTSIDE a story (behaviors, ctx.emit) snap to their end state. `onlymapjs record` remains the built-in push-model path; `@nika-js/onlymap-remotion` packages the pull-model path for Remotion.
|
|
48
49
|
- Travel-map animation: a `trace` story step takes `follow` (camera rides the drawing tip along the path) and `easing="linear|ease-in|ease-out|ease-in-out"`; combine with a TripsLayer route + pins toggled by later steps, and export with `npx onlymapjs record`.
|