@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
@@ -15,6 +15,9 @@
15
15
  * runtime state through the same RuntimeContext contract widgets get (HU5).
16
16
  */
17
17
  import { type CameraOptions } from "./runtime-core";
18
+ import { type LightingIR, type LightingOptions } from "./scene-lighting";
19
+ import type { TerrainIR } from "./terrain";
20
+ import { type SnapshotOptions } from "./snapshot";
18
21
  import type { LayerIR } from "./ir";
19
22
  import { type RuntimeContext } from "./ctx";
20
23
  import type { Selection } from "./selection";
@@ -61,6 +64,8 @@ export interface LayerDescriptor {
61
64
  updateTriggers?: Record<string, unknown>;
62
65
  /** deck.gl props (camelCase), accessors as plain functions — passed through. */
63
66
  props?: Record<string, unknown>;
67
+ /** Per-layer terrain mode (spec: "Terrain") — the `terrain` attribute's twin; absent = the type default. */
68
+ terrain?: "drape" | "offset" | "off";
64
69
  }
65
70
  /** Camera state, both an input (options/setView) and an output (getViewState). */
66
71
  export interface CameraState {
@@ -88,6 +93,14 @@ export interface MapControllerOptions {
88
93
  };
89
94
  /** Runtime error boundary — deck.gl-level failures in the structured validation shape. */
90
95
  onRuntimeError?: (entry: ValidationEntry) => void;
96
+ /**
97
+ * Camera settled (trailing-debounced, the `om-view-changed` twin) — for
98
+ * camera persistence. Per-frame viewport churn goes to `watch(["viewport"])`;
99
+ * this fires once with the destination.
100
+ */
101
+ onViewChange?: (view: CameraState) => void;
102
+ /** Scene lighting (spec: "Scene Lighting") — the lighting* attributes' programmatic twin; omit for deck's default lights. */
103
+ lighting?: LightingOptions;
91
104
  }
92
105
  /** Descriptor → layer IR: the programmatic counterpart of one parse-manifest.ts loop iteration. */
93
106
  export declare function descriptorToIR(desc: LayerDescriptor, onDataLoaded: () => void): LayerIR | null;
