@nika-js/onlymap 0.3.4 → 0.4.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.
@@ -28,6 +28,7 @@
28
28
  " data=\"${4:../data/bikes.json}\"",
29
29
  " label=\"${5:Points}\" color=\"${6:#e63946}\"",
30
30
  " get-position=\"[\\$lon, \\$lat]\"",
31
+ " radius=\"${7:6}\" radius-units=\"pixels\"",
31
32
  " pickable></om-layer>",
32
33
  "",
33
34
  " <!-- Rich HTML popup — follows the last click via anchor-from=\"selection\" -->",
@@ -110,6 +111,7 @@
110
111
  " data=\"${2:../data/bikes.json}\"",
111
112
  " label=\"${3:Points}\" color=\"${4:#e63946}\"",
112
113
  " get-position=\"[\\$lon, \\$lat]\"",
114
+ " radius=\"${5:6}\" radius-units=\"pixels\"",
113
115
  " pickable></om-layer>$0"
114
116
  ]
115
117
  },
package/README.md CHANGED
@@ -69,7 +69,7 @@ Or with no build step at all, straight from a CDN:
69
69
 
70
70
  The bare package URL serves `dist/onlymap.standalone.js`, a single-file bundle built for exactly this (jsDelivr too). Use a CDN that serves the package's raw files — **not** a rebundling CDN like esm.sh, which re-splits the bundle into duplicate copies of the deck.gl/luma.gl runtime and breaks every layer's shader compilation.
71
71
 
72
- Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with 451 unit/behavioral tests and 27 Playwright GPU tests.
72
+ Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with 503 unit/behavioral tests and 30 Playwright GPU tests.
73
73
 
