@nika-js/onlymap 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.vscode/onlymap.code-snippets +4 -4
  2. package/LICENSE.md +22 -12
  3. package/README.md +54 -11
  4. package/dist/actions.d.ts +2 -2
  5. package/dist/badge.d.ts +4 -0
  6. package/dist/{basemap-Bn4TmZtQ.js → basemap-DrK6zcu8.js} +2337 -2273
  7. package/dist/basemap-registry.d.ts +10 -0
  8. package/dist/basemap.d.ts +23 -0
  9. package/dist/color.d.ts +9 -0
  10. package/dist/ctx.d.ts +10 -1
  11. package/dist/deck.d.ts +28 -0
  12. package/dist/deck.js +16 -0
  13. package/dist/elements/om-map.d.ts +30 -0
  14. package/dist/error-reporting.d.ts +17 -0
  15. package/dist/history.d.ts +53 -0
  16. package/dist/html-data.d.ts +2 -2
  17. package/dist/{index-D2zVsZ79.js → index-C9tgnPNw.js} +1 -1
  18. package/dist/{index-BZs_x9Dx.js → index-CiuGqS0i.js} +2 -2
  19. package/dist/{index-oE1Kouy1.js → index-CsicbycJ.js} +18395 -16371
  20. package/dist/{index-3UyMg0Md.js → index-DXoRERAy.js} +1 -1
  21. package/dist/{index-DSjndBOf.js → index-Ztkd30f8.js} +1 -1
  22. package/dist/index.d.ts +19 -0
  23. package/dist/internal-ids.d.ts +7 -0
  24. package/dist/ir-diff.d.ts +1 -1
  25. package/dist/ir-snapshot.d.ts +2 -0
  26. package/dist/ir.d.ts +7 -0
  27. package/dist/license.d.ts +64 -0
  28. package/dist/onlymapjs.js +54 -32
  29. package/dist/onlymapjs.umd.cjs +426 -342
  30. package/dist/programmatic.d.ts +35 -0
  31. package/dist/react/om-layer.d.ts +2 -0
  32. package/dist/react.js +18 -16
  33. package/dist/runtime-core.d.ts +79 -1
  34. package/dist/scene-lighting.d.ts +83 -0
  35. package/dist/selection.d.ts +1 -1
  36. package/dist/snapshot.d.ts +14 -0
  37. package/dist/telemetry-schema.d.ts +53 -0
  38. package/dist/telemetry.d.ts +60 -0
  39. package/dist/terrain.d.ts +113 -0
  40. package/dist/version.d.ts +8 -0
  41. package/docs/custom-layers.md +99 -0
  42. package/docs/react.md +1 -0
  43. package/docs/telemetry.md +78 -0
  44. package/docs/testing.md +2 -2
  45. package/llms.txt +6 -5
  46. package/onlymapjs.html-data.json +172 -29
  47. package/package.json +25 -5
  48. package/skills/onlymapjs/SKILL.md +4 -1
  49. package/skills/onlymapjs/references/syntax.md +32 -1
  50. package/skills/onlymapjs/references/testing.md +2 -0
