@nika-js/onlymap 0.6.7 → 0.6.13
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 +36 -0
- package/README.md +17 -3
- package/dist/{LercDecode.es-BzboSQ2U.js → LercDecode.es-DB1mYTrm.js} +1 -1
- package/dist/attribute-resolution.d.ts +2 -0
- package/dist/{basemap-pWXbFGjp.js → basemap-CDp9EsvL.js} +1 -1
- package/dist/classify.d.ts +62 -0
- package/dist/draw-controller.d.ts +4 -1
- package/dist/elements/om-map.d.ts +21 -1
- package/dist/{geoparquet-BPl1T2Or.js → geoparquet-CI3o8X5-.js} +1 -1
- package/dist/html-data.d.ts +1 -1
- package/dist/{index-CqsQz9Bp.js → index-BJh0t_nt.js} +2 -2
- package/dist/{index-iVMlGZS9.js → index-Bue-53Xo.js} +1 -1
- package/dist/{index-CAuq7wRs.js → index-CnKC1yuV.js} +13565 -13225
- package/dist/{index-Bgx3CJru.js → index-DMu0qL9C.js} +1 -1
- package/dist/{index-B9a6f006.js → index-Dio4lUCZ.js} +1 -1
- package/dist/layers/marker-icons.d.ts +12 -1
- package/dist/layers/route-layer.d.ts +32 -0
- package/dist/layers/tracking-layer.d.ts +3 -0
- package/dist/{lerc-BgzbFAc7.js → lerc-B1LFm9ra.js} +2 -2
- package/dist/onlymap.standalone.js +31212 -30872
- package/dist/onlymapjs.js +1 -1
- package/dist/programmatic.d.ts +19 -0
- package/dist/{raster-DtyMY54Z.js → raster-DQ1dS7mZ.js} +2 -2
- package/dist/{raster-pipeline-BmHRCUEb.js → raster-pipeline-mXzwFf0A.js} +1 -1
- package/dist/runtime-core.d.ts +22 -1
- package/dist/testing.d.ts +4 -1
- package/dist/version.d.ts +1 -1
- package/dist/{zarr-CTpFZo_m.js → zarr-BSoVUvcs.js} +2 -2
- package/docs/routing.md +23 -4
- package/docs/testing.md +1 -1
- package/llms.txt +7 -5
- package/onlymapjs.html-data.json +48 -1
- package/package.json +3 -2
- package/skills/onlymapjs/SKILL.md +6 -5
- package/skills/onlymapjs/references/syntax.md +7 -5
package/dist/onlymapjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { aA as e, aB as r, aC as t, aD as o, aE as b, aF as i, ap as l, aG as n, aH as S, aI as g, aJ as c, aK as E, aL as L, aM as T, aN as A, aO as d, aP as p, aQ as m, aR as _, aS as I, aT as u, aU as M, aV as y, aW as D, ao as O, aX as R, aw as f, aY as N, aZ as P, a_ as h, W as F, a$ as B, b0 as C, b1 as v, b2 as G, b3 as U, b4 as W, b5 as w, b6 as x, b7 as Y, b8 as H, b9 as X, ba as k, bb as J, bc as K, bd as V, be as z, bf as Q, bg as Z, bh as $, bi as j, bj as q, bk as aa, bl as sa, bm as ea, bn as ra, bo as ta, bp as oa, bq as ba, br as ia, bs as la, bt as na, bu as Sa, bv as ga, bw as ca, bx as Ea, by as La, bz as Ta, bA as Aa, bB as da, bC as pa, bD as ma, bE as _a, bF as Ia, bG as ua, bH as Ma, bI as ya, bJ as Da, bK as Oa, bL as Ra, bM as fa, bN as Na, bO as Pa, bP as ha, bQ as Fa, bR as Ba, bS as Ca, bT as va, bU as Ga, bV as Ua, bW as Wa, bX as wa, bY as xa, bZ as Ya, b_ as Ha, b$ as Xa, c0 as ka, c1 as Ja, c2 as Ka, c3 as Va, c4 as za, c5 as Qa, c6 as Za } from "./index-
|
|
1
|
+
import { aA as e, aB as r, aC as t, aD as o, aE as b, aF as i, ap as l, aG as n, aH as S, aI as g, aJ as c, aK as E, aL as L, aM as T, aN as A, aO as d, aP as p, aQ as m, aR as _, aS as I, aT as u, aU as M, aV as y, aW as D, ao as O, aX as R, aw as f, aY as N, aZ as P, a_ as h, W as F, a$ as B, b0 as C, b1 as v, b2 as G, b3 as U, b4 as W, b5 as w, b6 as x, b7 as Y, b8 as H, b9 as X, ba as k, bb as J, bc as K, bd as V, be as z, bf as Q, bg as Z, bh as $, bi as j, bj as q, bk as aa, bl as sa, bm as ea, bn as ra, bo as ta, bp as oa, bq as ba, br as ia, bs as la, bt as na, bu as Sa, bv as ga, bw as ca, bx as Ea, by as La, bz as Ta, bA as Aa, bB as da, bC as pa, bD as ma, bE as _a, bF as Ia, bG as ua, bH as Ma, bI as ya, bJ as Da, bK as Oa, bL as Ra, bM as fa, bN as Na, bO as Pa, bP as ha, bQ as Fa, bR as Ba, bS as Ca, bT as va, bU as Ga, bV as Ua, bW as Wa, bX as wa, bY as xa, bZ as Ya, b_ as Ha, b$ as Xa, c0 as ka, c1 as Ja, c2 as Ka, c3 as Va, c4 as za, c5 as Qa, c6 as Za } from "./index-CnKC1yuV.js";
|
|
2
2
|
export {
|
|
3
3
|
e as ALL_POSITION_VALUES,
|
|
4
4
|
r as AUDIT_EXEMPTIONS,
|
package/dist/programmatic.d.ts
CHANGED
|
@@ -61,6 +61,11 @@ export interface LayerDescriptor {
|
|
|
61
61
|
filterCategories?: unknown[];
|
|
62
62
|
/** Multi-dimension categorical GPU filter (up to 4) — mirrors `filter-category-fields`. Wins over `filterCategoryField`/`filterCategories` if both are set. */
|
|
63
63
|
filterCategoryFields?: LayerCategoryFilterFieldSpec[];
|
|
64
|
+
/** Classified symbology (issue #12) — mirrors `classify-by`/`classify-scale`/`classify-classes`/`classify-ramp`; computes class breaks from the data and installs a fill-color accessor + legend. An explicit fill-color prop wins. */
|
|
65
|
+
classifyBy?: string;
|
|
66
|
+
classifyScale?: "quantile" | "equal-interval" | "jenks";
|
|
67
|
+
classifyClasses?: number;
|
|
68
|
+
classifyRamp?: string;
|
|
64
69
|
/** Dashed lines on a path layer (spec: "PathStyleExtension") — `[dashLength, gapLength]` in line-width units; mirrors the `dash` attribute. */
|
|
65
70
|
dash?: [number, number];
|
|
66
71
|
/** Stretch each segment's dashes to start and end on a dash (deck `dashJustified`); mirrors `dash-justified`. */
|
|
@@ -123,6 +128,20 @@ export interface MapControllerOptions {
|
|
|
123
128
|
onMapPoint?: (coordinate: [number, number] | null, kind: "click" | "hover") => void;
|
|
124
129
|
/** A Tile3DLayer finished loading its root tileset — the `om-tileset-load` DOM event's twin; `tileset` is the live deck `Tileset3D`. */
|
|
125
130
|
onTilesetLoad?: (layerId: string, tileset: unknown) => void;
|
|
131
|
+
/** A Route layer finished resolving — the `om-route-resolved` DOM event's twin; `route` carries the normalized geometry/distanceMeters/durationSec/legs/bounds. */
|
|
132
|
+
onRouteResolved?: (layerId: string, route: {
|
|
133
|
+
geometry?: {
|
|
134
|
+
type: "LineString";
|
|
135
|
+
coordinates: [number, number][];
|
|
136
|
+
};
|
|
137
|
+
distanceMeters?: number;
|
|
138
|
+
durationSec?: number;
|
|
139
|
+
legs?: {
|
|
140
|
+
distanceMeters: number;
|
|
141
|
+
durationSec: number;
|
|
142
|
+
}[];
|
|
143
|
+
bounds?: [[number, number], [number, number]];
|
|
144
|
+
}) => void;
|
|
126
145
|
/** A `pick-features` layer decoded its property table — the DOM front-end's internal `featureTables` bookkeeping, surfaced here since `ctx.features()` depends on it (see `RuntimeCoreCallbacks.onFeatureTable`). */
|
|
127
146
|
onFeatureTable?: (layerId: string, rows: Record<string, unknown>[]) => void;
|
|
128
147
|
/** A `carriesGeoreference` layer (BIMLayer) finished loading and read its own file's georeferencing (see `RuntimeCoreCallbacks.onGeoreference`). No front end auto-applies terrain (what-you-write-is-what-you-see) — call `controller.setTerrain(...)` from here if you want a reaction. `approximatePlacement` mirrors the DOM lane's data-quality warning. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as re, t as Ae, i as nt, a as Re, s as rt, C as ot, b as it, F as at, A as st, d as lt, R as he, e as ct, p as ut, m as dt, f as ht, g as pt, h as gt } from "./raster-pipeline-
|
|
1
|
+
import { c as re, t as Ae, i as nt, a as Re, s as rt, C as ot, b as it, F as at, A as st, d as lt, R as he, e as ct, p as ut, m as dt, f as ht, g as pt, h as gt } from "./raster-pipeline-mXzwFf0A.js";
|
|
2
2
|
import pe from "./index-CW1n5LdO.js";
|
|
3
3
|
function mt(e, t) {
|
|
4
4
|
const n = e.length / 3, r = new Uint8ClampedArray(n * 4), o = 0, i = n, a = n * 2;
|
|
@@ -1069,7 +1069,7 @@ A.set(m.Zstd, () => import("./zstd-jXobGRcq.js").then((e) => e.decode));
|
|
|
1069
1069
|
A.set(m.Jpeg, () => Promise.resolve(oe));
|
|
1070
1070
|
A.set(m.Jpeg6, () => Promise.resolve(oe));
|
|
1071
1071
|
A.set(m.Webp, () => Promise.resolve(oe));
|
|
1072
|
-
A.set(m.Lerc, () => import("./lerc-
|
|
1072
|
+
A.set(m.Lerc, () => import("./lerc-B1LFm9ra.js").then((e) => e.l).then((e) => e.decode));
|
|
1073
1073
|
async function ie(e, t, n) {
|
|
1074
1074
|
const r = A.get(t);
|
|
1075
1075
|
if (!r)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { w as ae } from "./mgrs-BY9bIvp4.js";
|
|
2
|
-
import { am as ce, an as le, ao as ee, ap as te, aq as ue, b as he, ar as de, l as Z, as as fe, d as pe, at as me, au as ge, av as ve, aw as ne } from "./index-
|
|
2
|
+
import { am as ce, an as le, ao as ee, ap as te, aq as ue, b as he, ar as de, l as Z, as as fe, d as pe, at as me, au as ge, av as ve, aw as ne } from "./index-CnKC1yuV.js";
|
|
3
3
|
function Pe(o, e, t) {
|
|
4
4
|
const { projectedCorners: n } = e, { topLeft: s, topRight: r, bottomRight: a, bottomLeft: i } = n, c = t(s[0], s[1]), u = t(r[0], r[1]), l = t(a[0], a[1]), d = t(i[0], i[1]), f = [
|
|
5
5
|
c,
|
package/dist/runtime-core.d.ts
CHANGED
|
@@ -8,6 +8,20 @@ import type { LayerIR } from "./ir";
|
|
|
8
8
|
import { type Selection } from "./selection";
|
|
9
9
|
import type { ValidationEntry } from "./validation";
|
|
10
10
|
import type { MapViewport } from "./basemap";
|
|
11
|
+
/** RouteLayer's own `onRouteResolved` report (spec: "Routing & Tracking") — a normalized Route plus its bounds. `bounds` drives `follow="fit-route"` here; the WHOLE object forwards to `RuntimeCoreCallbacks.onRouteResolved` (the `om-route-resolved` consumer event), so a page can read a provider-resolved route's geometry/distance/duration without re-fetching it. */
|
|
12
|
+
interface RouteResolvedInfo {
|
|
13
|
+
geometry?: {
|
|
14
|
+
type: "LineString";
|
|
15
|
+
coordinates: [number, number][];
|
|
16
|
+
};
|
|
17
|
+
distanceMeters?: number;
|
|
18
|
+
durationSec?: number;
|
|
19
|
+
legs?: {
|
|
20
|
+
distanceMeters: number;
|
|
21
|
+
durationSec: number;
|
|
22
|
+
}[];
|
|
23
|
+
bounds?: [[number, number], [number, number]];
|
|
24
|
+
}
|
|
11
25
|
/**
|
|
12
26
|
* The tile-level EXT_structural_metadata property-table index, resolved
|
|
13
27
|
* independently of `FeatureMeshLayer`'s own copy of this same computation.
|
|
@@ -119,7 +133,7 @@ export interface RuntimeCoreCallbacks {
|
|
|
119
133
|
* needs (a vertex dropped in blank space). Coordinate is null only when
|
|
120
134
|
* deck reports none (e.g. off-globe).
|
|
121
135
|
*/
|
|
122
|
-
onMapPoint?: (coordinate: [number, number] | null, kind: "click" | "hover") => void;
|
|
136
|
+
onMapPoint?: (coordinate: [number, number] | null, kind: "click" | "hover", pointerType?: string) => void;
|
|
123
137
|
/**
|
|
124
138
|
* XY snapping (spec: issue #34 Part A) — fires ALONGSIDE onMapPoint on
|
|
125
139
|
* every click/hover, `null` whenever that point ISN'T a snap (no config,
|
|
@@ -153,6 +167,13 @@ export interface RuntimeCoreCallbacks {
|
|
|
153
167
|
* @loaders.gl/tiles type dependency in the core).
|
|
154
168
|
*/
|
|
155
169
|
onTilesetLoad?: (layerId: string, tileset: unknown) => void;
|
|
170
|
+
/**
|
|
171
|
+
* A `carriesRoute` layer (Route) finished resolving its route — direct
|
|
172
|
+
* `geometry` or a `RoutingProvider` round-trip alike. The `om-route-resolved`
|
|
173
|
+
* consumer event's source; `route` carries the normalized geometry/
|
|
174
|
+
* distanceMeters/durationSec/legs plus the fitted bounds.
|
|
175
|
+
*/
|
|
176
|
+
onRouteResolved?: (layerId: string, route: RouteResolvedInfo) => void;
|
|
156
177
|
/**
|
|
157
178
|
* A `carriesGeoreference` layer (BIMLayer) finished loading its source file
|
|
158
179
|
* and read whatever georeferencing it declares — `hasFullMapConversion`
|
package/dist/testing.d.ts
CHANGED
|
@@ -59,8 +59,11 @@ export interface TestHarness {
|
|
|
59
59
|
* Feeds a map coordinate through the same path a real deck click/hover
|
|
60
60
|
* takes — drives the draw controller and fires the `om-map-point` event.
|
|
61
61
|
* For testing custom capture tools (sketch/AOI) without a GPU.
|
|
62
|
+
* `pointerType` ("touch" | "pen" | "mouse") simulates that input modality —
|
|
63
|
+
* pass "touch" to exercise touch-only behavior like the draw controller's
|
|
64
|
+
* double-tap completion; omitted means a synthetic pick with no modality.
|
|
62
65
|
*/
|
|
63
|
-
mapPoint(coordinate: [number, number] | null, kind?: "click" | "hover"): Promise<void>;
|
|
66
|
+
mapPoint(coordinate: [number, number] | null, kind?: "click" | "hover", pointerType?: string): Promise<void>;
|
|
64
67
|
/**
|
|
65
68
|
* Sets the camera directly (center/zoom/pitch/bearing) — everything
|
|
66
69
|
* viewport-derived reacts for real: `viewport`-watching widgets,
|
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, g as pr, h 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, g as pr, h as mr } from "./raster-pipeline-mXzwFf0A.js";
|
|
2
|
+
import { ap as gr } from "./index-CnKC1yuV.js";
|
|
3
3
|
import $t from "./index-CW1n5LdO.js";
|
|
4
4
|
var Et;
|
|
5
5
|
function h(e, t, n) {
|
package/docs/routing.md
CHANGED
|
@@ -20,9 +20,28 @@ Two authoring paths, mutually exclusive (`geometry` wins outright if both are pr
|
|
|
20
20
|
- `origin` / `destination` (+ optional `waypoints`, `profile="driving|walking|cycling"`) — resolved via the `RoutingProvider` registered under `provider`'s name. A changed input (a live attribute edit, an undo, a story step) re-resolves, aborting any in-flight request.
|
|
21
21
|
- `color` / `casing-color` style the line (defaults `#2563eb` / `#0f172a`).
|
|
22
22
|
- `follow="fit-route"` fits the camera to the route once it resolves — no manual `flyToBounds`.
|
|
23
|
+
- **Tail modes** — link the route to a `Tracking` layer with `progress-from="<tracking-layer-id>"` and pick how the traveled portion renders with `tail`:
|
|
24
|
+
- `tail="none"` — **client view**: only current position → destination renders; the traveled line and the origin pin disappear behind the rider. What a customer waiting on a delivery should see.
|
|
25
|
+
- `tail="dim"` — **operator view**: the traveled portion darkens (about 35% brightness of `color`, or set `tail-color`) while current position → destination keeps the live color.
|
|
26
|
+
- default (`full`) — the whole route in one color; the split is ignored.
|
|
27
|
+
|
|
28
|
+
The split point is the tracking marker's *interpolated* position, projected onto the nearest point of the route line (real GPS fixes sit off the line), advancing per frame with the glide — not per fix. Before the first fix arrives, the whole route renders as remaining.
|
|
23
29
|
|
|
24
30
|
Re-routing is just attribute writes — see the gallery's **Compute a Route** example, where two map clicks set new endpoints and everything downstream (reconcile, provider round-trip, camera re-fit) is ordinary library machinery.
|
|
25
31
|
|
|
32
|
+
### Reading a resolved route back — `om-route-resolved`
|
|
33
|
+
|
|
34
|
+
Whenever a Route layer resolves — direct `geometry` or a provider round-trip alike — the map dispatches **`om-route-resolved`** (the `om-tileset-load` pattern): `detail = { layerId, route }`, where `route` carries the normalized `geometry`, `distanceMeters`, `durationSec`, `legs`, and the fitted `bounds`. That's how a page reads a provider-computed route without re-fetching it:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
mapEl.addEventListener("om-route-resolved", (e) => {
|
|
38
|
+
const { layerId, route } = e.detail;
|
|
39
|
+
// route.geometry.coordinates, route.distanceMeters, route.durationSec …
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`MapController` mirrors it as the `onRouteResolved(layerId, route)` option. The event fires again on every re-resolve (a changed `origin`/`destination`, an undo, a story step). The gallery's **Delivery Riders** simulation and **Compute a Route** readout card both run entirely on this surface — no page-side fetching or adapter-to-UI plumbing.
|
|
44
|
+
|
|
26
45
|
## Registering a provider
|
|
27
46
|
|
|
28
47
|
```js
|
|
@@ -81,12 +100,12 @@ The `"nika"` provider is registered by default as the manifest's default `provid
|
|
|
81
100
|
- `bearing-field` (default `"bearing"`) names a plain data field — degrees clockwise from north — that rotates the arrow marker. On a GeoJSON row it reads `properties.<field>`; on a flat row, `<field>` directly.
|
|
82
101
|
- `interpolate-ms` (default `1000`) makes the marker **glide** between two fixes instead of jumping — driven by the same per-frame channel the story effects use, so no re-render or accessor recompute per frame. Bearing interpolates the short way around (350° → 10° sweeps through 0°). `prefers-reduced-motion` collapses the glide to an instant move.
|
|
83
102
|
- `follow="follow"` eases the camera toward each new fix over the same duration, so camera and marker arrive together.
|
|
84
|
-
- `color` / `size` style the default arrow
|
|
103
|
+
- `color` / `size` style the marker; `icon="arrow|car|motorcycle"` picks its shape (default arrow) — every shape is drawn nose-up and baked in `color`, so bearing rotation and per-rider tinting apply identically. Unknown names fall back to the arrow (validation warns).
|
|
85
104
|
|
|
86
|
-
One entity per layer in v1 — for a fleet, use one `Tracking` layer per vehicle,
|
|
105
|
+
One entity per layer in v1 — for a fleet, use one `Tracking` layer per vehicle: the glide interpolation is keyed per layer, so several riders animate independently at once (the gallery's **Delivery Riders** example runs three along real OSRM routes). Beyond a handful, drop down to a plain `IconLayer` over a keyed stream with `transition="get-position 300ms"` for the glide (the live AIS shipping example's pattern) — hundreds of entities in one layer.
|
|
87
106
|
|
|
88
107
|
## Current limits (stated, not discovered)
|
|
89
108
|
|
|
90
|
-
- Congestion coloring
|
|
91
|
-
- Route metadata
|
|
109
|
+
- Congestion coloring and route alternatives are not implemented — within each tail segment the line is one solid style.
|
|
110
|
+
- Route metadata has no `ctx` watch token yet — but the `om-route-resolved` event (below) delivers each resolved route to the page, which covers the common cases.
|
|
92
111
|
- `Tracking` renders one entity per layer; multi-entity fleets on one layer are a documented follow-on.
|
package/docs/testing.md
CHANGED
|
@@ -113,7 +113,7 @@ it("panning away empties viewport-scoped widgets", async () => {
|
|
|
113
113
|
});
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
The harness API: `pick` (synthetic picks fed through the exact code path real deck.gl picks take — columnar layers pick object-less by index, exactly like live), `clearSelection` (an empty pick — default kind `"hover"`, a hover-off that runs the tooltip auto-hide path; pass `"click"` for a click on empty space, the gesture that dismisses a `selection-type="click"` popup), `mapPoint` (a click/hover map coordinate through the real onMapPoint path — fires `om-map-point`, drives the draw controller), `emit` (any action, same payload contract as `ctx.emit`/`data-emit`), `setView`, `layers()` (the live IR), `flush`, `unmount`. Every verb settles the library's internal batching before resolving — **you never write a sleep**.
|
|
116
|
+
The harness API: `pick` (synthetic picks fed through the exact code path real deck.gl picks take — columnar layers pick object-less by index, exactly like live), `clearSelection` (an empty pick — default kind `"hover"`, a hover-off that runs the tooltip auto-hide path; pass `"click"` for a click on empty space, the gesture that dismisses a `selection-type="click"` popup), `mapPoint` (a click/hover map coordinate through the real onMapPoint path — fires `om-map-point`, drives the draw controller; an optional third `pointerType` argument — `"touch"`/`"pen"`/`"mouse"` — simulates that input modality, e.g. `mapPoint([1,2], "click", "touch")` twice at one spot exercises touch double-tap completion), `emit` (any action, same payload contract as `ctx.emit`/`data-emit`), `setView`, `layers()` (the live IR), `flush`, `unmount`. Every verb settles the library's internal batching before resolving — **you never write a sleep**.
|
|
117
117
|
|
|
118
118
|
**Remote data:** mock `fetch` and the harness waits for it via the readiness signal:
|
|
119
119
|
|
package/llms.txt
CHANGED
|
@@ -27,13 +27,13 @@ Programmatic/native bridge rule: `MapController.setLayers()` accepts normal func
|
|
|
27
27
|
- Full JavaScript in accessor blocks needs the `js` attribute on the layer (`<om-layer js>` + `<script type="om/accessors">`). Without it, blocks are restricted to `export const name = d => <expression>` — no statements, no loops, no nested functions.
|
|
28
28
|
- Dashed lines are a single attribute: `dash="[6, 3]"` (or SVG-style `dash="6 3"`, plus optional `dash-justified`) on a path-stroking layer (`PathLayer`, `GeoJsonLayer`, `PolygonLayer`, `TripsLayer`). Do NOT hand-wire deck's `PathStyleExtension`/`getDashArray` — the attribute mounts it for you. Values are `[dashLength, gapLength]` in the SAME units as the line width; `dash` on a non-path layer (ScatterplotLayer, etc.) is ignored with a warning.
|
|
29
29
|
- To capture where the user CLICKS on the map (a measure tool, drop-a-pin, a custom rectangle/circle AOI, snap-to-feature), listen for the `om-map-point` event on `<om-map>`: `mapEl.addEventListener('om-map-point', e => { const { coordinate, kind } = e.detail; })` — `coordinate` is `[lng,lat]` (or `null` off-globe), `kind` is `"click"`|`"hover"`, and it fires on every click/hover including empty-map clicks. Do NOT reach for deck.gl internals (`mapEl.getMap()`, `.deckInstance`, `.deck.viewManager`) or unproject canvas pixels — those are not exposed on `<om-map>` and return nothing. The built-in `draw` widget handles polygon/line/point sketching; `om-map-point` is for tools it doesn't cover.
|
|
30
|
-
- `<om-widget type="draw" modes="point line polygon" target="sketch" save="both" autosave="<key>">` is the sketch-capture toolbar — `target` binds the store a `data="draw:<target>"` layer reads. `export-3d` (bare = GLB, `="b3dm"` for Cesium/3D-Tiles pipelines) adds an "Export 3D" button (issue #34), separate from `save` (that's the drawn shape's own GeoJSON): outline a polygon over loaded `Tile3DLayer`/`BIMLayer` content, close it, and it clips every loaded tile's triangles to that footprint (a plain 2D clip, no elevation-picking involved), re-frames them to a local coordinate frame at the footprint's centroid, and downloads it, each triangle carrying its own source color (vertex colors) — no textures (BIM/IFC materials are flat colors, not textured meshes). Only currently-VISIBLE 3D Tiles/BIM layers are included — `visible="false"` (or `toggle-layer`) excludes a layer, with distinct console warnings for "nothing loaded" vs. "everything hidden." Validation warns on an unrecognized `export-3d` value.
|
|
30
|
+
- `<om-widget type="draw" modes="point line polygon" target="sketch" save="both" autosave="<key>">` is the sketch-capture toolbar — `target` binds the store a `data="draw:<target>"` layer reads. Lines/polygons complete via double-click, touch double-tap (iOS WebViews never synthesize `dblclick` from taps — the library detects the tap pair itself, so touch parity is native), Enter, or the toolbar's explicit **Finish** button; Escape cancels the in-progress shape. `export-3d` (bare = GLB, `="b3dm"` for Cesium/3D-Tiles pipelines) adds an "Export 3D" button (issue #34), separate from `save` (that's the drawn shape's own GeoJSON): outline a polygon over loaded `Tile3DLayer`/`BIMLayer` content, close it, and it clips every loaded tile's triangles to that footprint (a plain 2D clip, no elevation-picking involved), re-frames them to a local coordinate frame at the footprint's centroid, and downloads it, each triangle carrying its own source color (vertex colors) — no textures (BIM/IFC materials are flat colors, not textured meshes). Only currently-VISIBLE 3D Tiles/BIM layers are included — `visible="false"` (or `toggle-layer`) excludes a layer, with distinct console warnings for "nothing loaded" vs. "everything hidden." Validation warns on an unrecognized `export-3d` value.
|
|
31
31
|
- Clip box (issue #34): `<om-map clip-box-min="[lng,lat,elev]" clip-box-max="[lng,lat,elev]">` cuts a real axis-aligned 3D box through the whole scene — geometry outside it discarded, every layer clipped by default (`clip="off"` on an `<om-layer>` opts out), works on ANY layer type including georeferenced `Tile3DLayer`/`BIMLayer` content (not just flat `GeoJsonLayer` extrusions). `clip-box-invert` shows outside instead of inside; `clip-box-highlight` dims clipped-out geometry instead of discarding it (non-destructive preview). Attribute-backed (undoable, story-steppable) via `set-clip-box {min, max, invert?, highlight?}` (`{clear:true}` removes it) and `<om-widget type="clip-box">` (six number inputs + invert/highlight checkboxes + clear button). v1 is axis-aligned only — rotation is a documented follow-up.
|
|
32
|
-
- `<om-widget type="measure" modes="distance area volume" units="metric|imperial|nautical">` is the geodesic ruler: click to place points, live labels + a totals panel, read the value programmatically via the `om-measure` event (`detail.mode`/`.totalMeters`/`.areaMeters2`/`.perimeterMeters`/`.cutMeters3`/`.fillMeters3`/`.netMeters3`/`.totalMeters3`/`.cutAdjustedMeters3`/`.fillAdjustedMeters3`/`.cutMassKg`/`.fillMassKg`/`.stale`/`.profileSeries` — profile points are `{x: metres from the first vertex, y: elevation}` plus `vertexIndex` on the samples that ARE drawn corners, so a chart can mark them; vertex 0 is the leftmost, and with `profile` on the map badges the first two vertices `1 · Start` and `2` in draw order so the ring's winding direction is readable at constant cost, while the chart marks every corner). `volume` outlines a footprint like `area` (close it with a double-click/Enter — it turns solid teal, "ready"), then a fixed-screen-pixel-size double-headed arrow gizmo appears at the centroid: drag up to fill, down to cut (unbounded distance), reading Cut/Fill/Net (signed, fill−cut)/Total (unsigned, cut+fill) — always RAW geometric volumes, never altered by `swell`/`shrink`. It REQUIRES `terrain` on `<om-map>` (validation warns a `volume` mode with none — cut/fill against flat ground with no elevation surface has nothing to measure against). The math is a REAL per-cell grid integration: closing a footprint bulk-loads its covering DEM tiles and integrates terrain-vs-base per cell on a metric tangent-plane grid (cell size = the DEM's GSD, scanline point-in-polygon, bilinear seam-correct sampling, worker-offloaded) — mixed cut AND fill in one footprint on undulating ground, with `cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3` (± = per-cell cellArea × 1.5 × GSD, per side)/`nodataFraction` published on the readout; without terrain a flat-plane fallback runs with no error figures. `base-surface` picks the reference: `custom` (default — the gizmo's target plane) or boundary-derived stockpile strategies with no gizmo (`triangulated` boundary TIN, `plane`, `lowest`, `highest`, `average`). Five more volume-only attributes (no-ops, and validation warns, without `volume` in `modes`): `base-surface` (above); `profile` (elevation samples around the footprint's own perimeter, live while sketching, dispatched on `profileSeries` for a paired `dynamic-chart`); `deadband` (m³, zeroes a Cut/Fill figure below the threshold); `density` (t/m³ metric, lb/yd³ imperial) and `swell`/`shrink` (multipliers, default 1×) populate a separate Material section instead — Bank/Loose/Compacted convention, `cutAdjustedMeters3` = raw × swell (loose/haul, bigger), `fillAdjustedMeters3` = raw ÷ shrink (loose/borrow needed, also bigger), tonnage from the raw (mass-conserving) volume — shown only once one of the three is actually configured.
|
|
32
|
+
- `<om-widget type="measure" modes="distance area volume" units="metric|imperial|nautical">` is the geodesic ruler: click to place points, live labels + a totals panel, read the value programmatically via the `om-measure` event (`detail.mode`/`.totalMeters`/`.areaMeters2`/`.perimeterMeters`/`.cutMeters3`/`.fillMeters3`/`.netMeters3`/`.totalMeters3`/`.cutAdjustedMeters3`/`.fillAdjustedMeters3`/`.cutMassKg`/`.fillMassKg`/`.stale`/`.profileSeries` — profile points are `{x: metres from the first vertex, y: elevation}` plus `vertexIndex` on the samples that ARE drawn corners, so a chart can mark them; vertex 0 is the leftmost, and with `profile` on the map badges the first two vertices `1 · Start` and `2` in draw order so the ring's winding direction is readable at constant cost, while the chart marks every corner). `volume` outlines a footprint like `area` (close it with a double-click/double-tap/Enter — it turns solid teal, "ready"), then a fixed-screen-pixel-size double-headed arrow gizmo appears at the centroid: drag up to fill, down to cut (unbounded distance), reading Cut/Fill/Net (signed, fill−cut)/Total (unsigned, cut+fill) — always RAW geometric volumes, never altered by `swell`/`shrink`. It REQUIRES `terrain` on `<om-map>` (validation warns a `volume` mode with none — cut/fill against flat ground with no elevation surface has nothing to measure against). The math is a REAL per-cell grid integration: closing a footprint bulk-loads its covering DEM tiles and integrates terrain-vs-base per cell on a metric tangent-plane grid (cell size = the DEM's GSD, scanline point-in-polygon, bilinear seam-correct sampling, worker-offloaded) — mixed cut AND fill in one footprint on undulating ground, with `cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3` (± = per-cell cellArea × 1.5 × GSD, per side)/`nodataFraction` published on the readout; without terrain a flat-plane fallback runs with no error figures. `base-surface` picks the reference: `custom` (default — the gizmo's target plane) or boundary-derived stockpile strategies with no gizmo (`triangulated` boundary TIN, `plane`, `lowest`, `highest`, `average`). Five more volume-only attributes (no-ops, and validation warns, without `volume` in `modes`): `base-surface` (above); `profile` (elevation samples around the footprint's own perimeter, live while sketching, dispatched on `profileSeries` for a paired `dynamic-chart`); `deadband` (m³, zeroes a Cut/Fill figure below the threshold); `density` (t/m³ metric, lb/yd³ imperial) and `swell`/`shrink` (multipliers, default 1×) populate a separate Material section instead — Bank/Loose/Compacted convention, `cutAdjustedMeters3` = raw × swell (loose/haul, bigger), `fillAdjustedMeters3` = raw ÷ shrink (loose/borrow needed, also bigger), tonnage from the raw (mass-conserving) volume — shown only once one of the three is actually configured.
|
|
33
33
|
|
|
34
34
|
## Element vocabulary
|
|
35
35
|
|
|
36
|
-
- `<om-map center="[lng, lat]" zoom="11" pitch="55" bearing="20" basemap="positron">` — the root. Give it a height (`om-map { display:block; height:100vh }` with `html,body{height:100%}`, or a sized container) — a custom element is display:inline by default and collapses to zero size; with none set, the library falls back to display:block + a 400px floor so a bare map still shows (any height you DO set wins over the floor, including one below 400px), and a collapsed map logs a console warning. A map hidden on purpose (`hidden`, or inside a `display:none` panel) stays hidden and does not warn. `basemap` accepts a free preset (`liberty`, `bright`, `positron`, `dark-matter`, `voyager`, `osm` — no keys; or `maptiler-streets`/`maptiler-dataviz`/`maptiler-satellite` with `basemap-key="…"` or `OmMap.configureBasemap({ maptilerKey })`), `maplibre` (bare demo style), a style URL (e.g. a MapTiler-customized style; any scheme fetch supports, including desktop asset protocols like Tauri's, query strings fine), or `none` (standalone canvas). The attribute is LIVE — writing it switches the basemap in place (camera + layers survive); the `set-basemap {basemap}` action and `<om-widget type="basemap-switcher" options="positron dark-matter osm">` do the same. Register more via `OmMap.registerBasemap(name, { style })`. Required provider attribution renders automatically (`attribution="false"` opts out). `pitch`/`bearing` tilt the initial camera (use for 3D content). Terrain: `terrain="terrarium"` (keyless AWS DEM; also `mapterhorn` — keyless, CARTO Positron drape by default — `maptiler-terrain` with a key, a raw `{z}/{x}/{y}` DEM URL + `terrain-decoder="terrarium|mapbox-rgb|<JSON>"`, or `off`) raises a 3D surface — geographic layers DRAPE onto it automatically (per-layer `terrain="drape|offset|off"` overrides; 3D-model layers sit ON it), `terrain-exaggeration` scales relief, `terrain-texture` drapes imagery; terrain REPLACES an active basemap while on (restored when off); `set-terrain` action, `terrain` watch token, `OmMap.registerTerrain(name, {...})` for more sources. a georeferenced BIM model REQUIRES the map to author `terrain` explicitly — any value including an explicit `terrain="off"` (flat-ground siting); a model that resolves real elevation (IfcMapConversion + OrthogonalHeight) on a map with no `terrain` attribute raises an ERROR through the validation channel at load time (the library never writes attributes for you — no auto-terrain). Scene lighting for 3D content: `lighting="daylight|studio|flat|custom"` (absent = deck defaults) with `lighting-ambient`/`lighting-sun`/`lighting-sun-azimuth`/`lighting-sun-elevation`/`lighting-camera` overrides and `lighting-sun-date` (ISO/epoch — solar-position sun computed at the map center, wins over azimuth/elevation); attribute-backed (undoable, live-editable), story-steppable via the `set-lighting {lighting, sunAzimuth, ...}` action (`lighting="default"` removes the attribute set; a bare preset is a clean reset — stale overrides clear), native UI via `<om-widget type="lighting">` (preset radios + tuning sliders), widget watch token `lighting`. Clip box (issue #34): `clip-box-min="[lng,lat,elev]"` + `clip-box-max="[lng,lat,elev]"` cut a real axis-aligned 3D box through the scene — every layer clipped by default (per-layer `clip="off"` opts out), `clip-box-invert` shows outside instead of inside, `clip-box-highlight` dims clipped-out geometry instead of discarding it; works on ANY layer including georeferenced Tile3DLayer/BIMLayer content; attribute-backed (undoable) via `set-clip-box {min, max, invert?, highlight?}` (`{clear:true}` removes it), native UI `<om-widget type="clip-box">`; v1 axis-aligned only. XY snapping (issue #34 Part A): `snap="vertex edge midpoint"` + `snap-tolerance="12"` (px, default 12) refines a click/hover to the nearest vertex/edge/edge-midpoint of whichever feature deck ALREADY picked under the cursor — not a spatial index, only that one feature's own geometry is searched, on the CPU, when snapping is on. Applies to every layer by default (`snap="off"` on any `<om-layer>` opts it out, mirroring `clip="off"`) and to a `BIMLayer`'s own edge/crease overlay (its real wall corners/edges, converted from the model's local mesh coordinates to real `[lng,lat]` automatically) — the raw triangle MESH itself is not yet a snap target. Vertex beats midpoint beats edge on range conflicts; hold Space to place a point nearby without snapping. GeoJsonLayer extrudes polygons declaratively: `extruded get-elevation="$height"` (+ `wireframe`). `widgets-hidden` attribute (or the `set-widgets-visible {visible}` action / `<om-widget type="widgets-toggle">` button) hides every widget WITHOUT destroying state — attribution never hides (license); transient (not an undo step) but story-steppable, so a step can clear chrome for a cinematic take. Slots auto-dim while an open `<om-overlay>` popup covers them (position stability over the popup dodging; `widgets-dim="off"` disables). `validate` attribute enables live validation + on-page error panel. Include a `map-id="<random UUID>"` on complete pages (identifies the map artifact for usage telemetry — not a visitor id; the page author deletes it to opt out); `telemetry="off"` disables usage telemetry + library-error reporting for the map (global: `OmMap.configureTelemetry({ disabled: true })`; schema: docs/telemetry.md). Free plan limits (HOSTED http(s) pages only — a dev context (localhost, file://, any non-web scheme) lifts every cap while the attribution badge stays; the exemption is technical convenience, not a license grant — commercial deployment incl. packaged apps still requires a key): 5 layers, 25k rows per layer — layers past a limit don't render and validation says why; a `license-key="om_live_…"` attribute (publishable, origin-restricted) or `OmMap.configureLicense(key)` lifts limits and removes the corner badge. When a user's map exceeds free limits, say so and point at the validation message rather than silently restructuring their data. Events on the element: `om-map-ready` (boot complete; `await mapEl.ready` is the promise twin), `om-validation-error`, `om-view-changed` (camera settled after a move, debounced; `detail = {longitude, latitude, zoom, pitch, bearing, origin}` where `origin` is `"user"` for gesture bursts vs `"programmatic"` for API/story moves — the camera-persistence hook, and the echo-suppression signal when syncing camera state to an app store), `om-map-point` (`detail = {coordinate: [lng,lat]|null, kind}` — every click/hover map coordinate incl. empty-map clicks, for custom capture tools beyond the draw widget), `om-tileset-load` (`detail = {layerId, tileset}` — a Tile3DLayer's live deck Tileset3D, for tools needing the real tileset like region export). `MapController` mirrors these as `onViewChange`/`onMapPoint`/`onTilesetLoad` options. `await mapEl.snapshot()` returns a canvas-only PNG dataURL of the scene (basemap + layers at device pixels; `{as:"blob"}` for files) — DOM widgets/overlays/attribution are NOT captured, so exports must render provider credits themselves.
|
|
36
|
+
- `<om-map center="[lng, lat]" zoom="11" pitch="55" bearing="20" basemap="positron">` — the root. Give it a height (`om-map { display:block; height:100vh }` with `html,body{height:100%}`, or a sized container) — a custom element is display:inline by default and collapses to zero size; with none set, the library falls back to display:block + a 400px floor so a bare map still shows (any height you DO set wins over the floor, including one below 400px), and a collapsed map logs a console warning. A map hidden on purpose (`hidden`, or inside a `display:none` panel) stays hidden and does not warn. `basemap` accepts a free preset (`liberty`, `bright`, `positron`, `dark-matter`, `voyager`, `osm` — no keys; or `maptiler-streets`/`maptiler-dataviz`/`maptiler-satellite` with `basemap-key="…"` or `OmMap.configureBasemap({ maptilerKey })`), `maplibre` (bare demo style), a style URL (e.g. a MapTiler-customized style; any scheme fetch supports, including desktop asset protocols like Tauri's, query strings fine), or `none` (standalone canvas). The attribute is LIVE — writing it switches the basemap in place (camera + layers survive); the `set-basemap {basemap}` action and `<om-widget type="basemap-switcher" options="positron dark-matter osm">` do the same. Register more via `OmMap.registerBasemap(name, { style })`. Required provider attribution renders automatically (`attribution="false"` opts out). `pitch`/`bearing` tilt the initial camera (use for 3D content). Terrain: `terrain="terrarium"` (keyless AWS DEM; also `mapterhorn` — keyless, CARTO Positron drape by default — `maptiler-terrain` with a key, a raw `{z}/{x}/{y}` DEM URL + `terrain-decoder="terrarium|mapbox-rgb|<JSON>"`, or `off`) raises a 3D surface — geographic layers DRAPE onto it automatically (per-layer `terrain="drape|offset|off"` overrides; 3D-model layers sit ON it), `terrain-exaggeration` scales relief, `terrain-texture` drapes imagery; terrain REPLACES an active basemap while on (restored when off); `set-terrain` action, `terrain` watch token, `OmMap.registerTerrain(name, {...})` for more sources. a georeferenced BIM model REQUIRES the map to author `terrain` explicitly — any value including an explicit `terrain="off"` (flat-ground siting); a model that resolves real elevation (IfcMapConversion + OrthogonalHeight) on a map with no `terrain` attribute raises an ERROR through the validation channel at load time (the library never writes attributes for you — no auto-terrain). Scene lighting for 3D content: `lighting="daylight|studio|flat|custom"` (absent = deck defaults) with `lighting-ambient`/`lighting-sun`/`lighting-sun-azimuth`/`lighting-sun-elevation`/`lighting-camera` overrides and `lighting-sun-date` (ISO/epoch — solar-position sun computed at the map center, wins over azimuth/elevation); attribute-backed (undoable, live-editable), story-steppable via the `set-lighting {lighting, sunAzimuth, ...}` action (`lighting="default"` removes the attribute set; a bare preset is a clean reset — stale overrides clear), native UI via `<om-widget type="lighting">` (preset radios + tuning sliders), widget watch token `lighting`. Clip box (issue #34): `clip-box-min="[lng,lat,elev]"` + `clip-box-max="[lng,lat,elev]"` cut a real axis-aligned 3D box through the scene — every layer clipped by default (per-layer `clip="off"` opts out), `clip-box-invert` shows outside instead of inside, `clip-box-highlight` dims clipped-out geometry instead of discarding it; works on ANY layer including georeferenced Tile3DLayer/BIMLayer content; attribute-backed (undoable) via `set-clip-box {min, max, invert?, highlight?}` (`{clear:true}` removes it), native UI `<om-widget type="clip-box">`; v1 axis-aligned only. XY snapping (issue #34 Part A): `snap="vertex edge midpoint"` + `snap-tolerance="12"` (px, default 12) refines a click/hover to the nearest vertex/edge/edge-midpoint of whichever feature deck ALREADY picked under the cursor — not a spatial index, only that one feature's own geometry is searched, on the CPU, when snapping is on. Applies to every layer by default (`snap="off"` on any `<om-layer>` opts it out, mirroring `clip="off"`) and to a `BIMLayer`'s own edge/crease overlay (its real wall corners/edges, converted from the model's local mesh coordinates to real `[lng,lat]` automatically) — the raw triangle MESH itself is not yet a snap target. Vertex beats midpoint beats edge on range conflicts; hold Space to place a point nearby without snapping. GeoJsonLayer extrudes polygons declaratively: `extruded get-elevation="$height"` (+ `wireframe`). `widgets-hidden` attribute (or the `set-widgets-visible {visible}` action / `<om-widget type="widgets-toggle">` button) hides every widget WITHOUT destroying state — attribution never hides (license); transient (not an undo step) but story-steppable, so a step can clear chrome for a cinematic take. Slots auto-dim while an open `<om-overlay>` popup covers them (position stability over the popup dodging; `widgets-dim="off"` disables). `validate` attribute enables live validation + on-page error panel. Include a `map-id="<random UUID>"` on complete pages (identifies the map artifact for usage telemetry — not a visitor id; the page author deletes it to opt out); `telemetry="off"` disables usage telemetry + library-error reporting for the map (global: `OmMap.configureTelemetry({ disabled: true })`; schema: docs/telemetry.md). Free plan limits (HOSTED http(s) pages only — a dev context (localhost, file://, any non-web scheme) lifts every cap while the attribution badge stays; the exemption is technical convenience, not a license grant — commercial deployment incl. packaged apps still requires a key): 5 layers, 25k rows per layer — layers past a limit don't render and validation says why; a `license-key="om_live_…"` attribute (publishable, origin-restricted) or `OmMap.configureLicense(key)` lifts limits and removes the corner badge. When a user's map exceeds free limits, say so and point at the validation message rather than silently restructuring their data. Events on the element: `om-map-ready` (boot complete; `await mapEl.ready` is the promise twin), `om-validation-error`, `om-view-changed` (camera settled after a move, debounced; `detail = {longitude, latitude, zoom, pitch, bearing, origin}` where `origin` is `"user"` for gesture bursts vs `"programmatic"` for API/story moves — the camera-persistence hook, and the echo-suppression signal when syncing camera state to an app store), `om-map-point` (`detail = {coordinate: [lng,lat]|null, kind}` — every click/hover map coordinate incl. empty-map clicks, for custom capture tools beyond the draw widget), `om-tileset-load` (`detail = {layerId, tileset}` — a Tile3DLayer's live deck Tileset3D, for tools needing the real tileset like region export), `om-route-resolved` (`detail = {layerId, route}` — a Route layer resolved, direct geometry or provider round-trip; `route` carries the normalized geometry/distanceMeters/durationSec/legs/bounds — how a page reads a provider-computed route without re-fetching, re-fired on every re-resolve). `MapController` mirrors these as `onViewChange`/`onMapPoint`/`onTilesetLoad`/`onRouteResolved` options. `await mapEl.snapshot()` returns a canvas-only PNG dataURL of the scene (basemap + layers at device pixels; `{as:"blob"}` for files) — DOM widgets/overlays/attribution are NOT captured, so exports must render provider credits themselves.
|
|
37
37
|
- `<om-layer id="..." type="ScatterplotLayer" data="./points.json">` — any deck.gl layer class by `type` (39 layer types total: 32 bundled deck.gl core/geo/aggregation/mesh layers, plus the native `COGLayer`/`ZarrLayer` raster types, `ImageOverlay` for georeferenced drone JPEGs, `BIMLayer` for BIM source files, and `Route`/`Tracking` for routing/live-tracking — see the dedicated bullet below), plus `PopupLayer` (WebGL badges/labels at scale: `layout="badge|pin-label|card"`, `min-zoom`/`max-zoom`). Every type's full attribute list ships in the package's `onlymapjs.html-data.json` — consult it instead of guessing attribute names or reading the minified dist. TextLayer's default font atlas covers ASCII only: set `character-set` when label text carries other glyphs (`—`, `·`, accents) or deck warns and renders them blank. External layer classes register via `OmMap.registerLayer({type, deckClass, props})` — build them on `@nika-js/onlymap/deck` (the bundled `CompositeLayer`/`TileLayer`/… re-exports), never a separately-installed deck.gl (different class hierarchy, breaks in the renderer); function-valued props ride the subclass's `static defaultProps`; register at module top level BEFORE the manifest mounts (see docs/custom-layers.md). Data: `data` URL (JSON, GeoJSON, CSV/TSV `.csv` — parsed to typed columns — Shapefile `.shp` (+`.dbf` attributes) and KML `.kml` as GeoJSON features, GPX `.gpx` (waypoints/tracks/routes → features tagged `_gpxKind`; a `#waypoints`/`#tracks`/`#routes` URL fragment selects one part), FlatGeobuf `.fgb` (cloud-native binary vector, whole-file decode), GeoParquet `.parquet`/`.geoparquet` (cloud-native columnar vector — all-Point files stay columnar like Arrow, lines/polygons become GeoJSON features; requires the file's `geo` metadata with WKB geometry, and CRS84/EPSG:4326 — a projected CRS is a loud error telling you to reproject, not a silent misplacement; snappy/gzip/zstd row-group compression handled), CityJSON `.city.json` / CityJSONSeq `.city.jsonl` (semantic 3D city models — 3DBAG, PLATEAU — decoded to one of two shapes by the `data` URL, no CityJSON layer type: default → extruded footprints, `type="GeoJsonLayer" extruded get-elevation="$roof_height"`; `?om-surfaces=1` → one row PER FACE at its own real per-vertex height so a pitched LoD2.2 roof actually looks pitched, `type="SolidPolygonLayer" get-polygon="$polygon" full3d` (`extruded` stays at its ordinary `false` default) (flat-shaded — deck.gl only lights the `extruded` shader path — each row also carrying `surface_type`: RoofSurface/WallSurface/GroundSurface, and `fill_color`: a ninja-viewer-style default color per surface_type/cityobject_type, verified against cityjson-threejs-loader's own default palette — `get-fill-color` on `SolidPolygonLayer` reads it automatically when left unauthored, no color attribute required, and an authored `get-fill-color` still overrides it); derived properties (both modes) `roof_height` (area-weighted mean roof height above ground), `eaves_height`, `ridge_height`, `ground_height`, `roof_area`, `surface_count`, `lod`, `cityobject_id`, `cityobject_type`, `parent_id` win over same-named source attributes, plus surfaces-mode-only `polygon`/`outline`/`surface_type`/`fill_color` (`outline` is the face's outer ring flattened and closed — bind a companion `type="PathLayer" get-path="$outline"` layer to it for visible face edges, since surfaces mode is flat-shaded and `SolidPolygonLayer`'s own `wireframe` prop is a no-op when unextruded — always pair one, matching `filter-field`/`filter-range` to the fill layer), and a parent Building's attributes are inherited by its BuildingPart rows; national grids NL/CH/DE/JP/AT/SG reproject automatically including axis order, other EPSG codes fail with an error naming the code; highest LoD wins, pin one with `?om-lod=1.2` (combine as `?om-lod=1.2&om-surfaces=1`, cached independently); `.city.jsonl` fills in as it downloads in either mode — see docs/3d-assets.md), or Arrow IPC `.arrow`/`.feather` — large point datasets stay columnar, GeoArrow line/polygon geometry becomes GeoJSON features, zstd-compressed IPC is handled; other formats plug in via `OmMap.registerFormat({match, parse})`; data URLs accept any scheme the runtime's fetch supports — desktop webviews (Tauri, Electron) pass asset-protocol URLs straight in), inline `<script type="application/json">` (row arrays or column-oriented `{"columns": {"lon": [...], "lat": [...]}}`; must be a DIRECT child of the `<om-layer>`, and when present it wins — omit the `data` attribute), or `wss://` streaming (`key="mmsi"` upserts entities in place, `flush="250ms"` coalesces bursts, `source="name"` selects a `OmMap.registerSource` decoder plugin), or a polled REST snapshot (`refresh="5s"` re-fetches and replaces — for live endpoints that return the full current state). TILED layers: a `{z}/{x}/{y}` `data` template is deck's tile URL for `TileLayer`/`MVTLayer` (NOT rows) — passed through to deck verbatim, never fetched/parsed, so `<om-layer type="TileLayer" data="…/{z}/{x}/{y}.png">` works (raster gets a built-in BitmapLayer sublayer) and `type="MVTLayer" data="…/{z}/{x}/{y}.pbf"` self-renders vector tiles with `get-*` accessors applying to each decoded feature's properties; a tiled layer has no local rows so `ctx.data`/`ctx.stats`/`filter-*` don't apply. Authenticated endpoints: call `OmMap.configureData({ headers: {...} })` in a script — never put tokens in attributes. `$field` accessors work identically on all of them — never write column-index code yourself. One columnar restriction: the `js` full-JS opt-in is not allowed on Arrow/columnar layers (validation will tell you; use `$field` accessors instead). For 3D models use `type="ScenegraphLayer"` with `scenegraph="./model.glb"` (required) and `get-orientation="[0, $heading, 90]"` — the roll of 90 stands Y-up glTF models upright; see docs/3d-assets.md. 3D Tiles use `type="Tile3DLayer"` with `tileset="…/tileset.json"` (NOT `data`). Any pickable layer can opt into deck's DEPTH-pick pass with `pickable="3d"` instead of a bare `pickable` (issue #34) — the resolved click/hover coordinate then carries a real elevation (`{{z}}` in overlay/tooltip templates, `ctx.selection.coordinate`) instead of the ray∩ground-plane guess, so a click on a building face lands ON the face rather than on the ground behind it; `terrain` sets this on itself. For BIM/photogrammetry, `pickable` alone picks a whole TILE — add `pick-features` to pick an individual ELEMENT (a wall, a window, one IFC product), and the `selection` then carries that element's `featureId`/`properties`/`class` from the tile's own `EXT_mesh_features` + `EXT_structural_metadata` (`feature-id-property` selects the ID set, default `_FEATURE_ID_0`). `feature-styles` recolours/fades/highlights by feature ID — an array indexed BY id of `{color: [r,g,b], strength: 0-1, opacity: 0-1}`, live-settable, uploaded as a small lookup texture (no refetch). Use `strength` below 1 to tint rather than replace, or the model's own texture is hidden. Isolate/hide/ghost are DECLARATIVE and mirror the vector `filter-field`/`filter-categories` pair — `feature-filter-field="component"` names the metadata field, then `isolate-features='["Clock"]'` / `hide-features='["Windows","Skylight"]'` / `ghost-features='["Wall"]'` take JSON value lists matched against the tile's property table (`ghost-opacity` tunes the fade, default 0.18). `isolate-features` is EXCLUSIVE (anything unlisted is hidden); hiding is a shader discard so a hidden element also stops being pickable and whatever is behind it becomes selectable. They compose ONTO `feature-styles` (style table supplies colour, these supply visibility), and being attributes they are undoable and story-steppable — prefer them over computing a style table in page JS. Multi-material/multi-primitive models fan out correctly (glTF allows one material per primitive, so real IFC exports are usually many primitives); only genuinely instanced i3dm tiles stay tile-granularity. Limits to state rather than discover: texture-backed IDs — how photogrammetry classification ships — require `load-options='{"gltf":{"loadBuffers":true,"loadImages":true},"image":{"type":"data"}}'`, and omitting `image.type` makes the tileset take MINUTES to appear (loaders.gl otherwise reads the whole ID texture back through a canvas once per vertex); and `opacity` below 1 currently blanks the model. BIM WIDGETS (all need a `pick-features` layer): `<om-widget type="ifc-browser" layer="clinic" fields="ifcClass material container spatialPath" scale-fields="netVolume" rows="7">` groups the model by a property-table field, counts each value, and gives every row I/H/G buttons that WRITE `isolate-features`/`hide-features`/`ghost-features` (so it is a UI over the attributes — undoable and story-steppable; I is MULTI-SELECT, isolating the union of every pressed row or tree node, since `isolate-features` is a list and the layer matches it as a set) while keeping the companion outline layer's `filter-categories` in step; that same select also offers whichever TREES the file supports — Spatial (`spatialPath`), Type (`typePath`), System (`systemPath`) and Classification (`classificationPath`, built by walking `ReferencedSource`) — each expandable with counts aggregated upward and the same I/H/G on every node, so isolating a storey or a system or a CCS code reaches every element under it. Spatial is NOT privileged (on a real Danish project the classification tree covered 3,415 elements to spatial's 660). A tree is not a separate mode, just a group-by on a hierarchy column — LIST it in `fields` to offer it, or set `field="spatialPath"` to open on it — and `loadIfc` emits a hierarchy column ONLY when the file populates it, so a tree that would render empty is never offered; trees appear automatically when available (no need to list them in `fields`) and `field="spatialPath"` opens straight onto one. ONE browser per layer: `feature-filter-field` and the isolate/hide/ghost attributes are single-valued, so two instances on one layer clobber each other. The widget was renamed from `ifc-legend` (still registered as a deprecated alias) because it is a model browser, not a legend. non-physical classes (IfcSpace, IfcOpeningElement) are hidden unless `show-non-physical`, and `no-color` removes the colour select. `<om-widget type="feature-inspector" fields="ifcClass material container netVolume">` (renamed from `ifc-inspector`, kept as an alias) shows the picked element's properties. `<om-widget type="ifc-loader" layer="ifc" federate>` is a drop zone that parses `.ifc` files IN THE BROWSER and builds both layers itself; `federate` accepts SEVERAL models into one co-registered scene (one drop zone, one layer per model, each with a visibility toggle) rather than one widget per discipline, matching how coordination tools append models; a model that turns out to be georeferenced is AUTO-PLACED (its own coordinates/heading/scale written onto the layers the WIDGET created, camera flown there) — but the widget NEVER writes `<om-map>`'s own scene attributes (`basemap`, `terrain`): those are author-owned, and a georeferenced model on a map with neither raises a structured "no spatial context" warning instead of switching one on. COLOUR BY PROPERTY instead of hand-computing a `feature-styles` table: `feature-color-by` (categorical, `feature-palette` overrides the built-in cycle) or `feature-color-scale` (graduated over a numeric field), with `feature-color-strength` (default 0.85) controlling how hard the colour mixes over the model's own material. Setting NEITHER is the default and is meaningful — the model renders in its own IFC surface colours. A graduated ramp needs the field populated: Revit IFC2x3 exports often carry no `IfcElementQuantity`, so every `netVolume` is 0 and the ramp is flat. Widget scripts read the decoded property table with `ctx.features(layerId)` (undefined until the first tile carrying one lands) and re-render on the `features` watch token. `<om-widget type="ifc-clash" layers="arch mep" tolerance="0">` is the CLASH OVERLAY over two co-registered model layers: it flags element pairs whose bounding boxes interpenetrate, colours both sides via `feature-styles`, and flies to the centre of each overlap. v1 is an axis-aligned box test — fast and serverless, but it over-reports anything diagonal and says nothing about which clashes matter; zones/spaces/openings/proxies and same-class-same-name pairs are excluded as noise. The header carries an overlay on/off switch; an isolation mode select (None/Dim/Hide) sits above the results list and applies once a row is selected (with nothing selected, nothing is hidden), and clicking a row FOCUSES that clash (chosen pair at full strength, every other clashing element dropped to a faint tint, camera flown to the overlap centre) — without that, everything is highlighted and nothing is. Results are GROUPED by the side-A element with a count (one wall crossing four ducts is one row), and the two model selects appear only when more than two models are loaded. It CHECKS co-registration (matching `site-origin`) and says so when it fails, because two mis-registered models report zero clashes exactly like two clean ones. Persisting/sharing results is BCF's job and is out of scope. `<om-layer type="BIMLayer" src="./model.ifc">` is the declarative counterpart to `ifc-loader`/`loadIfc`: point it at a BIM source file (an .ifc today) and it runs the loader itself the moment src resolves — no pre-baked tileset, no site-origin/site-heading/site-scale (the file's own georeference is read and applied automatically; not wired up yet: an authored site-origin on a BIMLayer does not override it), and no separate PathLayer for the outline overlay (added automatically). `pick-features` defaults ON (unlike a plain Tile3DLayer); feature-filter-field/feature-styles/isolate-hide-ghost/feature-color-by/ghost-opacity all work unchanged, since BIMLayer forwards them to a real Tile3DLayer it builds internally. Known gap: the outline overlay does not yet follow isolate/hide/ghost the way the mesh does. Reach for BIMLayer when the model is fixed and known ahead of time; reach for ifc-loader when a visitor picks the file or several models need to federate. IN-BROWSER IFC: `loadIfc(bytes)` parses an `.ifc` with web-ifc (WASM, MPL-2.0, CDN-fetched on first use — NOT a package dependency; `configureIfc({wasmPath})` self-hosts) and returns `{tilesetUrl, edgesUrl, loadOptions, features (rows carry `ifcClass`/`name`/`material`/`container`/`netVolume`/`spatialPath`/`typePath`/`systemPath`/`classificationPath` — hierarchy columns joined by U+001F that the model trees navigate, each emitted only when the file populates it), lonLat, georeferenced, heading, scale, originSource, headingSource, stats, timings, bounds, revoke()}`; the output IS a tileset so picking/styling/`site-*`/isolate-hide-ghost work unchanged. CALL `revoke()` when swapping models — blob URLs are held by the document. FEDERATION: pass the first model's returned `origin` as `LoadIfcOptions.origin` for every later model of the same building, or each is centred on its own bounding box and they drift apart — a clash pass then finds nothing, which looks identical to a clean model. `ifc-loader` shares one origin AND one placement (`site-origin`/`site-heading`/`site-scale`) per map automatically — discipline files routinely declare IfcSite coordinates kilometres apart for the same building, so the first model loaded decides where it goes and the rest follow (`independent` opts out). Every element also carries a tile-local bounding box in the property table (`bboxMinE`/`bboxMinN`/`bboxMinU`/`bboxMaxE`/`bboxMaxN`/`bboxMaxU`). POSITION is read from the file preferring the trustworthy route: `IfcMapConversion` (a surveyed placement into a named projected CRS) WINS over `IfcSite.RefLatitude`/`RefLongitude`, which is very often an authoring default; `originSource` reports which was used, and anything other than `"map-conversion"` raises a structured `"warning"` through the same validation channel other `om-layer` errors use (the on-page panel with `validate` set, `om-validation-error`'s `detail.warnings`) for both `BIMLayer` and `ifc-loader`, once per layer — it never flips `valid` false, only flags the position may be off by tens of metres with no rotation correction applied; override with `site-origin`/`site-heading` or a proper `IfcMapConversion`. Un-projecting a map conversion is supported for WGS84 UTM zones (EPSG:326xx/327xx) and DECLINED with a warning for anything else — a guessed projection lands the model in another country while looking plausible. Eastings/northings are in the target CRS unit, frequently MILLIMETRES. Reading a file correctly and the model being somewhere sensible are SEPARATE problems: all three prepared samples declare placeholders (clinic on Revit's Boston default, which is a 1630 graveyard; duplex on a Chicago city-centre point; bridge — which does carry a real IfcMapConversion — into the mid-Pacific). `headingSource` distinguishes "map-conversion"/"true-north" (read) from "assumed" (file was silent) — report the assumption, never let it read as a measurement. `<om-map>` reads camera attributes ONCE at init, so `setAttribute("center", …)` after mount moves nothing: use `map.flyTo(lonLat, zoom)`. GEOREFERENCING is declarative on `Tile3DLayer` and `PathLayer`: `site-origin="[lng, lat]"` (or `[lng, lat, elevation]`) OVERRIDES the position baked into a tileset's root transform, `site-heading` is a bearing in degrees CLOCKWISE from true north (on its own it rotates the model where it stands), `site-scale` is a uniform multiplier; rotation and scale pivot on the model's own anchor, not the tileset origin. An IFC model is a PAIR of layers — the mesh tileset plus a `PathLayer` outline overlay whose paths are local east/north/up METRES — and both need the same three values or the building separates from its own edges. Never trust a model's declared position without looking at it: authoring tools ship a default project location that is indistinguishable from a survey (the buildingSMART Medical-Dental Clinic sample carries Revit's Boston default, the Duplex a Chicago city-centre point, so both land on occupied downtown blocks at an arbitrary rotation), `IfcMapConversion` is absent from most IFC2x3 exports, and `TrueNorth` is routinely unset. Editing `site-*` on a live Tile3DLayer reloads the tileset (deck.gl only reloads on a URL change); the PathLayer updates as a uniform. GeoTIFF/COG rasters use `type="COGLayer"` with `src="./dem.tif"` (NOT `data` — rasters stream tiles by Range request, they are not parsed rows): `min`/`max` set the rescale window (default 0–255; ALWAYS set them for float/16-bit data like DEMs), `colormap` picks a bundled ramp for single-band sources (gray, viridis, plasma, inferno, magma, cividis, rdylgn, rdbu, spectral, terrain, jet, turbo, ylorrd), `nodata` overrides the source sentinel (renders transparent); plain 8-bit RGB COGs need no styling attributes; restretch/recolor are GPU uniforms (no refetch) and the legend ramp derives from colormap+min/max automatically. Sources must be Cloud-Optimized (`gdal_translate -of COG` otherwise).
|
|
38
38
|
- Geotagged drone JPEGs are the library-owned `ImageOverlay` type, not row `data` and not a raw `BitmapLayer`: `<om-layer type="ImageOverlay" src="./photo.jpg" georeference="exif">`. It reads GPS/relative altitude/camera/focal length plus DJI gimbal metadata through the configured fetch policy, waits before `ready`, bakes yaw/roll, and computes visualization-grade flat-ground bounds. Unknown cameras need `sensor-width-mm` + `sensor-height-mm` (and `focal-length-mm` when EXIF lacks it). For collaborative/saved maps call `OmMap.resolveImageOverlay(fileOrUrl)`, upload its returned `image`, then reconstruct using `src` + the returned explicit `bounds` (no EXIF fetch). Use `COGLayer` for large orthomosaics; see docs/image-overlays.md.
|
|
39
39
|
- Chunked N-dimensional Zarr / GeoZarr rasters (climate/weather grids, datacubes) are the library-owned `ZarrLayer` type (built on `@developmentseed/deck.gl-zarr` + zarrita, a lazy chunk): `<om-layer type="ZarrLayer" src="./x.zarr" variable="temp" select="time=0" colormap="viridis" min="…" max="…">`. `src` not `data` (chunks stream through the layer's reader, never parsed rows). Pick the `variable` and pin EVERY non-spatial dimension in `select` ("init_time=0, lead_time=0, ensemble_member=0"); the two spatial dims are handled for you (a 2-D array needs no select). A GeoZarr-compliant store georeferences itself; a plain Zarr needs manual `bounds="[w,s,e,n]"` + `crs="EPSG:4326"` + `spatial-dims="<yName> <xName>"` (bounds without crs+spatial-dims is a validation error). `min`/`max`/`colormap`/`nodata` and the auto legend reuse the exact COGLayer raster pipeline. Beware store chunking: a dataset chunked coarsely over non-spatial dims (e.g. all forecast steps in one chunk) decodes far more than the pinned frame needs. `src` may be any absolute URL (`https://…/store.zarr`) — an external/remote store works with no server setup (a static host serves Zarr's extensionless chunk keys natively), but zarrita fetches it directly from the browser so the store MUST send CORS headers (`Access-Control-Allow-Origin`), and it must be PUBLIC — authenticated stores are not yet supported (ZarrLayer uses zarrita's own fetch, not `OmMap.configureData`).
|
|
@@ -45,8 +45,8 @@ Programmatic/native bridge rule: `MapController.setLayers()` accepts normal func
|
|
|
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 ("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), so 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
46
|
- 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
47
|
- `<om-story id="tour" autoplay loop interrupt="pause|ignore">` — a storyboard of `<om-step>` children. Each step: `action="..."` + payload attributes (same kebab-case rule as behaviors) + `duration`/`delay`/`parallel` timing. Steps REFERENCE layers/overlays by id (`layer=`/`target=`) — a step must NEVER contain elements (validation error). Control: `<om-widget type="player" story="tour">`, the story-play/story-pause/story-seek actions, or `storyEl.play()/pause()/seek(ms)`. Seeking restores initial state then applies steps before T; use declarative payloads (e.g. `action="toggle-layer" visible="true"`, not bare toggles) so scrubbing is deterministic. Scene actions are story-steppable AND scrub-capturable: `set-basemap`, `set-lighting` (a sunset story: steps walking sun-elevation down; a bare preset step is a clean reset), and `set-terrain` all rewind on seek — the story captures the map's scene attributes before first play. Effect verbs as bare step attributes: `<om-step fade layer="regions" duration="1s">` (opacity reveal — start the layer at `opacity="0"`), `pulse` (attention flash), `trace` (progressive draw — whole-layer needs a TripsLayer; add `feature-id="..."` to make ONE polygon/line draw itself on inside any layer, or use it from a click behavior for click-to-trace), `populate` (rows drop in one by one — ordered by the authored filter-field, a payload `field`, or data order).
|
|
48
|
-
- Filtering: `filter-field="magnitude" filter-range="[4, 10]"` on a layer (GPU-side, live-updatable via the `filter-layer` action); pair with `<om-widget type="filter" layer="..." field="...">`. For an epoch-millisecond field, make the slider labels readable with `<om-widget type="filter" layer="quakes" field="time" format="date" date-style="datetime" time-zone="UTC"></om-widget>`. Up to 4 numeric dimensions at once via `filter-fields='[{"field":"magnitude","range":[4,10]},{"field":"time","range":[…]}]'` (JSON array, additive to filter-field/filter-range — wins if both are authored) — one `<om-widget type="filter">` per field, each moves its own dimension independently (filter-layer merges the range onto the matching field rather than replacing the whole filter); a row must pass every active dimension (AND). A dimension can't be added live — the full set is declared up front in filter-fields. Categorical filtering is a SEPARATE mechanism (deck.gl's own discrete keep-list test, not a range) with its own attributes: `filter-category="fuel" filter-categories='["Coal","Gas"]'` (single) or `filter-category-fields='[{"field":"fuel","categories":[...]},...]'` (up to 4); the SAME `<om-widget type="filter">` auto-renders checkboxes instead of a slider when its `field` is declared categorically (mode is inferred from the layer's own filter, never a separate widget attribute) — one checkbox per distinct value present in the data, with its row count. A category dimension with no keep-list is dropped from the active filter (there is no "matches everything" category the way a numeric range has [-Infinity, Infinity]). Numeric and categorical filters on the same layer combine — a row must pass both. `ctx.stats`/`ctx.dataInViewport` respect whichever kind(s) are active by default (`{filtered:false}` opts out).
|
|
49
|
-
- Routing & tracking are two library-owned layer types (not `PathLayer`/`IconLayer` hand-wired) that expand into ordinary `PathLayer`/`IconLayer` instances internally, same pattern as `BIMLayer`→`Tile3DLayer`. `<om-layer type="Route" geometry='{"type":"LineString","coordinates":[[lng,lat],...]}'>` draws a styled route (casing + line + origin/destination pins) from geometry you already have — resolves SYNCHRONOUSLY, no network. `<om-layer type="Route" origin="[lng,lat]" destination="[lng,lat]" provider="nika" profile="driving">` (+ optional `waypoints`) resolves one ASYNCHRONOUSLY via a `RoutingProvider` named by `provider` — `"nika"` is registered by default but its endpoint is an UNVERIFIED PLACEHOLDER until NIKA's real routing service ships (register a working one with `OmMap.registerRoutingProvider(name, provider)` — a ~15-line adapter over OSRM's keyless public demo server (`router.project-osrm.org/route/v1/{profile}/{lng},{lat};{lng},{lat}?geometries=geojson&overview=full`, map `distance`/`duration`/`legs` onto `distanceMeters`/`durationSec`/`legs[]`) is the verified keyless real-data recipe; the skill's syntax.md carries it in full). `geometry` wins outright if both are authored (validation warns). `color`/`casing-color` style the line; `follow="fit-route"` auto-fits the camera once resolved. `<om-layer type="Tracking" get-position="[$lng,$lat]">` renders ONE moving entity (v1 — a fleet is one `Tracking` layer per vehicle) with bearing-derived icon rotation; position data arrives through the ORDINARY `data`/`source` mechanism, no separate tracking-subscription API. `bearing-field` (default `"bearing"`) names the plain field to rotate by (checks `properties.<field>` on GeoJSON rows, `<field>` directly on flat rows); `interpolate-ms` (default `1000`) glides the marker between two fixes via the per-frame channel instead of jumping; `follow="follow"` eases the camera along with it, same timing. `color`/`size` style the default arrow
|
|
48
|
+
- Filtering: `filter-field="magnitude" filter-range="[4, 10]"` on a layer (GPU-side, live-updatable via the `filter-layer` action); pair with `<om-widget type="filter" layer="..." field="...">`. For an epoch-millisecond field, make the slider labels readable with `<om-widget type="filter" layer="quakes" field="time" format="date" date-style="datetime" time-zone="UTC"></om-widget>`. Up to 4 numeric dimensions at once via `filter-fields='[{"field":"magnitude","range":[4,10]},{"field":"time","range":[…]}]'` (JSON array, additive to filter-field/filter-range — wins if both are authored) — one `<om-widget type="filter">` per field, each moves its own dimension independently (filter-layer merges the range onto the matching field rather than replacing the whole filter); a row must pass every active dimension (AND). A dimension can't be added live — the full set is declared up front in filter-fields. Categorical filtering is a SEPARATE mechanism (deck.gl's own discrete keep-list test, not a range) with its own attributes: `filter-category="fuel" filter-categories='["Coal","Gas"]'` (single) or `filter-category-fields='[{"field":"fuel","categories":[...]},...]'` (up to 4); the SAME `<om-widget type="filter">` auto-renders checkboxes instead of a slider when its `field` is declared categorically (mode is inferred from the layer's own filter, never a separate widget attribute) — one checkbox per distinct value present in the data, with its row count. A category dimension with no keep-list is dropped from the active filter (there is no "matches everything" category the way a numeric range has [-Infinity, Infinity]). Numeric and categorical filters on the same layer combine — a row must pass both. `ctx.stats`/`ctx.dataInViewport` respect whichever kind(s) are active by default (`{filtered:false}` opts out). CLASSIFIED SYMBOLOGY: `classify-by="<numeric field>"` (+ `classify-scale="quantile|equal-interval|jenks"`, `classify-classes="2-12"` default 5, `classify-ramp="viridis|plasma|inferno|magma|cividis|turbo|blues|greens|oranges|purples|reds|ylorrd|rdbu|spectral"`) computes class breaks FROM THE DATA at reconcile time, installs the fill-color accessor and the auto classes legend — use it when the user asks for graduated/choropleth styling WITHOUT hand-authoring domains; an authored get-fill-color/color always wins (validation warns on the conflict); URL-backed layers classify when their data arrives. TEMPORAL PLAYBACK: `<om-widget type="time-slider" layer="…" field="<numeric/epoch-ms field>" duration="20s" window="<span in field units>" loop format="date" date-style time-zone>` — play/pause/scrub emitting the ordinary filter-layer action (cumulative from the domain start, or a sliding window with `window`); manifest stays the source of truth so undo/story/external filter edits re-sync the thumb — prefer it over hand-rolling a playback loop.
|
|
49
|
+
- Routing & tracking are two library-owned layer types (not `PathLayer`/`IconLayer` hand-wired) that expand into ordinary `PathLayer`/`IconLayer` instances internally, same pattern as `BIMLayer`→`Tile3DLayer`. `<om-layer type="Route" geometry='{"type":"LineString","coordinates":[[lng,lat],...]}'>` draws a styled route (casing + line + origin/destination pins) from geometry you already have — resolves SYNCHRONOUSLY, no network. `<om-layer type="Route" origin="[lng,lat]" destination="[lng,lat]" provider="nika" profile="driving">` (+ optional `waypoints`) resolves one ASYNCHRONOUSLY via a `RoutingProvider` named by `provider` — `"nika"` is registered by default but its endpoint is an UNVERIFIED PLACEHOLDER until NIKA's real routing service ships (register a working one with `OmMap.registerRoutingProvider(name, provider)` — a ~15-line adapter over OSRM's keyless public demo server (`router.project-osrm.org/route/v1/{profile}/{lng},{lat};{lng},{lat}?geometries=geojson&overview=full`, map `distance`/`duration`/`legs` onto `distanceMeters`/`durationSec`/`legs[]`) is the verified keyless real-data recipe; the skill's syntax.md carries it in full). `geometry` wins outright if both are authored (validation warns). `color`/`casing-color` style the line; `follow="fit-route"` auto-fits the camera once resolved. `<om-layer type="Tracking" get-position="[$lng,$lat]">` renders ONE moving entity (v1 — a fleet is one `Tracking` layer per vehicle) with bearing-derived icon rotation; position data arrives through the ORDINARY `data`/`source` mechanism, no separate tracking-subscription API. `bearing-field` (default `"bearing"`) names the plain field to rotate by (checks `properties.<field>` on GeoJSON rows, `<field>` directly on flat rows); `interpolate-ms` (default `1000`) glides the marker between two fixes via the per-frame channel instead of jumping; `follow="follow"` eases the camera along with it, same timing. `color`/`size` style the marker; `icon="arrow|car|motorcycle"` picks the shape (default arrow — all nose-up, baked in `color`, unknown names fall back with a validation warning). Tail modes: on the ROUTE layer, `progress-from="<tracking-layer-id>"` + `tail="none"` (client view — only current position → destination renders, origin pin dropped) or `tail="dim"` (operator view — traveled portion darkened; `tail-color` overrides) split the route at the marker's interpolated position per frame; default full ignores the split; validation warns on partial wiring.
|
|
50
50
|
|
|
51
51
|
## Decision rule for annotations
|
|
52
52
|
|
|
@@ -56,6 +56,8 @@ UI panel (legend, chart, stats) → `<om-widget>`. Rich HTML at one map location
|
|
|
56
56
|
|
|
57
57
|
In a React codebase, do NOT render om-* elements from JSX (React and the library would contend over the same DOM). Use the first-party adapter instead: `import { OmMap, OmLayer, OmWidget, OmOverlay, useOmMap } from "@nika-js/onlymap/react"` — camelCase deck.gl props, accessors as plain JS functions (`getFillColor={d => ...}`, no expression language), interactions as `onClick`/`onHover` handlers, widget state via the `useOmMap(watchTokens)` hook (tearing-safe: it rides `useSyncExternalStore` over the controller's per-token stores). To sync map state into Redux/MobX/Zustand/Jotai, use `controller.getStore(token)` — a framework-free `{subscribe, getSnapshot}` per watch token with cached plain-data snapshots and `origin: "user"|"programmatic"` tagging for echo-free two-way camera binding; ~20-line recipes: [docs/external-stores.md](docs/external-stores.md). Guide: [docs/react.md](docs/react.md).
|
|
58
58
|
|
|
59
|
+
If the target is an Expo/React Native MOBILE app, use the separate `@nika-js/onlymap-native` package instead of this one: same layer vocabulary, but accessors are OnlyMap expression STRINGS (`getPosition="[$lon, $lat]"` — functions cannot cross its JSON bridge), descriptors are plain JSON, and native UI goes beside the map (no om-* elements, no `<OmWidget>`/`<OmOverlay>` components). That package ships its own `llms.txt` and skill; follow those for native work.
|
|
60
|
+
|
|
59
61
|
## Docs
|
|
60
62
|
|
|
61
63
|
- [README](README.md): thesis, authoring overview, build/run commands
|
package/onlymapjs.html-data.json
CHANGED
|
@@ -457,6 +457,22 @@
|
|
|
457
457
|
"name": "filter-category-fields",
|
|
458
458
|
"description": "Multi-dimension categorical GPU filter (up to 4) — JSON array of {field, categories} objects, e.g. '[{\"field\":\"fuel\",\"categories\":[\"Coal\",\"Gas\"]}]'. Wins over filter-category/filter-categories if both are set."
|
|
459
459
|
},
|
|
460
|
+
{
|
|
461
|
+
"name": "classify-by",
|
|
462
|
+
"description": "Classified symbology: the numeric field to compute class breaks from (quantile/equal-interval/jenks). Installs a fill-color accessor + auto legend; an authored get-fill-color/color wins."
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"name": "classify-scale",
|
|
466
|
+
"description": "Classification mode: quantile (default), equal-interval, or jenks."
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"name": "classify-classes",
|
|
470
|
+
"description": "Number of classes, 2–12 (default 5)."
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"name": "classify-ramp",
|
|
474
|
+
"description": "Named color ramp for classification: viridis (default), plasma, inferno, magma, cividis, turbo, blues, greens, oranges, purples, reds, ylorrd, rdbu, spectral."
|
|
475
|
+
},
|
|
460
476
|
{
|
|
461
477
|
"name": "dash",
|
|
462
478
|
"description": "Dashed line pattern \"[dashLength, gapLength]\" (or \"6 3\") in line-width units. PathLayer/GeoJsonLayer/PolygonLayer/TripsLayer."
|
|
@@ -1066,6 +1082,18 @@
|
|
|
1066
1082
|
"name": "casing-color",
|
|
1067
1083
|
"description": "deck.gl casingColor."
|
|
1068
1084
|
},
|
|
1085
|
+
{
|
|
1086
|
+
"name": "tail",
|
|
1087
|
+
"description": "deck.gl tail."
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
"name": "tail-color",
|
|
1091
|
+
"description": "deck.gl tailColor."
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
"name": "progress-from",
|
|
1095
|
+
"description": "deck.gl progressFrom."
|
|
1096
|
+
},
|
|
1069
1097
|
{
|
|
1070
1098
|
"name": "follow",
|
|
1071
1099
|
"description": "deck.gl follow."
|
|
@@ -1078,6 +1106,10 @@
|
|
|
1078
1106
|
"name": "size",
|
|
1079
1107
|
"description": "deck.gl size."
|
|
1080
1108
|
},
|
|
1109
|
+
{
|
|
1110
|
+
"name": "icon",
|
|
1111
|
+
"description": "deck.gl icon."
|
|
1112
|
+
},
|
|
1081
1113
|
{
|
|
1082
1114
|
"name": "interpolate-ms",
|
|
1083
1115
|
"description": "deck.gl interpolateMs."
|
|
@@ -1953,6 +1985,9 @@
|
|
|
1953
1985
|
{
|
|
1954
1986
|
"name": "widgets-toggle"
|
|
1955
1987
|
},
|
|
1988
|
+
{
|
|
1989
|
+
"name": "time-slider"
|
|
1990
|
+
},
|
|
1956
1991
|
{
|
|
1957
1992
|
"name": "ifc-browser"
|
|
1958
1993
|
},
|
|
@@ -2050,7 +2085,19 @@
|
|
|
2050
2085
|
},
|
|
2051
2086
|
{
|
|
2052
2087
|
"name": "field",
|
|
2053
|
-
"description": "Data field (filter / vega-lite widgets)."
|
|
2088
|
+
"description": "Data field (filter / vega-lite / time-slider widgets)."
|
|
2089
|
+
},
|
|
2090
|
+
{
|
|
2091
|
+
"name": "window",
|
|
2092
|
+
"description": "time-slider widget: sliding-window span in the field's own units (ms for epoch fields). Absent = cumulative reveal from the domain start."
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
"name": "duration",
|
|
2096
|
+
"description": "time-slider widget: wall time of one full sweep, e.g. \"20s\" (default). Player-widget-free playback over a layer's time field."
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
"name": "loop",
|
|
2100
|
+
"description": "time-slider widget: restart the sweep from the domain start when it completes."
|
|
2054
2101
|
},
|
|
2055
2102
|
{
|
|
2056
2103
|
"name": "on",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nika-js/onlymap",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.13",
|
|
4
4
|
"description": "Declarative deck.gl maps for HTML and React — interactive WebGL mapping with GeoJSON/CSV/Arrow data, MapLibre basemaps, widgets, popups, and live streams from a custom-element manifest or typed React components. TypeScript, no build step.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"publishConfig": {
|
|
@@ -111,7 +111,8 @@
|
|
|
111
111
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
112
112
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.e2e.json --noEmit && tsc -p cloud/workers/telemetry/tsconfig.json && tsc -p cloud/workers/examples/tsconfig.json",
|
|
113
113
|
"test": "vitest run",
|
|
114
|
-
"prepublishOnly": "npm run build",
|
|
114
|
+
"prepublishOnly": "node dev/tools/announce-release.mjs --check && npm run build",
|
|
115
|
+
"postpublish": "node dev/tools/announce-release.mjs",
|
|
115
116
|
"try": "vite-node dev/expr-repl.mjs",
|
|
116
117
|
"test:e2e": "playwright test",
|
|
117
118
|
"check-layout": "playwright test e2e/layout-audit.spec.ts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: onlymapjs
|
|
3
|
-
description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), a responsive/mobile map whose controls auto-fold on narrow screens, auditing a map's widget layout with the check-layout tool, syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), BIM/IFC models (loading .ifc files in the browser, 3D Tiles per-element picking, isolate/hide/ghost, clash detection, model federation), routes and directions (a styled A-to-B route line, OSRM or another routing engine, click-to-route), live vehicle/rider/delivery tracking (a moving marker gliding between GPS fixes with a follow camera), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
|
|
3
|
+
description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), a responsive/mobile map whose controls auto-fold on narrow screens, auditing a map's widget layout with the check-layout tool, syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), BIM/IFC models (loading .ifc files in the browser, 3D Tiles per-element picking, isolate/hide/ghost, clash detection, model federation), routes and directions (a styled A-to-B route line, OSRM or another routing engine, click-to-route), live vehicle/rider/delivery tracking (a moving marker gliding between GPS fixes with a follow camera), a React Native or Expo mobile map (route to the separate @nika-js/onlymap-native package), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# OnlyMapJS
|
|
@@ -23,7 +23,7 @@ Use OnlyMapJS as a declarative HTML map library. Write custom elements such as `
|
|
|
23
23
|
</script>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
For no-build CDN pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.6.
|
|
26
|
+
For no-build CDN pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.6.13` (the bare package URL serves `dist/onlymap.standalone.js`) — plus `<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.6.13/dist/onlymapjs.css">`. Never a rebundling CDN (esm.sh, skypack): re-bundling duplicates the deck.gl/luma.gl runtime and every layer fails shader compilation.
|
|
27
27
|
|
|
28
28
|
## React Projects
|
|
29
29
|
|
|
@@ -57,15 +57,16 @@ Load the smallest reference needed for the task:
|
|
|
57
57
|
- Accessor values are expressions: `get-position="[$lon, $lat]"`.
|
|
58
58
|
- `scale()` always needs an explicit `domain=`.
|
|
59
59
|
- Format epoch-millisecond or ISO fields with the safe `formatDate()` built-in, e.g. `get-text="formatDate($time, 'datetime', 'UTC')"`. Do not use `new Date()`, `Intl`, or method calls in restricted expressions.
|
|
60
|
-
- For a built-in filter over epoch milliseconds, add `format="date"` with optional `date-style="date|datetime|time|iso"` and `time-zone="UTC|local|<IANA zone>"`; do not hand-roll a time slider only to format its labels.
|
|
60
|
+
- For a built-in filter over epoch milliseconds, add `format="date"` with optional `date-style="date|datetime|time|iso"` and `time-zone="UTC|local|<IANA zone>"`; do not hand-roll a time slider only to format its labels. For temporal PLAYBACK (play/pause/scrub over a time field, cumulative or a sliding `window`), use `<om-widget type="time-slider" layer="…" field="…" duration="20s" loop>` — never hand-roll the playback loop.
|
|
61
61
|
- ScatterplotLayer points need an explicit size — `radius="6" radius-units="pixels"`, `get-radius="..."`, or `radius-min-pixels="..."`: deck's default is 1 METER, sub-pixel at city zooms, and validation warns on layers with no radius source.
|
|
62
|
-
- Prefer canonical color expressions — a `sequential`/`diverging`/`threshold` `scale()` or an equality ternary chain — over hand-rolled arithmetic: the legend widget parses these shapes and renders a matching gradient ramp / class ranges / category palette automatically.
|
|
62
|
+
- Prefer canonical color expressions — a `sequential`/`diverging`/`threshold` `scale()` or an equality ternary chain — over hand-rolled arithmetic: the legend widget parses these shapes and renders a matching gradient ramp / class ranges / category palette automatically. When the user wants graduated/choropleth classes WITHOUT hand-picking domains, use `classify-by="<field>" classify-scale="quantile|equal-interval|jenks" classify-classes="5" classify-ramp="viridis"` instead — breaks are computed from the data and the classes legend is automatic (an authored get-fill-color/color wins).
|
|
63
63
|
- Inline handlers such as `onclick` are wrong. Use `data-emit`, `<om-behavior>`, or widget scripts.
|
|
64
64
|
- Full JavaScript accessor blocks require the `js` attribute on `<om-layer>`.
|
|
65
65
|
- Do not put secrets in markup. Use `OmMap.configureData({ headers, credentials, fetch })`.
|
|
66
66
|
|
|
67
67
|
## Authoring Decisions
|
|
68
68
|
|
|
69
|
+
- Target is an Expo/React Native MOBILE app -> the separate `@nika-js/onlymap-native` package, not this one: same layer vocabulary, but accessors are OnlyMap expression STRINGS (`getPosition="[$lon, $lat]"` — functions cannot cross its JSON bridge), descriptors are plain JSON, no om-* elements and no widget/overlay components (build native UI beside the map). It ships its own llms.txt and skill; follow those for native work.
|
|
69
70
|
- UI panel, control, chart, legend, stats, filter, or draw toolbar -> `<om-widget>`.
|
|
70
71
|
- Sparse rich HTML at one geographic location -> `<om-overlay>`.
|
|
71
72
|
- Many labels/badges -> `<om-layer type="PopupLayer">`.
|
|
@@ -82,7 +83,7 @@ Load the smallest reference needed for the task:
|
|
|
82
83
|
- Geotagged drone JPEG -> `<om-layer type="ImageOverlay" src="…jpg" georeference="exif">`; for saved/collaborative maps persist the processed image and reconstruct with explicit `bounds` (see syntax.md; this is visualization-grade, not orthorectification).
|
|
83
84
|
- Dashed line/route/boundary (or any dashed stroke) -> the `dash` attribute on a path-stroking layer: `dash="[6, 3]"` (or SVG-style `dash="6 3"`, + optional `dash-justified`) on `PathLayer`/`GeoJsonLayer`/`PolygonLayer`/`TripsLayer`. `[dashLength, gapLength]` in line-width units. Do NOT hand-wire deck's `PathStyleExtension`/`getDashArray` — the attribute mounts it; `dash` on a non-path layer is ignored with a warning.
|
|
84
85
|
- Draw an A-to-B route/trip line with styling (casing + line + origin/destination markers) -> `<om-layer type="Route" geometry='{"type":"LineString","coordinates":[[lng,lat],...]}'>` if you already have the path; `origin="[lng,lat]" destination="[lng,lat]"` (+ `provider`, default `"nika"`) to resolve one via a registered `RoutingProvider` instead (`OmMap.registerRoutingProvider`). `follow="fit-route"` auto-fits the camera once it resolves. Do NOT hand-roll two `PathLayer`s for casing/line — the attribute gives you both plus waypoint pins.
|
|
85
|
-
- Show one moving entity (a vehicle, a rider, a live position feed) with rotation and smooth movement between updates -> `<om-layer type="Tracking" get-position="[$lng,$lat]">`. Position data is just `data`/`source` like any layer — no separate subscription API. `bearing-field` (default `"bearing"`) names the field to rotate the icon by; `interpolate-ms` (default `1000`) is how long it glides between two fixes instead of jumping; `follow="follow"` eases the camera along with it. v1 renders ONE entity per layer — for a fleet, one `Tracking` layer per vehicle.
|
|
86
|
+
- Show one moving entity (a vehicle, a rider, a live position feed) with rotation and smooth movement between updates -> `<om-layer type="Tracking" get-position="[$lng,$lat]">`. Position data is just `data`/`source` like any layer — no separate subscription API. `bearing-field` (default `"bearing"`) names the field to rotate the icon by; `interpolate-ms` (default `1000`) is how long it glides between two fixes instead of jumping; `follow="follow"` eases the camera along with it. v1 renders ONE entity per layer — for a fleet, one `Tracking` layer per vehicle (glides are keyed per layer, so several animate independently). Client-facing trip page -> pair with a `Route` linked via `progress-from` + `tail="none"` (only the remaining trip renders); dispatcher/operator view -> `tail="dim"` (darkened traveled trail).
|
|
86
87
|
- CityJSON/CityJSONSeq per-face surfaces mode (`?om-surfaces=1`) -> always pair the `SolidPolygonLayer` with a companion `<om-layer type="PathLayer">` using `get-path="$outline"` to make roof/wall edges visible. This mode is unlit and `SolidPolygonLayer`'s own `wireframe` prop does nothing here (deck only builds wireframe geometry when `extruded: true`); the `PathLayer` is the only way to see face edges. Give it the same `filter-field`/`filter-range` as the fill layer so filtered-out buildings' outlines disappear too. See syntax.md.
|
|
87
88
|
- Live entity updates -> `wss://` stream with `key` and optional `source` decoder.
|
|
88
89
|
- REST snapshot that changes over time -> `refresh="5s"`.
|