74
74
  The [examples](https://github.com/NikaGeospatial/onlymapjs/tree/main/examples) are the best tour: widgets, behaviors & overlays, basemaps, columnar/Arrow data, manual drawing, 3D models, scene lighting (with the native lighting widget), DEM terrain, a live WebSocket ship feed, and a polled driver fleet.
75
75
 
@@ -223,8 +223,9 @@ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot
223
223
 
224
224
  - **`OmMap.*`** — `validate`, `snapshotIR`, `registerLayer`, `registerWidget`, `registerAction`, `registerSource`, `registerFormat`, `registerBasemap`, `configureBasemap`, `configureData`, `configureTelemetry`, `configureLicense`, `getLayerSchema`
225
225
  - **`@nika-js/onlymap/deck`** — the bundled deck.gl classes (`CompositeLayer`, `TileLayer`, …) for building custom layer types: shims must extend the same class hierarchy the core renders with, not a second installed deck.gl copy. Recipe: [docs/custom-layers.md](docs/custom-layers.md)
226
- - **On a `<om-map>` element** — `ready` (promise), `flyTo(coords, zoom?)`, `setLayerVisible(id, bool)`, `getLayers()`, `emit(action, payload)`, `snapshot(opts?)` (canvas-only PNG of basemap + layers at device pixels — DOM widgets/overlays and provider attribution are NOT captured, so exports must render credits themselves; `{as: "blob"}` for files, default dataURL); the `om-view-changed` event fires once the camera settles (debounced; `detail` = `{longitude, latitude, zoom, pitch, bearing}`) — the camera-persistence hook; `document.querySelector("om-map")` is fully typed
227
- - **`MapController`** — the framework-grade programmatic front-end (typed `LayerDescriptor`s → the same reconcile core, no DOM manifest): `setLayers`, `watch`, `emit`, camera methods, `injectPick`, `ready`, `snapshot`, an `onViewChange` option (the `om-view-changed` twin). The React adapter rides it; usable directly from vanilla TS or other frameworks
226
+ - **On a `<om-map>` element** — `ready` (promise), `flyTo(coords, zoom?)`, `setLayerVisible(id, bool)`, `getLayers()`, `emit(action, payload)`, `snapshot(opts?)` (canvas-only PNG of basemap + layers at device pixels — DOM widgets/overlays and provider attribution are NOT captured, so exports must render credits themselves; `{as: "blob"}` for files, default dataURL); the `om-view-changed` event fires once the camera settles (debounced; `detail` = `{longitude, latitude, zoom, pitch, bearing, origin}`, where `origin` is `"user"` for gesture-driven bursts vs `"programmatic"` for API/story moves — the echo-suppression signal for state sync) — the camera-persistence hook; `document.querySelector("om-map")` is fully typed
227
+ - **`MapController`** — the framework-grade programmatic front-end (typed `LayerDescriptor`s → the same reconcile core, no DOM manifest): `setLayers`, `watch`, `emit`, camera methods, `injectPick`, `ready`, `snapshot`, an `onViewChange(view, origin)` option (the `om-view-changed` twin). The React adapter rides it; usable directly from vanilla TS or other frameworks
228
+ - **`getStore(token)`** — the external-store contract: per-token `{subscribe, getSnapshot}` stores (`viewport`/`selection`/`layers`/`data:<id>`) with cached immutable plain-data snapshots and `origin` tagging — directly consumable by `useSyncExternalStore` (the React adapter's own hooks ride it), MobX autoruns, Redux listeners, Zustand mirrors. ~20-line integration-tested recipes for Redux Toolkit, MobX/mobx-keystone, Zustand, and Jotai: [docs/external-stores.md](docs/external-stores.md)
228
229
  - **Testing** — `mountForTest`, and imports are SSR-safe (importing in Node/jsdom never touches browser globals)
229
230
 
230
231
  ## Free tier & licensing
@@ -19,7 +19,7 @@ async function v(N = {}) {
19
19
  if (n) {
20
20
  const { createRequire: I } = await import(
21
21
  /*webpackIgnore:true*/
22
- "./lerc-vBCRLD5s.js"
22
+ "./lerc-CSc9BX_F.js"
23
23
  ).then((g) => g._);
24
24
  var O = I(import.meta.url);
25
25
  }
@@ -1,4 +1,4 @@
1
- import { C as ny, L as sy, M as oy, m as ay, c as ka, z as oc, a as Sd, G as sm, V as ly, W as cy, b as uy, g as hy, d as Vu, f as Ed, e as dy, l as py, u as fy, D as Qf, h as my } from "./index-BR1HjJoE.js";
1
+ import { C as ny, L as sy, M as oy, m as ay, c as ka, z as oc, a as Sd, G as sm, V as ly, W as cy, b as uy, g as hy, d as Vu, f as Ed, e as dy, l as py, u as fy, D as Qf, h as my } from "./index-BoxZ3Mec.js";
2
2
  const Cd = Math.PI / 180, gy = 180 / Math.PI;
3
3
  function em(Pe, Q = 0) {
4
4
  const ge = Math.min(180, Pe) * Cd;
@@ -25177,8 +25177,14 @@ class Ry {
25177
25177
  this.initialized ? Q() : this.loadCallbacks.push(Q);
25178
25178
  }
25179
25179
  /** Fires continuously during pan/zoom/rotate — MapLibre's own camera-change event, standing in for deck's onViewStateChange (which doesn't apply — deck isn't driving the camera). */
25180
+ /**
25181
+ * Viewport-change stream with origin tagging (spec: "External-Store
25182
+ * Contract") — MapLibre attaches `originalEvent` (the DOM pointer/wheel
25183
+ * event) to gesture-driven move events; our own easeTo/jumpTo/fitBounds
25184
+ * fire without one. The ecosystem's standard user-vs-programmatic signal.
25185
+ */
25180
25186
  onMove(Q) {
25181
- this.map.on("move", Q);
25187
+ this.map.on("move", (ge) => Q(ge.originalEvent ? "user" : "programmatic"));
25182
25188
  }
25183
25189
  setLayers(Q) {
25184
25190
  this.overlay.setProps({ layers: Q });
package/dist/basemap.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import "maplibre-gl/dist/maplibre-gl.css";
2
2
  import type { DeckProps, Viewport } from "@deck.gl/core";
3
3
  import type { ResolvedBasemap } from "./basemap-registry";
4
+ import type { ViewOrigin } from "./external-store";
4
5
  export interface ViewportLike {
5
6
  project(xyz: number[]): number[];
6
7
  getBounds(): [number, number, number, number];
@@ -65,7 +66,13 @@ export declare class MapLibreBasemapAdapter {
65
66
  private markInitialized;
66
67
  onLoad(cb: () => void): void;
67
68
  /** Fires continuously during pan/zoom/rotate — MapLibre's own camera-change event, standing in for deck's onViewStateChange (which doesn't apply — deck isn't driving the camera). */
68
- onMove(cb: () => void): void;
69
+ /**
70
+ * Viewport-change stream with origin tagging (spec: "External-Store
71
+ * Contract") — MapLibre attaches `originalEvent` (the DOM pointer/wheel
72
+ * event) to gesture-driven move events; our own easeTo/jumpTo/fitBounds
73
+ * fire without one. The ecosystem's standard user-vs-programmatic signal.
74
+ */
75
+ onMove(cb: (origin: ViewOrigin) => void): void;
69
76
  setLayers(layers: DeckProps["layers"]): void;
70
77
  /**
71
78
  * Scene lighting (spec: "Scene Lighting") — MapboxOverlay accepts full
package/dist/ctx.d.ts CHANGED
@@ -57,5 +57,9 @@ export interface BuildCtxDeps {
57
57
  canRedo: boolean;
58
58
  };
59
59
  }
60
+ /** `ctx.viewport.bounds` / the `viewport` external-store snapshot — one bounds derivation (incl. the world-extent fallback) for both (spec: "External-Store Contract"). */
61
+ export declare function viewportBounds(viewport: ReturnType<RuntimeCore["getViewport"]>): [[number, number], [number, number]];
62
+ /** `ctx.layers` / the `layers` external store — one builder for both (spec: "External-Store Contract"). */
63
+ export declare function buildLayerMetas(layerIRs: ReadonlyMap<string, LayerIR>): LayerMetaSnapshot[];
60
64
  /** Built fresh on every widget-render invocation — never mutated, never reused. */
61
65
  export declare function buildCtx({ layerIRs, core, mapEl, selection, history }: BuildCtxDeps): RuntimeContext;
@@ -47,14 +47,12 @@ export declare class OmMapElement extends HTMLElementBase {
47
47
  private readyFired;
48
48
  private resolveReady;
49
49
  readonly ready: Promise<void>;
50
- private viewChangedTimer;
50
+ private readonly viewSettle;
51
51
  private firstReconcileDone;
52
52
  private rendererLoadPending;
53
53
  private loadDispatched;
54
54
  connectedCallback(): void;
55
55
  disconnectedCallback(): void;
56
- /** Trailing-debounced `om-view-changed` dispatch — see the field's doc comment. */
57
- private scheduleViewChangedEvent;
58
56
  registerWidgetInternal(el: Element, watch: string[], notify: (ctx: RuntimeContext) => void): void;
59
57
  unregisterWidgetInternal(el: Element): void;
60
58
  registerOverlayInternal(overlay: OverlayHost): void;
@@ -192,6 +190,8 @@ declare global {
192
190
  zoom: number;
193
191
  pitch: number;
194
192
  bearing: number;
193
+ /** "user" if any change in the settled burst came from a canvas gesture; "programmatic" for pure API/action/story moves. */
194
+ origin: "user" | "programmatic";
195
195
  }>;
196
196
  }
197
197
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * External-store contract (spec: "External-Store Contract — `getStore` &
3
+ * `useSyncExternalStore`") — the framework-free `{subscribe, getSnapshot}`
4
+ * surface `MapController.getStore(token)` hands to React's
5
+ * `useSyncExternalStore`, MobX autoruns, Redux listeners, Zustand mirrors,
6
+ * and any future framework adapter. One contract, N consumers. Also home to
7
+ * the shared settle/burst-origin tracker both front-ends' "camera settled"
8
+ * signals ride — one settle contract, two front-ends, one implementation.
9
+ *
10
+ * The load-bearing rule is CACHED IDENTITY: `getSnapshot()` returns the
11
+ * SAME reference until that token's event fires (a fresh object per call
12
+ * makes `useSyncExternalStore` loop forever — zustand v5's headline
13
+ * breaking change was exactly this fix). Invalidation only marks dirty;
14
+ * the next read rebuilds — a 60fps camera stream with no subscribers
15
+ * builds nothing.
16
+ *
17
+ * Snapshots are PLAIN SERIALIZABLE DATA — no closures — so Redux devtools
18
+ * and persistence middleware never choke on them. Rich reads (`project`,
19
+ * `data()`, `stats()`) stay on `ctx`; `data:<id>` exposes a VERSION STAMP,
20
+ * not the rows (mirroring a 100k-row array into a store per tick is the
21
+ * anti-pattern the Redux performance guidance warns about).
22
+ */
23
+ /**
24
+ * Who moved the camera: a pointer/wheel gesture on the canvas ("user") or
25
+ * a camera API / action / transition frame ("programmatic"). The echo-loop
26
+ * half of two-way store binding — a bridge writes back only user-originated
27
+ * changes; the map's idempotent setters are the other half.
28
+ */
29
+ export type ViewOrigin = "user" | "programmatic";
30
+ /** The per-token store — directly consumable by `useSyncExternalStore(store.subscribe, store.getSnapshot)`. */
31
+ export interface TokenStore<T = unknown> {
32
+ /** `cb` fires after this token's state changed (read the new snapshot inside it). Returns the unsubscribe function. */
33
+ subscribe(cb: () => void): () => void;
34
+ /** Cached immutable snapshot — the SAME reference until the next change event for this token. */
35
+ getSnapshot(): T;
36
+ }
37
+ /** `viewport` store snapshot — CameraState plus bounds and the origin of the current settle burst. */
38
+ export interface ViewportSnapshot {
39
+ longitude: number;
40
+ latitude: number;
41
+ zoom: number;
42
+ pitch: number;
43
+ bearing: number;
44
+ /** [[west, south], [east, north]] — same shape as `ctx.viewport.bounds`. */
45
+ bounds: [[number, number], [number, number]];
46
+ /**
47
+ * Burst-latched origin: "user" if any change since the last settle was a
48
+ * gesture — so a drag's inertia tail (MapLibre inertial moves carry no
49
+ * `originalEvent`) can't relabel the gesture mid-burst. Matches what the
50
+ * settled signals (`om-view-changed`, `onViewChange`) report.
51
+ */
52
+ origin: ViewOrigin;
53
+ }
54
+ /** `data:<layerId>` store snapshot — a version stamp, not the rows (fetch rows via `ctx.data()`). */
55
+ export interface DataVersionSnapshot {
56
+ /** Bumps on every data change for the layer (load, stream tick, poll refresh). 0 = never loaded / unknown layer. */
57
+ version: number;
58
+ /** Current row count (0 for unknown layers). */
59
+ rows: number;
60
+ }
61
+ /**
62
+ * The store-token roster — ONE source of truth shared by
63
+ * `MapController.getStore` (throws otherwise) and the React adapter's
64
+ * warn-skip, so the two can never disagree about which tokens are stores.
65
+ */
66
+ export declare function isStoreToken(token: string): boolean;
67
+ /** The per-token store table — memoized store objects (calling `get` twice for one token returns the identical store). */
68
+ export declare class TokenStoreRegistry {
69
+ private readonly entries;
70
+ /** Memoized per token — `build` is captured on first call and reused. */
71
+ get<T>(token: string, build: () => T): TokenStore<T>;
72
+ /** Marks the token dirty and notifies its subscribers. Unknown/never-requested tokens no-op (nothing to invalidate). */
73
+ invalidate(token: string): void;
74
+ /** Drop every entry and subscriber — controller teardown: a consumer that never unsubscribed must not pin the controller in memory. */
75
+ clear(): void;
76
+ }
77
+ /** Trailing debounce for the settled-camera signals — long enough to coalesce a pan/flight's per-frame churn, short enough that camera persistence feels immediate. */
78
+ export declare const VIEW_SETTLE_DEBOUNCE_MS = 150;
79
+ /**
80
+ * The settle/burst tracker behind `om-view-changed` and `onViewChange` —
81
+ * shared by both front-ends so the debounce window and the burst-origin
82
+ * rule ("user" if ANY change since the last settle was a gesture; an
83
+ * inertia tail can't relabel the gesture) can never drift apart.
84
+ */
85
+ export declare class ViewSettleTracker {
86
+ private readonly onSettle;
87
+ private timer;
88
+ private burstHadUser;
89
+ constructor(onSettle: (origin: ViewOrigin) => void);
90
+ /** Record one viewport change and (re)arm the trailing settle timer. */
91
+ bump(origin: ViewOrigin): void;
92
+ /** Burst-latched origin for snapshots taken mid-burst: the gesture wins over a trailing programmatic frame. */
93
+ latchedOrigin(lastFrameOrigin: ViewOrigin): ViewOrigin;
94
+ cancel(): void;
95
+ }