@@ -0,0 +1,99 @@
1
+ # Registering an external layer class
2
+
3
+ `OmMap.registerLayer` makes any deck.gl layer class a first-class manifest
4
+ type: validation, IntelliSense generation, and attribute resolution all read
5
+ the same schema the built-ins use. This page is the recipe for bringing a
6
+ layer class the library doesn't bundle — a community layer, a company shim, a
7
+ composite that wraps a whole data pipeline.
8
+
9
+ ## Rule 1: build on `@nika-js/onlymap/deck`, never on your own deck.gl
10
+
11
+ OnlyMapJS bundles deck.gl (it has zero runtime dependencies). If your class
12
+ extends a `@deck.gl/*` copy you installed yourself, it belongs to a
13
+ **different class hierarchy** — two `Layer` base classes, two luma.gl
14
+ runtimes — and fails inside the bundled renderer. The `deck` subpath
15
+ re-exports the bundled classes, so your shim extends the exact objects the
16
+ core renders with:
17
+
18
+ ```js
19
+ import { CompositeLayer, TileLayer, BitmapLayer } from "@nika-js/onlymap/deck";
20
+ ```
21
+
22
+ Available: `Layer`, `CompositeLayer`, `LayerExtension`, `WebMercatorViewport`,
23
+ `COORDINATE_SYSTEM`, `GeoJsonLayer`, `ScatterplotLayer`, `IconLayer`,
24
+ `BitmapLayer`, `TileLayer`, `Tile3DLayer`, `SimpleMeshLayer`,
25
+ `ScenegraphLayer` — plus authoring types (`LayerProps`, `DefaultProps`,
26
+ `UpdateParameters`, `PickingInfo`, `LayersList`; typechecking against them
27
+ needs `@deck.gl/core` as a dev dependency).
28
+
29
+ ## Rule 2: functions ride `static defaultProps`, not attributes
30
+
31
+ HTML attributes are strings — function-valued props (`renderSubLayers`,
32
+ `getTileData`, load callbacks) are inexpressible by design. Put them on a
33
+ thin subclass; deck.gl's own defaultProps merge delivers them to every
34
+ instance:
35
+
36
+ ```js
37
+ class CogLayer extends CompositeLayer {
38
+ static layerName = "CogLayer";
39
+ static defaultProps = {
40
+ renderTile: { type: "function", value: renderTile },
41
+ onRasterLoad: { type: "function", value: () => {} },
42
+ };
43
+ renderLayers() {
44
+ /* compose TileLayer/BitmapLayer from the subpath here */
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Rule 3: the schema wires attributes to deck props
50
+
51
+ ```js
52
+ import { OmMap } from "@nika-js/onlymap";
53
+
54
+ OmMap.registerLayer({
55
+ type: "CogLayer",
56
+ deckClass: CogLayer,
57
+ props: [
58
+ // deck's URL prop is `data`, but OnlyMapJS reserves the `data` attribute
59
+ // for its own loader — alias it, and the layer class fetches for itself
60
+ // (the built-in Tile3DLayer `tileset` attribute uses the same trick).
61
+ { attr: "src", kind: "scalar", deckProp: "data", type: "string", required: true },
62
+ // JSON attributes and dot-path descriptors compose: the JSON sets the
63
+ // object, later dot-paths merge into it.
64
+ { attr: "load-options", kind: "scalar", deckProp: "loadOptions", type: "json" },
65
+ { attr: "max-error", kind: "scalar", deckProp: "loadOptions.cog.maxError", type: "number" },
66
+ { attr: "opacity", kind: "scalar", deckProp: "opacity", type: "number", default: 1 },
67
+ { attr: "visible", kind: "scalar", deckProp: "visible", type: "boolean", default: true },
68
+ // accessor-kind props get the full expression language:
69
+ // { attr: "get-color", kind: "accessor", deckProp: "getColor" },
70
+ ],
71
+ });
72
+ ```
73
+
74
+ Then the manifest just works:
75
+
76
+ ```html
77
+ <om-layer id="rast" type="CogLayer" src="https://example.com/landcover.tif"
78
+ max-error="16" opacity="0.9"></om-layer>
79
+ ```
80
+
81
+ ## Rule 4: register before the manifest mounts
82
+
83
+ Registration after mount does not retrigger reconciles — an unknown-type
84
+ layer is warn-skipped until the next DOM mutation. Call `registerLayer` at
85
+ module top level (the `registerSource`/`registerFormat` convention), before
86
+ the `<om-map>` connects.
87
+
88
+ ## Verifying without a GPU
89
+
90
+ `OmMap.snapshotIR(html)` resolves your registered type through the same
91
+ pipeline the live reconciler runs — assert the aliased URL lands on
92
+ `props.data`, dot-paths nest, functions appear as `[function]` — in plain
93
+ jsdom/happy-dom, no WebGL.
94
+
95
+ ## The programmatic alternative
96
+
97
+ On the `MapController` front-end (and the React adapter), `props` passes
98
+ function values directly — no subclass needed. The subclass recipe exists so
99
+ the **manifest** front-end can express what attributes can't.
package/docs/react.md CHANGED
@@ -116,5 +116,6 @@ React ≥ 18 is an optional peer dependency — it's only loaded if you import `
116
116
 
117
117
  - **Stories** as React components (a `<Story>`/timeline hook is on the roadmap) — an HTML `<om-story>` needs the HTML front-end.
118
118
  - **The draw widget** — HTML front-end only for now.
119
+ - **Undo/redo** — manifest history is a DOM-front-end feature; in React your state (and its undo) belongs to the app. `ctx.history` reads `{ canUndo: false, canRedo: false }` here.
119
120
  - Per-feature `trace` (it animates via runtime manifest elements) — whole-layer `trace` on a TripsLayer works.
120
121
  - A `scale()` helper mirroring the expression language — use `d3-scale` or plain functions.