@@ -118,6 +131,7 @@ export declare class MapController {
118
131
  private commitPending;
119
132
  private destroyed;
120
133
  private firstCommitDone;
134
+ private viewChangeTimer;
121
135
  private readyFired;
122
136
  private resolveReady;
123
137
  readonly ready: Promise<void>;
@@ -176,6 +190,25 @@ export declare class MapController {
176
190
  * (none ↔ maplibre) remount seeded with the current camera.
177
191
  */
178
192
  setBasemap(basemap: string | null): void;
193
+ /** Scene lighting (spec: "Scene Lighting") — swap the LightingEffect live; null restores deck's default lights. */
194
+ setLighting(options: LightingOptions | null): void;
195
+ /** Resolved scene-lighting IR (test inspection) — null = deck default lights. */
196
+ getLighting(): LightingIR | null;
197
+ /**
198
+ * Terrain (spec: "Terrain") — a resolved TerrainIR, or null to turn the
199
+ * surface off. Note: terrain suppresses an active basemap while on
200
+ * (restored on null) — the flat-canvas desync rule.
201
+ */
202
+ setTerrain(ir: TerrainIR | null): void;
203
+ /** Resolved terrain IR (test inspection) — null = no surface. */
204
+ getTerrain(): TerrainIR | null;
205
+ /**
206
+ * Canvas-only scene snapshot (spec: "Snapshot API") — basemap + deck
207
+ * composited at device pixels; framework-rendered widgets/overlays are
208
+ * NOT captured (render provider credits yourself on exports). Await
209
+ * `ready` first; headless controllers reject.
210
+ */
211
+ snapshot(opts?: SnapshotOptions): Promise<string | Blob>;
179
212
  flyTo(center: [number, number], zoom?: number, opts?: CameraOptions): void;
180
213
  flyToBounds(bounds: [[number, number], [number, number]], padding?: number, opts?: CameraOptions): void;
181
214
  setView(partial: Partial<CameraState>, opts?: CameraOptions): void;
@@ -183,4 +216,6 @@ export declare class MapController {
183
216
  /** Projects a lng/lat through the current viewport — undefined before the viewport resolves. */
184
217
  project(lngLat: [number, number]): [number, number] | undefined;
185
218
  destroy(): void;
219
+ /** Trailing-debounced onViewChange — see MapControllerOptions.onViewChange. */
220
+ private scheduleViewChange;
186
221
  }
@@ -26,6 +26,8 @@ export interface OmLayerProps {
26
26
  flush?: string;
27
27
  /** Polling interval for REST snapshot URLs, e.g. "5s". */
28
28
  refresh?: string;
29
+ /** Behavior under active map terrain; absent uses the layer type's default. */
30
+ terrain?: "drape" | "offset" | "off";
29
31
  /** deck.gl updateTriggers — supply a changing value when an accessor's OUTPUT changes (deck ignores function identity). */
30
32
  updateTriggers?: Record<string, unknown>;
31
33
  /** Click pick on this layer (object flattened/materialized, same as ctx.selection). */
package/dist/react.js CHANGED
@@ -1,7 +1,7 @@
1
1
  var J = Object.defineProperty;
2
2
  var Q = (e, t, n) => t in e ? J(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n;
3
3
  var A = (e, t, n) => Q(e, typeof t != "symbol" ? t + "" : t, n);
4
- import { jsxs as U, jsx as x } from "react/jsx-runtime";
4
+ import { jsxs as U, jsx as C } from "react/jsx-runtime";
5
5
  import { createContext as X, useContext as Y, forwardRef as Z, useRef as f, useState as k, useCallback as ee, useMemo as I, useLayoutEffect as te, useEffect as g, useImperativeHandle as re } from "react";
6
6
  import { MapController as ne } from "@nika-js/onlymap";
7
7
  import { createPortal as B } from "react-dom";
@@ -53,7 +53,7 @@ function ie(e, t) {
53
53
  return Object.entries(t).some(([n, r]) => r !== void 0 && e[n] !== r);
54
54
  }
55
55
  const he = Z(function(t, n) {
56
- const { center: r, zoom: l, pitch: a, bearing: u, basemap: i, basemapKey: c, attribution: y, headless: m, className: O, style: z, children: R } = t, d = f(null), [o, T] = k(null), [C, M] = k(null), [j, L] = k({
56
+ const { center: r, zoom: l, pitch: a, bearing: u, basemap: i, basemapKey: c, attribution: y, headless: m, className: O, style: z, children: R } = t, d = f(null), [o, T] = k(null), [p, M] = k(null), [j, L] = k({
57
57
  "top-left": null,
58
58
  "top-right": null,
59
59
  "bottom-left": null,
@@ -65,7 +65,7 @@ const he = Z(function(t, n) {
65
65
  q.map((s) => [
66
66
  s,
67
67
  (h) => {
68
- L((p) => p[s] === h ? p : { ...p, [s]: h });
68
+ L((x) => x[s] === h ? x : { ...x, [s]: h });
69
69
  }
70
70
  ])
71
71
  ),
@@ -100,12 +100,12 @@ const he = Z(function(t, n) {
100
100
  }
101
101
  });
102
102
  T(h);
103
- let p = !0;
103
+ let x = !0;
104
104
  return h.ready.then(() => {
105
105
  var F;
106
- p && ((F = b.current) == null || F.call(b));
106
+ x && ((F = b.current) == null || F.call(b));
107
107
  }), () => {
108
- p = !1, T(null), h.destroy();
108
+ x = !1, T(null), h.destroy();
109
109
  };
110
110
  }, [G]), g(() => {
111
111
  !o || V.current === i || (V.current = i, o.setBasemap(i ?? null));
@@ -121,19 +121,19 @@ const he = Z(function(t, n) {
121
121
  });
122
122
  }, [o]);
123
123
  const W = I(() => o ? new se(o) : null, [o]), _ = I(
124
- () => o && W && C ? { controller: o, layers: W, overlayPane: C, corners: j } : null,
125
- [o, W, C, j]
124
+ () => o && W && p ? { controller: o, layers: W, overlayPane: p, corners: j } : null,
125
+ [o, W, p, j]
126
126
  );
127
127
  return /* @__PURE__ */ U("div", { className: O, style: { position: "relative", overflow: "hidden", ...z }, children: [
128
- /* @__PURE__ */ x("div", { ref: d, style: { position: "absolute", inset: 0 } }),
129
- /* @__PURE__ */ x(
128
+ /* @__PURE__ */ C("div", { ref: d, style: { position: "absolute", inset: 0 } }),
129
+ /* @__PURE__ */ C(
130
130
  "div",
131
131
  {
132
132
  ref: v,
133
133
  style: { position: "absolute", top: 0, left: 0, zIndex: 10, pointerEvents: "none" }
134
134
  }
135
135
  ),
136
- q.map((s) => /* @__PURE__ */ x(
136
+ q.map((s) => /* @__PURE__ */ C(
137
137
  "div",
138
138
  {
139
139
  "data-om-widget-corner": s,
@@ -142,7 +142,7 @@ const he = Z(function(t, n) {
142
142
  },
143
143
  s
144
144
  )),
145
- _ && /* @__PURE__ */ x(H.Provider, { value: _, children: R })
145
+ _ && /* @__PURE__ */ C(H.Provider, { value: _, children: R })
146
146
  ] });
147
147
  }), ae = /* @__PURE__ */ new Set([
148
148
  "id",
@@ -159,6 +159,7 @@ const he = Z(function(t, n) {
159
159
  "streamKey",
160
160
  "flush",
161
161
  "refresh",
162
+ "terrain",
162
163
  "updateTriggers",
163
164
  "onClick",
164
165
  "onHover"
@@ -182,6 +183,7 @@ function ge(e) {
182
183
  key: e.streamKey,
183
184
  flush: e.flush,
184
185
  refresh: e.refresh,
186
+ terrain: e.terrain,
185
187
  updateTriggers: e.updateTriggers,
186
188
  props: l
187
189
  };
@@ -206,7 +208,7 @@ function ge(e) {
206
208
  function ve({ position: e = "top-left", className: t, style: n, children: r }) {
207
209
  const { corners: l } = K("OmWidget"), a = l[e];
208
210
  return a ? B(
209
- /* @__PURE__ */ x("div", { className: t, style: { pointerEvents: "auto", ...n }, children: r }),
211
+ /* @__PURE__ */ C("div", { className: t, style: { pointerEvents: "auto", ...n }, children: r }),
210
212
  a
211
213
  ) : null;
212
214
  }
@@ -227,9 +229,9 @@ function be(e) {
227
229
  () => t.registerOverlay((R, d) => {
228
230
  const o = r.current;
229
231
  if (!o) return;
230
- const { anchor: T, anchorFrom: C, layer: M, visible: j = !0, anchorOffset: L } = i.current;
232
+ const { anchor: T, anchorFrom: p, layer: M, visible: j = !0, anchorOffset: L } = i.current;
231
233
  let v;
232
- if (C === "selection" ? ((!M || (d == null ? void 0 : d.layerId) === M) && (l.current = (d == null ? void 0 : d.coordinate) ?? void 0, u(d)), v = l.current) : v = T, !j || !v || !R) {
234
+ if (p === "selection" ? ((!M || (d == null ? void 0 : d.layerId) === M) && (l.current = (d == null ? void 0 : d.coordinate) ?? void 0, u(d)), v = l.current) : v = T, !j || !v || !R) {
233
235
  o.style.visibility = "hidden";
234
236
  return;
235
237
  }
@@ -246,7 +248,7 @@ function be(e) {
246
248
  });
247
249
  const { children: c, className: y, style: m, interactive: O = !0 } = e, z = typeof c == "function" ? c(a) : c;
248
250
  return B(
249
- /* @__PURE__ */ x(
251
+ /* @__PURE__ */ C(
250
252
  "div",
251
253
  {
252
254
  ref: r,
@@ -1,8 +1,10 @@
1
+ import { type LightingIR } from "./scene-lighting";
2
+ import { type TerrainIR } from "./terrain";
1
3
  import type { LayerIR } from "./ir";
2
4
  import { type Selection } from "./selection";
3
5
  import type { ValidationEntry } from "./validation";
4
6
  import type { MapViewport } from "./basemap";
5
- /** `basemap="mapbox-*"` isn't implemented — kept maplibre-free so validation/runtime can check without loading the chunk. */
7
+ /** Mapbox presets/protocol URLs aren't implemented — kept maplibre-free so validation/runtime can check without loading the chunk. */
6
8
  export declare function isMapboxBasemap(raw: string): boolean;
7
9
  export interface InitialView {
8
10
  longitude: number;
@@ -90,6 +92,48 @@ export declare class RuntimeCore {
90
92
  private basemapOptions?;
91
93
  /** Bumped on every renderer (re)init — a superseded lazy chunk load must not install its adapter over a newer renderer. */
92
94
  private rendererGeneration;
95
+ /** Pre-gate descriptors — what a license settle re-reconciles from (lastIRs holds the GATED set the renderer sees). */
96
+ private pregateIRs;
97
+ /**
98
+ * Scene lighting (spec: "Scene Lighting") — the retained IR, rebuilt into
99
+ * a LightingEffect on apply. Survives renderer remounts (initRenderer
100
+ * reads it, the lastIRs contract) and the lazy basemap-chunk window (the
101
+ * adapter constructor reads it on arrival). null = deck default lights.
102
+ */
103
+ private lighting;
104
+ /**
105
+ * Terrain (spec: "Terrain") — the retained surface IR. Non-null appends
106
+ * the internal terrain layer and patches drape/offset layers in
107
+ * buildLayers. `terrainGeneration` bumps on every terrain state change:
108
+ * extension sets must be BIRTH-stable, so patched layers get
109
+ * `#t<generation>`-based deck ids (fresh instances per flip — see
110
+ * applyTerrain); an explicit renderer-id map restores authored pick ids.
111
+ */
112
+ private terrain;
113
+ private terrainGeneration;
114
+ /** Rendered deck layer id → authored manifest/controller id (terrain uses fresh renderer ids). */
115
+ private renderedLayerIds;
116
+ /**
117
+ * The basemap attribute suppressed while terrain is active — a flat
118
+ * MapLibre canvas at sea level visibly desyncs from a raised surface
119
+ * (true coexistence is the interleaved-compositing TODO), so terrain
120
+ * forces standalone rendering and restores the basemap when it turns off.
121
+ */
122
+ private suppressedBasemapAttr?;
123
+ /**
124
+ * Snapshot capture queue (spec: "Snapshot API") — the WebGL context has
125
+ * no preserveDrawingBuffer, so pixels are only readable synchronously
126
+ * inside a post-render callback. snapshot() enqueues a capture and forces
127
+ * a redraw; the permanent onAfterRender (standalone Deck constructor)
128
+ * drains the queue while the drawing buffer is valid. `cancel` settles the
129
+ * promise when the renderer goes away before that frame renders (teardown/
130
+ * destroy) — a queued capture must never leave its caller hanging.
131
+ */
132
+ private snapshotCaptures;
133
+ /** (layer, reason) pairs already reported — gate errors fire once per violation, not per reconcile. */
134
+ private readonly gateWarned;
135
+ private badge?;
136
+ private unsubscribeLicense?;
93
137
  constructor(parent: HTMLElement, initialView: InitialView, callbacks?: RuntimeCoreCallbacks, basemapAttr?: string, headless?: HeadlessOptions, basemapOptions?: BasemapRuntimeOptions);
94
138
  /** `basemap` attribute → concrete style, logging resolution problems (unknown preset / missing key) — the demo-style fallback still renders. */
95
139
  private resolveBasemap;
@@ -99,6 +143,40 @@ export declare class RuntimeCore {
99
143
  * (none ↔ maplibre). Reads/seeds the camera from `this.viewState`.
100
144
  */
101
145
  private initRenderer;
146
+ /** The current lighting as a deck `effects` array — [] restores deck's default lights. */
147
+ private buildEffects;
148
+ /**
149
+ * Scene lighting (spec: "Scene Lighting") — swap the LightingEffect on
150
+ * the live renderer. null restores deck's default lights. Headless stores
151
+ * the IR (inspectable via the map's internal getter) with no renderer to
152
+ * drive; a change during the lazy basemap-chunk window is picked up by
153
+ * the adapter constructor (which reads `this.lighting` on arrival). A
154
+ * sunDate-driven sun resolves az/el HERE, against the current map center
155
+ * — deterministic per apply, not tracked per frame.
156
+ */
157
+ setLighting(ir: LightingIR | null): void;
158
+ getLightingInternal(): LightingIR | null;
159
+ /**
160
+ * Terrain on/off/source change (spec: "Terrain"). Bumps the generation
161
+ * (fresh deck ids for extension-carrying layers — birth-stable extension
162
+ * sets), swaps the renderer mode when needed (terrain suppresses an
163
+ * active basemap; turning it off restores the suppressed one), and
164
+ * re-applies layers. Headless stores the IR for inspection.
165
+ */
166
+ setTerrain(ir: TerrainIR | null): void;
167
+ getTerrainInternal(): TerrainIR | null;
168
+ private drainSnapshotCaptures;
169
+ /** Settle queued snapshot promises when the renderer they were waiting on goes away (mode flip / destroy) — never leave a caller hanging. */
170
+ private cancelSnapshotCaptures;
171
+ /**
172
+ * Canvas-only scene snapshot (spec: "Snapshot API") — the composite of
173
+ * basemap + deck canvases at device pixels, captured right after a forced
174
+ * repaint (no preserveDrawingBuffer in either context). DOM widgets,
175
+ * overlays, the badge, and the attribution control are NOT captured —
176
+ * consumers rendering exports must add provider credits themselves.
177
+ * Headless rejects (no renderer); pre-ready rejects (await map.ready).
178
+ */
179
+ snapshot(): Promise<HTMLCanvasElement>;
102
180
  /**
103
181
  * Live basemap change (spec: "Basemap presets & switching"), both paths:
104
182
  * - maplibre → maplibre: `map.setStyle()` — deck layers survive (the
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Scene lighting (spec: "Scene Lighting") — the manifest surface over
3
+ * deck.gl's LightingEffect, resolving the backlog TODO ("Lighting/effects
4
+ * manifest surface"). Attributes on <om-map>, the basemap= precedent:
5
+ * singleton scene state as attributes gets undo/redo for free (attribute
6
+ * history) and live switching through the same MutationObserver path.
7
+ *
8
+ * <om-map lighting="daylight">
9
+ * <om-map lighting="custom" lighting-ambient="0.6" lighting-sun="1.5"
10
+ * lighting-sun-azimuth="135" lighting-sun-elevation="45">
11
+ * <om-map lighting="daylight" lighting-sun-date="2026-07-15T14:00:00Z">
12
+ *
13
+ * The sun is ALWAYS a DirectionalLight driven by azimuth/elevation;
14
+ * `lighting-sun-date` is sugar that computes az/el from solar position at
15
+ * the map center when lighting is applied (deliberately NOT deck's
16
+ * experimental `_SunLight`, which recomputes per frame from the viewport —
17
+ * an attribute-declared scene should be deterministic, and the underscore
18
+ * API is upgrade churn). Absent `lighting` = deck's default lights —
19
+ * today's behavior, so the whole surface is additive.
20
+ */
21
+ import { AmbientLight, _CameraLight as CameraLight, DirectionalLight, LightingEffect } from "@deck.gl/core";
22
+ export interface LightingIR {
23
+ /** Preset the values were seeded from (validation/UI echo; "custom" for a fully hand-tuned scene). */
24
+ preset: LightingPresetName;
25
+ /** Ambient intensity — always-on fill. */
26
+ ambient: number;
27
+ /** Sun (directional) intensity; 0 omits the sun light entirely. */
28
+ sun: number;
29
+ /** Sun compass bearing, ° clockwise from north. */
30
+ sunAzimuth: number;
31
+ /** Sun height above the horizon, ° (0 = horizon, 90 = zenith). */
32
+ sunElevation: number;
33
+ /** Camera-following fill intensity (model inspection); 0 omits it. */
34
+ camera: number;
35
+ /** Epoch ms — when set, az/el are recomputed from solar position at apply time. */
36
+ sunDate?: number;
37
+ }
38
+ export type LightingPresetName = "daylight" | "studio" | "flat" | "custom";
39
+ export declare const LIGHTING_PRESET_NAMES: readonly ["daylight", "studio", "flat", "custom"];
40
+ export declare function isLightingPreset(name: string): name is LightingPresetName;
41
+ /** Programmatic twin of the lighting-* attributes (MapControllerOptions.lighting / setLighting). */
42
+ export interface LightingOptions {
43
+ preset?: LightingPresetName;
44
+ ambient?: number;
45
+ sun?: number;
46
+ sunAzimuth?: number;
47
+ sunElevation?: number;
48
+ camera?: number;
49
+ /** Epoch ms or ISO 8601 — the lighting-sun-date twin. */
50
+ sunDate?: number | string;
51
+ }
52
+ /** Preset seeds values; explicit fields override individual seeds; sunDate (resolved at apply time) wins over az/el. */
53
+ export declare function resolveLighting(options: LightingOptions): LightingIR;
54
+ /**
55
+ * `lighting*` attributes → IR. Returns null when `lighting` itself is
56
+ * absent (deck default lights; stray lighting-* overrides without it are
57
+ * inert — validation warns). Malformed numbers fall back to the preset seed
58
+ * (validation flags them; the live path must still render something).
59
+ */
60
+ export declare function parseLightingAttrs(getAttr: (name: string) => string | null): LightingIR | null;
61
+ /**
62
+ * Solar azimuth/elevation at a timestamp + location, in the manual-sun
63
+ * convention (azimuth ° CW from north, elevation ° above horizon). The
64
+ * standard suncalc derivation (the same math deck's `_SunLight` uses via
65
+ * @math.gl/sun): solar azimuth measured south→west, so matching the
66
+ * DirectionalLight convention is `azimuth = solar + 180°`. Below-horizon
67
+ * sun clamps to the horizon — a flat scene can't render a sun beneath it.
68
+ */
69
+ export declare function solarAzElDegrees(timestamp: number, latitude: number, longitude: number): {
70
+ azimuth: number;
71
+ elevation: number;
72
+ };
73
+ /**
74
+ * IR → the named lights map a LightingEffect is constructed from —
75
+ * separated so tests can assert composition without reaching into
76
+ * LightingEffect's private fields. The ambient light is always present;
77
+ * sun and camera lights only at intensity > 0 (deck renders a 0-intensity
78
+ * light as subtle noise, not nothing). `center` (current map center) feeds
79
+ * the sunDate → az/el resolution.
80
+ */
81
+ export declare function buildLights(ir: LightingIR, center?: [longitude: number, latitude: number]): Record<string, AmbientLight | DirectionalLight | CameraLight>;
82
+ /** IR → deck.gl LightingEffect (see buildLights for the composition rules). */
83
+ export declare function buildLightingEffect(ir: LightingIR, center?: [longitude: number, latitude: number]): LightingEffect;
@@ -27,5 +27,5 @@ interface PickingInfoLike {
27
27
  x: number;
28
28
  y: number;
29
29
  }
30
- export declare function toSelection(info: PickingInfoLike, type: "hover" | "click"): Selection | null;
30
+ export declare function toSelection(info: PickingInfoLike, type: "hover" | "click", resolveLayerId?: (renderedId: string) => string): Selection | null;
31
31
  export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Snapshot serialization (spec: "Snapshot API") — shared by OmMapElement
3
+ * and MapController: the composite canvas RuntimeCore.snapshot() produces,
4
+ * serialized to the consumer's preferred shape.
5
+ */
6
+ export interface SnapshotOptions {
7
+ /** Image MIME type — default "image/png". */
8
+ type?: "image/png" | "image/jpeg" | "image/webp";
9
+ /** Lossy-format quality 0–1 (jpeg/webp). */
10
+ quality?: number;
11
+ /** Output shape — default "dataURL" (what print/export pipelines embed directly); "blob" for uploads/files. */
12
+ as?: "dataURL" | "blob";
13
+ }
14
+ export declare function serializeSnapshot(canvas: HTMLCanvasElement, opts: SnapshotOptions): Promise<string | Blob>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The telemetry WIRE SCHEMA (spec: "Monetization Gates / Usage telemetry";
3
+ * public doc: docs/telemetry.md — keep all three in sync). Deliberately a
4
+ * types-only module with zero DOM/browser references: the ingestion Worker
5
+ * (cloud/workers/telemetry, WebWorker lib) type-imports this same file, so
6
+ * client and server can never drift apart silently — a schema change breaks
7
+ * the other side's typecheck in the same commit.
8
+ */
9
+ export interface TelemetryLayerSnapshot {
10
+ type: string;
11
+ rows: number;
12
+ streaming: boolean;
13
+ refresh: boolean;
14
+ }
15
+ /** Library-caused error report (spec: "Monetization Gates / Usage telemetry" — library-error reporting). Scrubbed: no manifest content, no layer data, no page paths. */
16
+ export interface TelemetryErrorReport {
17
+ event: "library_error";
18
+ pageLoadId: string;
19
+ version: string;
20
+ /** Hostname only — never the path. */
21
+ origin: string;
22
+ dev: boolean;
23
+ /** Hash of message + top own-code frame — the dedup/grouping key. */
24
+ signature: string;
25
+ message: string;
26
+ /** Top stack lines from the library's own code, query strings stripped. */
27
+ frames: string[];
28
+ ua: string;
29
+ }
30
+ export interface TelemetrySnapshot {
31
+ event: "map_ready";
32
+ /** Random per page load — dedups beacon retries & multi-map pages. NOT persistent, NOT a visitor id. */
33
+ pageLoadId: string;
34
+ /** The authored `map-id` attribute — identifies the map artifact, not the visitor. */
35
+ mapId: string | null;
36
+ version: string;
37
+ /** "free" until the license module (monetization M1) exists. */
38
+ plan: string;
39
+ keyId: string | null;
40
+ /** Hostname only — never the path. */
41
+ origin: string;
42
+ frontend: "html" | "react" | "programmatic";
43
+ renderer: "maplibre" | "standalone";
44
+ /** Dev context (localhost et al.) — the only beacons the server may GeoIP (country code, IP discarded). */
45
+ dev: boolean;
46
+ layers: TelemetryLayerSnapshot[];
47
+ story: {
48
+ steps: number;
49
+ } | null;
50
+ widgets: string[];
51
+ draw: boolean;
52
+ undoRedo: boolean;
53
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Usage telemetry — client side (spec: "Monetization Gates / Usage
3
+ * telemetry", phase M3 client half). One snapshot per <om-map> instance per
4
+ * page load, built when that map reaches `ready` (manifest fully parsed —
5
+ * late mutations go uncounted, by design). Deployment-scoped, never
6
+ * visitor-scoped: the payload describes the page's use of the library.
7
+ *
8
+ * LIVE by default since the ingestion Worker deployment (2026-07-14): the
9
+ * default endpoint is the first-party Worker at om-api.nika.eco (spec's
10
+ * "beacon must not ship before something is listening" — it is listening).
11
+ * Disable with `configureTelemetry({ disabled: true })`, per map with
12
+ * `telemetry="off"`, or clear the endpoint (`{ endpoint: undefined }`).
13
+ * Disclosure: LICENSE.md §11; public schema: docs/telemetry.md.
14
+ *
15
+ * Non-negotiables (spec, binding):
16
+ * - telemetry never affects function: everything is wrapped, fire-and-
17
+ * forget, no retry, silent on failure; nothing awaits it;
18
+ * - no PII, no page URLs (hostname only), no visitor identifiers —
19
+ * `pageLoadId` dies with the page; `map-id` identifies the map ARTIFACT;
20
+ * - opt-out: `OmMap.configureTelemetry({ disabled: true })` or
21
+ * `telemetry="off"` on the map element;
22
+ * - headless maps (consumer test suites) never report.
23
+ */
24
+ import type { LayerIR } from "./ir";
25
+ import type { TelemetrySnapshot, TelemetryErrorReport } from "./telemetry-schema";
26
+ export type { TelemetrySnapshot, TelemetryLayerSnapshot } from "./telemetry-schema";
27
+ export interface TelemetryConfig {
28
+ /** Kill switch — disables the map-ready beacon and library-error reports globally. */
29
+ disabled?: boolean;
30
+ /** Ingestion endpoint — the first-party Worker by default; explicitly set `undefined` to silence all sends. */
31
+ endpoint?: string;
32
+ }
33
+ /** The first-party ingestion Worker (cloud/workers/telemetry) — a domain we control, never a vendor's. */
34
+ export declare const DEFAULT_TELEMETRY_ENDPOINT = "https://om-api.nika.eco/v1/t";
35
+ /** Merge-assign: only keys present in `partial` change (so `{ endpoint: undefined }` explicitly clears). */
36
+ export declare function configureTelemetry(partial: TelemetryConfig): void;
37
+ /** Read-only view for the error-reporting module (same gates, same switch). */
38
+ export declare function getTelemetryConfig(): Readonly<TelemetryConfig>;
39
+ /** One id per page load, shared by every map on the page. */
40
+ export declare const pageLoadId: string;
41
+ /** Hostname-only origin + dev flag — shared with error reporting. */
42
+ export declare function hostContext(): {
43
+ origin: string;
44
+ dev: boolean;
45
+ };
46
+ export declare function buildMapSnapshot(mapEl: Element, layerIRs: ReadonlyMap<string, LayerIR>, frontend?: TelemetrySnapshot["frontend"]): TelemetrySnapshot;
47
+ /**
48
+ * Fire-and-forget POST. `text/plain` is deliberate: it keeps the request
49
+ * CORS-simple (no preflight from every customer origin) — the Worker parses
50
+ * the body as JSON regardless of the content type. sendBeacon survives page
51
+ * unload; `fetch keepalive` is the fallback where sendBeacon is missing.
52
+ * Never throws, never retries, nothing observes the result.
53
+ */
54
+ export declare function postPayload(payload: TelemetrySnapshot | TelemetryErrorReport): void;
55
+ /**
56
+ * The om-map `ready` hook — one call per map instance per page load
57
+ * (guarded by the caller's readyFired latch). All gates checked here so the
58
+ * call site stays a single unconditional line.
59
+ */
60
+ export declare function reportMapReady(mapEl: Element, layerIRs: ReadonlyMap<string, LayerIR>): void;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Terrain (spec: "Terrain") — 3D elevation surface + per-layer draping,
3
+ * declared as `terrain*` attributes on <om-map> (the basemap=/lighting=
4
+ * scene-attribute precedent: free undo/redo, live MutationObserver
5
+ * switching).
6
+ *
7
+ * <om-map terrain="terrarium" terrain-exaggeration="1.5" pitch="55">
8
+ * <om-map terrain="https://tiles.example.com/dem/{z}/{x}/{y}.png"
9
+ * terrain-decoder="terrarium" terrain-max-zoom="14"
10
+ * terrain-texture="https://…/satellite/{z}/{x}/{y}.jpg">
11
+ *
12
+ * The seventh plugin registry (`OmMap.registerTerrain`) resolves preset
13
+ * names; a `{z}/{x}/{y}` URL is bring-your-own DEM (requires
14
+ * terrain-decoder). Geographic layers DRAPE onto the surface by default;
15
+ * a per-layer `terrain="drape|offset|off"` attribute overrides (3D-model
16
+ * layers default to `offset` — models sit ON terrain, not painted onto
17
+ * it).
18
+ *
19
+ * Terrain REPLACES an active basemap: a flat MapLibre canvas sits at sea
20
+ * level and visibly desyncs from a raised surface (true coexistence is
21
+ * the interleaved-compositing TODO). RuntimeCore suppresses the basemap
22
+ * while terrain is active and restores it when terrain turns off.
23
+ *
24
+ * `applyTerrain` is PURE (unit-testable without a GPU — headless never
25
+ * reaches the deck handoff): it computes each layer's terrain patches and
26
+ * the terrain-surface layer props; RuntimeCore feeds them to buildLayers.
27
+ */
28
+ import type { Layer } from "@deck.gl/core";
29
+ import type { LayerIR } from "./ir";
30
+ /** RGB DEM decoder: height = offset + R·rScaler + G·gScaler + B·bScaler. */
31
+ export interface ElevationDecoder {
32
+ rScaler: number;
33
+ gScaler: number;
34
+ bScaler: number;
35
+ offset: number;
36
+ }
37
+ export declare const DECODERS: Readonly<Record<string, ElevationDecoder>>;
38
+ export interface TerrainPreset {
39
+ /** `{z}/{x}/{y}` DEM tile URL template (may carry a `{key}` placeholder for keyed providers). */
40
+ elevationData: string;
41
+ /** Decoder name ("terrarium" / "mapbox-rgb") or explicit scalers. */
42
+ decoder: keyof typeof DECODERS | ElevationDecoder;
43
+ /**
44
+ * The provider's REAL tileset cap — deck over-samples past it for closer
45
+ * cameras; a too-high cap requests tiles that 404 and blanks the terrain.
46
+ */
47
+ maxZoom: number;
48
+ /** Optional draped imagery `{z}/{x}/{y}` URL template. */
49
+ texture?: string;
50
+ /** Display name (switcher UIs). */
51
+ label?: string;
52
+ /** Which key `{key}` substitutes — the basemap-registry convention. */
53
+ requiresKey?: "maptiler";
54
+ }
55
+ export declare function registerTerrain(name: string, preset: TerrainPreset): void;
56
+ export declare function getTerrain(name: string): TerrainPreset | undefined;
57
+ export declare function getAllTerrains(): ReadonlyMap<string, TerrainPreset>;
58
+ export interface TerrainIR {
59
+ elevationData: string;
60
+ elevationDecoder: ElevationDecoder;
61
+ maxZoom: number;
62
+ /** Multiplies decoded DEM heights (1 = true relief). */
63
+ exaggeration: number;
64
+ texture?: string;
65
+ }
66
+ export interface ResolvedTerrain {
67
+ ir: TerrainIR | null;
68
+ /** Resolution problem (unknown preset / missing key / missing decoder) — the caller decides how loudly to fail; `ir` is null when unresolvable. */
69
+ error?: string;
70
+ }
71
+ /** Is this `terrain` value a bring-your-own DEM URL rather than a preset name? (The basemap isStyleUrl convention.) */
72
+ export declare function isDemUrl(raw: string): boolean;
73
+ /**
74
+ * `terrain*` attributes → TerrainIR. Returns `{ir: null}` when `terrain`
75
+ * is absent/"off"; `{ir: null, error}` when set but unresolvable (unknown
76
+ * preset, keyless keyed preset, raw URL without a decoder) — terrain
77
+ * renders NOTHING in that case rather than a wrong surface, and validation
78
+ * carries the loud version of the same message.
79
+ */
80
+ export declare function parseTerrainAttrs(getAttr: (name: string) => string | null, maptilerKey?: string): ResolvedTerrain;
81
+ export declare const TERRAIN_LAYER_ID = "__onlymapjs-terrain";
82
+ export type LayerTerrainMode = "drape" | "offset" | "off";
83
+ export declare const LAYER_TERRAIN_MODES: readonly ["drape", "offset", "off"];
84
+ export declare function defaultTerrainMode(type: string): LayerTerrainMode;
85
+ export interface TerrainLayerPatch {
86
+ /**
87
+ * The deck-layer id to construct with. Suffixed `#t<generation>` while
88
+ * terrain is active: adding/removing an extension on a LIVE layer
89
+ * recompiles shaders and corrupts attribute state (the DataFilterExtension
90
+ * birth-mount rule), and a stale TerrainExtension vertical offset survives
91
+ * a terrain toggle on a stable id — a fresh id forces clean teardown +
92
+ * rebuild. RuntimeCore maps these generated ids back to authored ids for
93
+ * picks without guessing from the string shape.
94
+ */
95
+ deckId: string;
96
+ /** Extra constructor props: extensions (APPENDED to existing — DataFilterExtension stays mounted) + terrainDrawMode. Empty for mode "off". */
97
+ extraProps: Record<string, unknown>;
98
+ }
99
+ /**
100
+ * Per-layer terrain patches, PURE. `terrainActive` false → identity patches
101
+ * (stable ids, no props). Runtime-internal layers (trace temps, draw
102
+ * preview, tooltip plumbing) never drape — they're screen-space/transient.
103
+ */
104
+ export declare function applyTerrain(irs: readonly LayerIR[], terrainActive: boolean, generation: number): Map<string, TerrainLayerPatch>;
105
+ /**
106
+ * The terrain-surface layer itself. `operation: "terrain+draw"` makes it
107
+ * double as the elevation surface extension-carrying layers drape onto;
108
+ * `pickable: "3d"` runs the depth pick pass so click/hover coordinates
109
+ * unproject at the SURFACE's depth instead of z=0 sea level (without it,
110
+ * tilted-view picks/drawn vertices land behind the cursor). Exempt from
111
+ * license gates by the `__onlymapjs-` prefix.
112
+ */
113
+ export declare function buildTerrainLayer(ir: TerrainIR): Layer;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The library version, as a compile-time constant (telemetry payloads,
3
+ * future diagnostics). Kept in sync with package.json by a drift test
4
+ * (telemetry.test.ts) rather than a JSON import — package.json sits outside
5
+ * the build rootDir, and a `define` would need repeating across vite/vitest/
6
+ * vite-node configs.
7
+ */
8
+ export declare const LIBRARY_VERSION = "0.3.0";