@@ -0,0 +1,78 @@
1
+ # Telemetry
2
+
3
+ OnlyMapJS reports **one usage snapshot per map, per page load** — sent when a map reaches `ready` — and, separately, **errors caused by the library's own code**. This page documents exactly what is (and is not) collected, and how to turn it off.
4
+
5
+ > **Status: active.** Reports go to the first-party endpoint `https://om-api.nika.eco/v1/t` (never to a third-party domain from your pages). Disclosure lives in the license (LICENSE.md §11). Both opt-outs below are always honored.
6
+
7
+ ## What a snapshot contains
8
+
9
+ The payload is **deployment-scoped**: it describes the page's use of the library, never the visitor.
10
+
11
+ ```jsonc
12
+ {
13
+ "event": "map_ready",
14
+ "pageLoadId": "…", // random UUID per page load — dedups retries; dies with the page
15
+ "mapId": "…" | null, // the authored map-id attribute (see below)
16
+ "version": "0.2.3",
17
+ "plan": "free", "keyId": null,
18
+ "origin": "dashboard.example.com", // hostname ONLY — never the path or query
19
+ "frontend": "html", // html | react | programmatic
20
+ "renderer": "maplibre" | "standalone",
21
+ "dev": false, // true on localhost / *.local
22
+ "layers": [ { "type": "ScatterplotLayer", "rows": 1200, "streaming": false, "refresh": false } ],
23
+ "story": { "steps": 7 } | null,
24
+ "widgets": ["legend", "basemap-switcher"], // widget types only
25
+ "draw": false, "undoRedo": false
26
+ }
27
+ ```
28
+
29
+ What is **never** collected: page paths or URLs, your data or its contents, coordinates, IP addresses in the payload, cookies, or any persistent visitor identifier. `pageLoadId` is regenerated on every page load and cannot link visits. Snapshots are also never sent from `headless` maps, so test suites stay silent.
30
+
31
+ ## Library-error reporting
32
+
33
+ Unexpected exceptions **from the library's own code** — never your page scripts, never other libraries (the stack must point into OnlyMapJS's own bundle), and never manifest/validation mistakes (those are surfaced to you in the dev error panel instead) — are reported so bugs get fixed:
34
+
35
+ ```jsonc
36
+ {
37
+ "event": "library_error",
38
+ "pageLoadId": "…", "version": "0.2.3",
39
+ "origin": "dashboard.example.com", // hostname only
40
+ "dev": false,
41
+ "signature": "…", // hash of message + top frame — the grouping key
42
+ "message": "…", // truncated; query strings stripped
43
+ "frames": ["…"], // top library-code stack lines; query strings stripped
44
+ "ua": "…" // browser user-agent string
45
+ }
46
+ ```
47
+
48
+ Stacks are scrubbed before sending: query strings are stripped from every URL (they can carry keys), and no manifest content, layer data, or accessor source is ever included. At most one report per distinct error per page load, capped at five per page. The same opt-outs below disable error reporting — one switch, no fine print.
49
+
50
+ ## `map-id` — identifying the map, not the visitor
51
+
52
+ An optional authored attribute:
53
+
54
+ ```html
55
+ <om-map map-id="0f2c6a1e-88f7-4c3e-9d41-7b1f3f9f2ab7" ...>
56
+ ```
57
+
58
+ It identifies the **map artifact** — the same id on every visit by every visitor — so usage can distinguish "one popular dashboard" from "many different maps". It is the same category as an analytics measurement id in page source: author-controlled, nothing stored on the visitor's device. **Opt out by deleting or changing it**; it is never required, and validation never asks for it. The VS Code `!map` snippet generates one automatically.
59
+
60
+ ## Opting out
61
+
62
+ ```ts
63
+ OmMap.configureTelemetry({ disabled: true }); // global — kills usage snapshots AND error reports
64
+ ```
65
+
66
+ ```html
67
+ <om-map telemetry="off" ...> <!-- per map -->
68
+ ```
69
+
70
+ Clearing the endpoint (`OmMap.configureTelemetry({ endpoint: undefined })`) is a third, equivalent switch: no endpoint, no network.
71
+
72
+ ## The binding rules
73
+
74
+ These are design constraints, not promises of restraint:
75
+
76
+ 1. **Telemetry never affects function.** Nothing in the library waits on, retries, or reacts to a report; sends are fire-and-forget (`navigator.sendBeacon`) and silent on failure. Error reporting can never itself throw into your page.
77
+ 2. **No endpoint, no network.** The send layer is a no-op when the endpoint is cleared.
78
+ 3. **Dev-context geography only.** Only `dev: true` reports (a developer's own localhost) are GeoIP-resolved at the server — to a country code, with the IP discarded unwritten. Production reports are never GeoIP'd and visitor IPs are never stored.
package/docs/testing.md CHANGED
@@ -122,8 +122,8 @@ h = await mountForTest(PAGE); // resolves after the mocked fetch
122
122
 
123
123
  A *failing* fetch also settles readiness (the layer is just empty) — `mountForTest` never hangs on a bad URL.
124
124
 
125
- **What's real at this tier:** validation, accessor execution, `ctx.stats`/`data`/`dataInViewport`, declarative + viewport filtering, behaviors → actions, overlay anchoring/culling/interpolation (real Mercator math), widget reactivity, XSS escaping, columnar row materialization.
126
- **What isn't:** pixels, GPU attribute recompute, basemap compositing, and CDN-loaded widgets (`vega-lite` is browser-only — assert its *data* here via `ctx.stats`, its rendering at tier 3 if at all).
125
+ **What's real at this tier:** validation, accessor execution, `ctx.stats`/`data`/`dataInViewport`, declarative + viewport filtering, behaviors → actions, overlay anchoring/culling/interpolation (real Mercator math), widget reactivity, XSS escaping, columnar row materialization — **and the free-plan license gates** (5 layers / 25k rows per layer), which apply in headless tests exactly as in production so a passing suite can't hide a gated page. If your page legitimately exceeds the free limits, configure your license key in test setup — keys are publishable and verify **offline**, so CI needs no network or secrets vault: `OmMap.configureLicense("om_live_…")`. Gate violations surface as errors on the validation stream (`om-validation-error` / the `validate` attribute), naming the limit.
126
+ **What isn't:** pixels, GPU attribute recompute, basemap compositing, and CDN-loaded widgets (`vega-lite` is browser-only — assert its *data* here via `ctx.stats`, its rendering at tier 3 if at all). Telemetry is also silent here — headless maps never report.
127
127
 
128
128
  ## Tier 3 — visual: Playwright
129
129
 
package/llms.txt CHANGED
@@ -16,14 +16,15 @@ OnlyMapJS is NOT raw deck.gl and NOT generic HTML/JSX. The rules below are the d
16
16
 
17
17
  ## Element vocabulary
18
18
 
19
- - `<om-map center="[lng, lat]" zoom="11" pitch="55" bearing="20" basemap="positron">` — the root. `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), 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). `validate` attribute enables live validation + on-page error panel.
20
- - `<om-layer id="..." type="ScatterplotLayer" data="./points.json">` — any deck.gl layer class by `type` (all 33 bundled), plus `PopupLayer` (WebGL badges/labels at scale: `layout="badge|pin-label|card"`, `min-zoom`/`max-zoom`). Data: `data` URL (JSON, GeoJSON, CSV/TSV `.csv` — parsed to typed columns — Shapefile `.shp` (+`.dbf` attributes) and KML `.kml` as GeoJSON features, 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})`), inline `<script type="application/json">` (row arrays or column-oriented `{"columns": {"lon": [...], "lat": [...]}}`), 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). 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.
21
- - `<om-widget type="legend|layer-switcher|basemap-switcher|zoom-controls|scale-bar|attribution|filter|vega-lite" position="bottom-right">` — static UI panels. No `type` + HTML + `<script type="om/widget">` = custom widget with `ctx` (`ctx.layers`, `ctx.data(id)`, `ctx.dataInViewport(id)`, `ctx.stats(id, field)`, `ctx.viewport`, `ctx.selection`, `ctx.emit(action, payload)`), `this.watch = ['data:<layerId>', 'viewport', 'selection', 'layers']`, `this.$(sel)`, `vegaEmbed`/`d3` as globals.
19
+ - `<om-map center="[lng, lat]" zoom="11" pitch="55" bearing="20" basemap="positron">` — the root. `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 `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. 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`. GeoJsonLayer extrudes polygons declaratively: `extruded get-elevation="$height"` (+ `wireframe`). `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: 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}` — the camera-persistence hook). `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.
20
+ - `<om-layer id="..." type="ScatterplotLayer" data="./points.json">` — any deck.gl layer class by `type` (all 33 bundled), plus `PopupLayer` (WebGL badges/labels at scale: `layout="badge|pin-label|card"`, `min-zoom`/`max-zoom`). 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, 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": [...]}}`), 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). 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.
21
+ - `<om-widget type="legend|layer-switcher|basemap-switcher|lighting|zoom-controls|undo-redo|scale-bar|attribution|filter|vega-lite" position="bottom-right">` — static UI panels. No `type` + HTML + `<script type="om/widget">` = custom widget with `ctx` (`ctx.layers`, `ctx.data(id)`, `ctx.dataInViewport(id)`, `ctx.stats(id, field)`, `ctx.viewport`, `ctx.selection`, `ctx.emit(action, payload)`), `this.watch = ['data:<layerId>', 'viewport', 'selection', 'layers', 'history', 'basemap']` (`layers` also fires on visibility/filter changes; `basemap` on basemap switches; `history` on undo/redo availability), `this.$(sel)`, `vegaEmbed`/`d3` as globals.
22
22
  - `<om-overlay id="..." anchor-from="selection">` — rich geo-anchored HTML (≤ ~20 per map). Anchors: `anchor="[lng, lat]"` (static), `anchor-from="selection"` (follows picks), or `anchor-layer="regions" anchor-feature-id="mission"` (anchored to a feature's own geometry — bbox center — no coordinates in markup; `{{field}}` interpolates that feature's attributes). `{{field}}` interpolates the picked feature HTML-escaped; `{{{field}}}` is raw (avoid). For labels on many features use `PopupLayer`, not overlays.
23
- - `<om-behavior on="click|hover|drag|load|data-loaded" layer="..." action="...">` — declarative interaction. Built-in actions: `show-overlay`, `hide-overlay`, `show-tooltip`, `hide-tooltip`, `toggle-layer`, `filter-layer`, `highlight-feature`, `zoom-to-feature`, `set-basemap`. One payload contract everywhere: `{ layer, target, feature, featureId, coordinate }`.
23
+ - `<om-behavior on="click|hover|drag|load|data-loaded" layer="..." action="...">` — declarative interaction. Built-in actions: `show-overlay`, `hide-overlay`, `show-tooltip`, `hide-tooltip`, `toggle-layer`, `filter-layer`, `highlight-feature`, `zoom-to-feature`, `set-basemap`, `undo`, `redo`. One payload contract everywhere: `{ layer, target, feature, featureId, coordinate }`.
24
+ - Undo/redo is built in: user-facing manifest changes (layer toggles, filter changes, basemap switches, element add/remove, drawn sketches) are recorded automatically — the manifest is the state. `<om-widget type="undo-redo">` renders the buttons; Cmd/Ctrl-Z, Shift-Cmd/Ctrl-Z, and Ctrl-Y work on any map (text inputs keep their native undo). Camera moves, hover effects, and story playback are deliberately NOT undo steps. Widget scripts: `ctx.history.canUndo/canRedo` with watch token `history`; `ctx.emit("undo")`/`ctx.emit("redo")`.
24
25
  - `<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.
25
26
  - 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`.
26
- - `<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. 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).
27
+ - `<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).
27
28
  - 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="...">`.
28
29
 
29
30
  ## Decision rule for annotations
@@ -73,10 +73,108 @@
73
73
  }
74
74
  ]
75
75
  },
76
+ {
77
+ "name": "lighting",
78
+ "description": "Scene lighting preset for 3D content — seeds ambient/sun/camera values that lighting-* attributes override. Absent = deck.gl default lights.",
79
+ "values": [
80
+ {
81
+ "name": "daylight"
82
+ },
83
+ {
84
+ "name": "studio"
85
+ },
86
+ {
87
+ "name": "flat"
88
+ },
89
+ {
90
+ "name": "custom"
91
+ }
92
+ ]
93
+ },
94
+ {
95
+ "name": "lighting-ambient",
96
+ "description": "Ambient light intensity (always-on fill), e.g. 0.9."
97
+ },
98
+ {
99
+ "name": "lighting-sun",
100
+ "description": "Sun (directional light) intensity; 0 removes the sun."
101
+ },
102
+ {
103
+ "name": "lighting-sun-azimuth",
104
+ "description": "Sun compass bearing, degrees clockwise from north [0, 360)."
105
+ },
106
+ {
107
+ "name": "lighting-sun-elevation",
108
+ "description": "Sun height above the horizon, degrees [0, 90]."
109
+ },
110
+ {
111
+ "name": "lighting-sun-date",
112
+ "description": "ISO 8601 or epoch ms — computes the sun's azimuth/elevation from solar position at the map center (shadow studies). Overrides azimuth/elevation."
113
+ },
114
+ {
115
+ "name": "lighting-camera",
116
+ "description": "Camera-following fill light intensity (model inspection); 0 removes it."
117
+ },
118
+ {
119
+ "name": "terrain",
120
+ "description": "3D elevation surface: a registered terrain preset, a {z}/{x}/{y} DEM URL (needs terrain-decoder), or \"off\". Replaces an active basemap while on. Geographic layers drape by default (per-layer terrain attribute overrides).",
121
+ "values": [
122
+ {
123
+ "name": "off"
124
+ },
125
+ {
126
+ "name": "terrarium"
127
+ },
128
+ {
129
+ "name": "maptiler-terrain"
130
+ }
131
+ ]
132
+ },
133
+ {
134
+ "name": "terrain-decoder",
135
+ "description": "DEM RGB decoder: \"terrarium\", \"mapbox-rgb\", or {rScaler,gScaler,bScaler,offset} JSON. Required for raw DEM URLs; presets carry their own.",
136
+ "values": [
137
+ {
138
+ "name": "terrarium"
139
+ },
140
+ {
141
+ "name": "mapbox-rgb"
142
+ }
143
+ ]
144
+ },
145
+ {
146
+ "name": "terrain-exaggeration",
147
+ "description": "Vertical relief multiplier, applied in the DEM decoder; 1 = true relief."
148
+ },
149
+ {
150
+ "name": "terrain-max-zoom",
151
+ "description": "DEM tileset zoom cap — the provider's REAL limit (a too-high cap requests 404 tiles and blanks the terrain)."
152
+ },
153
+ {
154
+ "name": "terrain-texture",
155
+ "description": "Optional {z}/{x}/{y} imagery template draped over the surface (satellite, etc.)."
156
+ },
76
157
  {
77
158
  "name": "validate",
78
159
  "description": "Run manifest validation and show the on-page error panel."
79
160
  },
161
+ {
162
+ "name": "map-id",
163
+ "description": "Optional authored UUID identifying this map artifact (telemetry dedup across visits/pages — not a visitor id). Opt-out: delete it."
164
+ },
165
+ {
166
+ "name": "license-key",
167
+ "description": "OnlyMapJS license token (om_live_…) — lifts the free-tier limits (5 layers / 25k rows per layer) and removes the badge. Publishable, origin-restricted; or call OmMap.configureLicense once."
168
+ },
169
+ {
170
+ "name": "telemetry",
171
+ "description": "Set \"off\" to disable usage telemetry for this map (global opt-out: OmMap.configureTelemetry).",
172
+ "values": [
173
+ {
174
+ "name": "off"
175
+ }
176
+ ]
177
+ },
80
178
  {
81
179
  "name": "headless",
82
180
  "description": "No renderer — parsing/IR only (tests, SSR)."
@@ -270,6 +368,21 @@
270
368
  "name": "highlighted-id",
271
369
  "description": "Feature id to highlight (set by the highlight-feature action)."
272
370
  },
371
+ {
372
+ "name": "terrain",
373
+ "description": "Behavior under an active map terrain: drape onto the surface, offset (sit on it — the 3D-model default), or off. Absent = the type default.",
374
+ "values": [
375
+ {
376
+ "name": "drape"
377
+ },
378
+ {
379
+ "name": "offset"
380
+ },
381
+ {
382
+ "name": "off"
383
+ }
384
+ ]
385
+ },
273
386
  {
274
387
  "name": "key",
275
388
  "description": "Stream entity identity field — messages upsert by this key (wss data)."
@@ -330,6 +443,34 @@
330
443
  "name": "get-line-width",
331
444
  "description": "Accessor for deck.gl getLineWidth — expression language: $field, scale(), arithmetic."
332
445
  },
446
+ {
447
+ "name": "get-point-radius",
448
+ "description": "Accessor for deck.gl getPointRadius — expression language: $field, scale(), arithmetic."
449
+ },
450
+ {
451
+ "name": "point-radius-units",
452
+ "description": "deck.gl pointRadiusUnits."
453
+ },
454
+ {
455
+ "name": "point-radius-min-pixels",
456
+ "description": "deck.gl pointRadiusMinPixels."
457
+ },
458
+ {
459
+ "name": "line-width-units",
460
+ "description": "deck.gl lineWidthUnits."
461
+ },
462
+ {
463
+ "name": "get-elevation",
464
+ "description": "Accessor for deck.gl getElevation — expression language: $field, scale(), arithmetic."
465
+ },
466
+ {
467
+ "name": "extruded",
468
+ "description": "deck.gl extruded."
469
+ },
470
+ {
471
+ "name": "wireframe",
472
+ "description": "deck.gl wireframe."
473
+ },
333
474
  {
334
475
  "name": "get-text",
335
476
  "description": "Accessor for deck.gl getText — expression language: $field, scale(), arithmetic."
@@ -578,10 +719,6 @@
578
719
  "name": "elevation-scale",
579
720
  "description": "deck.gl elevationScale."
580
721
  },
581
- {
582
- "name": "line-width-units",
583
- "description": "deck.gl lineWidthUnits."
584
- },
585
722
  {
586
723
  "name": "line-width-scale",
587
724
  "description": "deck.gl lineWidthScale."
@@ -590,22 +727,10 @@
590
727
  "name": "line-width-max-pixels",
591
728
  "description": "deck.gl lineWidthMaxPixels."
592
729
  },
593
- {
594
- "name": "extruded",
595
- "description": "deck.gl extruded."
596
- },
597
- {
598
- "name": "wireframe",
599
- "description": "deck.gl wireframe."
600
- },
601
730
  {
602
731
  "name": "flat-shading",
603
732
  "description": "deck.gl flatShading."
604
733
  },
605
- {
606
- "name": "get-elevation",
607
- "description": "Accessor for deck.gl getElevation — expression language: $field, scale(), arithmetic."
608
- },
609
734
  {
610
735
  "name": "material",
611
736
  "description": "deck.gl material."
@@ -850,18 +975,10 @@
850
975
  "name": "point-radius-max-pixels",
851
976
  "description": "deck.gl pointRadiusMaxPixels."
852
977
  },
853
- {
854
- "name": "point-radius-min-pixels",
855
- "description": "deck.gl pointRadiusMinPixels."
856
- },
857
978
  {
858
979
  "name": "point-radius-scale",
859
980
  "description": "deck.gl pointRadiusScale."
860
981
  },
861
- {
862
- "name": "point-radius-units",
863
- "description": "deck.gl pointRadiusUnits."
864
- },
865
982
  {
866
983
  "name": "point-antialiasing",
867
984
  "description": "deck.gl pointAntialiasing."
@@ -870,10 +987,6 @@
870
987
  "name": "point-billboard",
871
988
  "description": "deck.gl pointBillboard."
872
989
  },
873
- {
874
- "name": "get-point-radius",
875
- "description": "Accessor for deck.gl getPointRadius — expression language: $field, scale(), arithmetic."
876
- },
877
990
  {
878
991
  "name": "icon-size-max-pixels",
879
992
  "description": "deck.gl iconSizeMaxPixels."
@@ -1415,6 +1528,18 @@
1415
1528
  },
1416
1529
  {
1417
1530
  "name": "set-basemap"
1531
+ },
1532
+ {
1533
+ "name": "set-lighting"
1534
+ },
1535
+ {
1536
+ "name": "set-terrain"
1537
+ },
1538
+ {
1539
+ "name": "undo"
1540
+ },
1541
+ {
1542
+ "name": "redo"
1418
1543
  }
1419
1544
  ]
1420
1545
  },
@@ -1465,6 +1590,12 @@
1465
1590
  },
1466
1591
  {
1467
1592
  "name": "basemap-switcher"
1593
+ },
1594
+ {
1595
+ "name": "lighting"
1596
+ },
1597
+ {
1598
+ "name": "undo-redo"
1468
1599
  }
1469
1600
  ]
1470
1601
  },
@@ -1488,7 +1619,7 @@
1488
1619
  },
1489
1620
  {
1490
1621
  "name": "watch",
1491
- "description": "Space-separated watch tokens: viewport, selection, layers, data:<layerId>."
1622
+ "description": "Space-separated watch tokens: viewport, selection, layers, history, basemap, lighting, terrain, data:<layerId>."
1492
1623
  },
1493
1624
  {
1494
1625
  "name": "layer",
@@ -1680,6 +1811,18 @@
1680
1811
  },
1681
1812
  {
1682
1813
  "name": "set-basemap"
1814
+ },
1815
+ {
1816
+ "name": "set-lighting"
1817
+ },
1818
+ {
1819
+ "name": "set-terrain"
1820
+ },
1821
+ {
1822
+ "name": "undo"
1823
+ },
1824
+ {
1825
+ "name": "redo"
1683
1826
  }
1684
1827
  ]
1685
1828
  },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nika-js/onlymap",
3
- "version": "0.2.2",
4
- "description": "Interactive WebGL maps from declarative HTML — a custom-element manifest drives deck.gl: rendering, data loading, live updates, picking, widgets, and validation. No build step.",
3
+ "version": "0.3.0",
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": {
7
7
  "access": "public"
@@ -17,16 +17,28 @@
17
17
  },
18
18
  "keywords": [
19
19
  "deck.gl",
20
+ "deckgl",
21
+ "map",
20
22
  "maps",
23
+ "mapping",
21
24
  "webgl",
22
25
  "geospatial",
23
26
  "gis",
27
+ "geojson",
28
+ "react",
29
+ "typescript",
24
30
  "declarative",
25
31
  "custom-elements",
26
32
  "web-components",
27
33
  "maplibre",
34
+ "cartography",
28
35
  "dataviz",
36
+ "data-visualization",
29
37
  "visualization",
38
+ "visualisation",
39
+ "heatmap",
40
+ "layers",
41
+ "geoarrow",
30
42
  "llm",
31
43
  "agent"
32
44
  ],
@@ -50,6 +62,11 @@
50
62
  "import": "./dist/react.js",
51
63
  "default": "./dist/react.js"
52
64
  },
65
+ "./deck": {
66
+ "types": "./dist/deck.d.ts",
67
+ "import": "./dist/deck.js",
68
+ "default": "./dist/deck.js"
69
+ },
53
70
  "./onlymapjs.css": "./dist/onlymapjs.css",
54
71
  "./onlymapjs.html-data.json": "./onlymapjs.html-data.json",
55
72
  "./onlymap.code-snippets": "./.vscode/onlymap.code-snippets",
@@ -80,16 +97,18 @@
80
97
  ],
81
98
  "scripts": {
82
99
  "dev": "vite",
83
- "build": "npm run typecheck && vite build && vite build --config vite.react.config.ts && npm run build:types",
100
+ "build": "npm run typecheck && vite build && vite build --config vite.react.config.ts && vite build --config vite.deck.config.ts && npm run build:types",
84
101
  "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly",
85
- "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.e2e.json --noEmit",
102
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.e2e.json --noEmit && tsc -p cloud/workers/telemetry/tsconfig.json",
86
103
  "test": "vitest run",
87
104
  "prepublishOnly": "npm run build",
88
105
  "try": "vite-node dev/expr-repl.mjs",
89
106
  "test:e2e": "playwright test",
90
107
  "gen:html-data": "vite-node dev/generate-html-data.ts",
91
108
  "gen:public": "npm run gen:html-data && vite-node dev/build-public.ts",
92
- "test:public": "npm run gen:public -- --dry-run --strict"
109
+ "test:public": "npm run gen:public -- --dry-run --strict",
110
+ "deploy:telemetry": "wrangler deploy --config cloud/workers/telemetry/wrangler.toml",
111
+ "dev:telemetry": "wrangler dev --config cloud/workers/telemetry/wrangler.toml"
93
112
  },
94
113
  "comment:deps": "This library ships a fully self-contained bundle in dist/ (verified: zero external bare imports), so it has NO runtime dependencies — deck.gl, loaders.gl, MapLibre, Arrow, d3, acorn, etc. are build-time-only and get bundled by Vite. Keeping them here (not in dependencies) is what stops every consumer install from pulling ~250 MB it never uses.",
95
114
  "devDependencies": {
@@ -128,6 +147,7 @@
128
147
  "vite": "^6.0.0",
129
148
  "vite-node": "^6.0.0",
130
149
  "vitest": "^4.1.9",
150
+ "wrangler": "^4.110.0",
131
151
  "ws": "^8.21.0"
132
152
  }
133
153
  }
@@ -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 examples, use a published module URL such as `https://esm.sh/@nika-js/onlymap@0.2.0`.
26
+ For no-build CDN examples, use a published module URL such as `https://esm.sh/@nika-js/onlymap@0.3.0`.
27
27
 
28
28
  ## React Projects
29
29
 
@@ -62,6 +62,7 @@ Load the smallest reference needed for the task:
62
62
  - Many labels/badges -> `<om-layer type="PopupLayer">`.
63
63
  - Guided tour or narrative sequence -> `<om-story>` with `<om-step>` siblings that reference existing layers/overlays by id.
64
64
  - Basemap choice or user-switchable basemaps -> `basemap` presets (`positron`, `liberty`, `dark-matter`, `osm`, ...) + `<om-widget type="basemap-switcher">`; MapTiler custom styles via a style URL or `basemap-key`.
65
+ - Undoable UI (step back after layer toggles, filter changes, basemap switches, sketch edits) -> `<om-widget type="undo-redo">`; Cmd/Ctrl-Z works even without the widget. Camera moves and story playback are not undo steps.
65
66
  - Live entity updates -> `wss://` stream with `key` and optional `source` decoder.
66
67
  - REST snapshot that changes over time -> `refresh="5s"`.
67
68
  - User sketching -> `data="draw:sketch"` layer plus `<om-widget type="draw" target="sketch">`.
@@ -71,6 +72,8 @@ Load the smallest reference needed for the task:
71
72
 
72
73
  When creating a map page, output a complete runnable HTML file unless the user asks for a fragment. Include CSS only as needed for page sizing or custom widgets/overlays. Keep the first screen the usable map, not a landing page.
73
74
 
75
+ Include a `map-id="<random UUID>"` attribute on `<om-map>` when creating a new complete page (generate a fresh UUID — never copy one from an example). It identifies the map artifact for usage telemetry, not the visitor; the author can delete it to opt out.
76
+
74
77
  Include an `<om-fallback>` element (a short "this map requires JavaScript — open in a browser" message, optionally with a hosted-version link) as a direct child of `<om-map>` on any complete page. For the fallback to render in no-JS previews, `onlymapjs.css` must load without JavaScript — a real `<link rel="stylesheet">` or inlined `<style>`, not only a runtime `import` (bundler-emitted stylesheets are fine).
75
78
 
76
79
  When modifying an existing page, preserve the user's data URLs, layer ids, and styling unless the request requires changing them.