@nika-js/onlymap 0.2.0 → 0.2.1

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.
@@ -75,8 +75,12 @@ export interface MapControllerOptions {
75
75
  zoom?: number;
76
76
  pitch?: number;
77
77
  bearing?: number;
78
- /** Same contract as the `basemap` attribute — "maplibre", a style URL, or omit for standalone deck.gl. */
78
+ /** Same contract as the `basemap` attribute — a preset name, "maplibre", a style URL, or omit for standalone deck.gl. */
79
79
  basemap?: string;
80
+ /** Publishable provider key for keyed presets (`maptiler-*`) — the `basemap-key` attribute's programmatic twin (configureBasemap covers the global case). */
81
+ basemapKey?: string;
82
+ /** Set false to opt out of the compact attribution control (render your own credits). */
83
+ attribution?: boolean;
80
84
  /** Headless mode (spec: "Consumer Testing Surface") — no renderer, real projection math; for jsdom/happy-dom tests. */
81
85
  headless?: {
82
86
  width?: number;
@@ -166,6 +170,12 @@ export declare class MapController {
166
170
  private scheduleOverlayFlush;
167
171
  /** Dispatches an action (spec: "Action payload contract") — camera moves, pulse/populate effects, custom registered actions. */
168
172
  emit(event: string, payload?: Record<string, unknown>): void;
173
+ /**
174
+ * Live basemap change — a preset name, style URL, or "none". Style-only
175
+ * switches keep the renderer, camera, and layers; renderer-mode changes
176
+ * (none ↔ maplibre) remount seeded with the current camera.
177
+ */
178
+ setBasemap(basemap: string | null): void;
169
179
  flyTo(center: [number, number], zoom?: number, opts?: CameraOptions): void;
170
180
  flyToBounds(bounds: [[number, number], [number, number]], padding?: number, opts?: CameraOptions): void;
171
181
  setView(partial: Partial<CameraState>, opts?: CameraOptions): void;
@@ -7,8 +7,12 @@ export interface OmMapProps {
7
7
  zoom?: number;
8
8
  pitch?: number;
9
9
  bearing?: number;
10
- /** Same contract as the `basemap` attribute — "maplibre", a style URL, or omit for standalone deck.gl. Changing it remounts the renderer. */
10
+ /** Same contract as the `basemap` attribute — a preset name ("positron", "dark-matter", …), "maplibre", a style URL, or omit for standalone deck.gl. Changing it switches live (style-only changes keep camera + layers; none ↔ maplibre remounts the renderer inside the controller). */
11
11
  basemap?: string;
12
+ /** Publishable provider key for keyed presets (maptiler-*) — or call configureBasemap once instead. */
13
+ basemapKey?: string;
14
+ /** Set false to opt out of the compact attribution control. */
15
+ attribution?: boolean;
12
16
  /** Headless mode for tests (spec: "Consumer Testing Surface") — no renderer, real projection math under jsdom/happy-dom. */
13
17
  headless?: boolean | {
14
18
  width?: number;
package/dist/react.js CHANGED
@@ -1,40 +1,40 @@
1
- var q = Object.defineProperty;
2
- var H = (e, t, n) => t in e ? q(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n;
3
- var D = (e, t, n) => H(e, typeof t != "symbol" ? t + "" : t, n);
4
- import { jsxs as G, jsx as w } from "react/jsx-runtime";
5
- import { createContext as J, useContext as B, forwardRef as Q, useRef as d, useState as p, useCallback as U, useMemo as j, useLayoutEffect as X, useImperativeHandle as Y, useEffect as O } from "react";
6
- import { MapController as Z } from "@nika-js/onlymap";
7
- import { createPortal as $ } from "react-dom";
8
- const A = J(null);
9
- function F(e) {
10
- const t = B(A);
1
+ var J = Object.defineProperty;
2
+ var Q = (e, t, n) => t in e ? J(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n;
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";
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
+ import { MapController as ne } from "@nika-js/onlymap";
7
+ import { createPortal as B } from "react-dom";
8
+ const H = X(null);
9
+ function K(e) {
10
+ const t = Y(H);
11
11
  if (!t) throw new Error(`[onlymapjs] ${e} must be rendered inside <OmMap>.`);
12
12
  return t;
13
13
  }
14
- function L(e, t) {
14
+ function D(e, t) {
15
15
  if (e === t) return !0;
16
16
  const n = Object.keys(e ?? {}), r = Object.keys(t ?? {});
17
17
  return n.length !== r.length ? !1 : n.every((l) => e[l] === t[l]);
18
18
  }
19
- function ee(e, t) {
20
- const { props: n, updateTriggers: r, ...l } = e, { props: a, updateTriggers: u, ...c } = t;
21
- return L(l, c) && L(n, a) && L(r, u);
19
+ function oe(e, t) {
20
+ const { props: n, updateTriggers: r, ...l } = e, { props: a, updateTriggers: u, ...i } = t;
21
+ return D(l, i) && D(n, a) && D(r, u);
22
22
  }
23
- class te {
23
+ class se {
24
24
  constructor(t) {
25
- D(this, "descriptors", /* @__PURE__ */ new Map());
25
+ A(this, "descriptors", /* @__PURE__ */ new Map());
26
26
  this.controller = t;
27
27
  }
28
28
  upsert(t) {
29
29
  const n = this.descriptors.get(t.id);
30
- n && ee(n, t) || (this.descriptors.set(t.id, t), this.controller.setLayers([...this.descriptors.values()]));
30
+ n && oe(n, t) || (this.descriptors.set(t.id, t), this.controller.setLayers([...this.descriptors.values()]));
31
31
  }
32
32
  remove(t) {
33
33
  this.descriptors.delete(t) && this.controller.setLayers([...this.descriptors.values()]);
34
34
  }
35
35
  }
36
- const _ = ["top-left", "top-right", "bottom-left", "bottom-right"];
37
- function re(e) {
36
+ const q = ["top-left", "top-right", "bottom-left", "bottom-right"];
37
+ function le(e) {
38
38
  return {
39
39
  position: "absolute",
40
40
  [e.startsWith("top") ? "top" : "bottom"]: 10,
@@ -49,94 +49,102 @@ function re(e) {
49
49
  pointerEvents: "none"
50
50
  };
51
51
  }
52
- function ne(e, t) {
52
+ function ie(e, t) {
53
53
  return Object.entries(t).some(([n, r]) => r !== void 0 && e[n] !== r);
54
54
  }
55
- const fe = Q(function(t, n) {
56
- const { center: r, zoom: l, pitch: a, bearing: u, basemap: c, headless: i, className: y, style: x, children: C } = t, E = d(null), [s, f] = p(null), [h, I] = p(null), [P, k] = p({
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({
57
57
  "top-left": null,
58
58
  "top-right": null,
59
59
  "bottom-left": null,
60
60
  "bottom-right": null
61
- }), z = U((o) => {
62
- I((m) => m === o ? m : o);
63
- }, []), K = j(
61
+ }), v = ee((s) => {
62
+ M((h) => h === s ? h : s);
63
+ }, []), w = I(
64
64
  () => Object.fromEntries(
65
- _.map((o) => [
66
- o,
67
- (m) => {
68
- k((R) => R[o] === m ? R : { ...R, [o]: m });
65
+ q.map((s) => [
66
+ s,
67
+ (h) => {
68
+ L((p) => p[s] === h ? p : { ...p, [s]: h });
69
69
  }
70
70
  ])
71
71
  ),
72
72
  []
73
- ), g = d({ center: r, zoom: l, pitch: a, bearing: u });
74
- g.current = { center: r, zoom: l, pitch: a, bearing: u };
75
- const v = d(t.onReady);
76
- v.current = t.onReady;
77
- const b = d(t.onViewStateChange);
78
- b.current = t.onViewStateChange;
79
- const S = d(t.onRuntimeError);
80
- S.current = t.onRuntimeError;
81
- const N = i ? JSON.stringify(i) : "";
82
- X(() => {
83
- if (!E.current) return;
84
- const o = g.current, m = new Z(E.current, {
85
- center: o.center,
86
- zoom: o.zoom,
87
- pitch: o.pitch,
88
- bearing: o.bearing,
89
- basemap: c,
90
- headless: i ? i === !0 ? {} : i : void 0,
91
- onRuntimeError: (M) => {
92
- var W;
93
- return (W = S.current) == null ? void 0 : W.call(S, M);
73
+ ), S = f({ center: r, zoom: l, pitch: a, bearing: u });
74
+ S.current = { center: r, zoom: l, pitch: a, bearing: u };
75
+ const b = f(t.onReady);
76
+ b.current = t.onReady;
77
+ const E = f(t.onViewStateChange);
78
+ E.current = t.onViewStateChange;
79
+ const P = f(t.onRuntimeError);
80
+ P.current = t.onRuntimeError;
81
+ const N = f(i);
82
+ N.current = i;
83
+ const V = f(void 0), G = m ? JSON.stringify(m) : "";
84
+ te(() => {
85
+ if (!d.current) return;
86
+ const s = S.current;
87
+ V.current = N.current;
88
+ const h = new ne(d.current, {
89
+ center: s.center,
90
+ zoom: s.zoom,
91
+ pitch: s.pitch,
92
+ bearing: s.bearing,
93
+ basemap: N.current,
94
+ basemapKey: c,
95
+ attribution: y,
96
+ headless: m ? m === !0 ? {} : m : void 0,
97
+ onRuntimeError: (F) => {
98
+ var $;
99
+ return ($ = P.current) == null ? void 0 : $.call(P, F);
94
100
  }
95
101
  });
96
- f(m);
97
- let R = !0;
98
- return m.ready.then(() => {
99
- var M;
100
- R && ((M = v.current) == null || M.call(v));
102
+ T(h);
103
+ let p = !0;
104
+ return h.ready.then(() => {
105
+ var F;
106
+ p && ((F = b.current) == null || F.call(b));
101
107
  }), () => {
102
- R = !1, f(null), m.destroy();
108
+ p = !1, T(null), h.destroy();
103
109
  };
104
- }, [c, N]), Y(n, () => s, [s]), O(() => {
105
- if (!s) return;
106
- const o = { longitude: r == null ? void 0 : r[0], latitude: r == null ? void 0 : r[1], zoom: l, pitch: a, bearing: u };
107
- ne(s.getViewState(), o) && s.setView(o);
108
- }, [s, r == null ? void 0 : r[0], r == null ? void 0 : r[1], l, a, u]), O(() => {
109
- if (s)
110
- return s.watch(["viewport"], () => {
111
- var o;
112
- return (o = b.current) == null ? void 0 : o.call(b, s.getViewState());
110
+ }, [G]), g(() => {
111
+ !o || V.current === i || (V.current = i, o.setBasemap(i ?? null));
112
+ }, [o, i]), re(n, () => o, [o]), g(() => {
113
+ if (!o) return;
114
+ const s = { longitude: r == null ? void 0 : r[0], latitude: r == null ? void 0 : r[1], zoom: l, pitch: a, bearing: u };
115
+ ie(o.getViewState(), s) && o.setView(s);
116
+ }, [o, r == null ? void 0 : r[0], r == null ? void 0 : r[1], l, a, u]), g(() => {
117
+ if (o)
118
+ return o.watch(["viewport"], () => {
119
+ var s;
120
+ return (s = E.current) == null ? void 0 : s.call(E, o.getViewState());
113
121
  });
114
- }, [s]);
115
- const T = j(() => s ? new te(s) : null, [s]), V = j(
116
- () => s && T && h ? { controller: s, layers: T, overlayPane: h, corners: P } : null,
117
- [s, T, h, P]
122
+ }, [o]);
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]
118
126
  );
119
- return /* @__PURE__ */ G("div", { className: y, style: { position: "relative", overflow: "hidden", ...x }, children: [
120
- /* @__PURE__ */ w("div", { ref: E, style: { position: "absolute", inset: 0 } }),
121
- /* @__PURE__ */ w(
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(
122
130
  "div",
123
131
  {
124
- ref: z,
132
+ ref: v,
125
133
  style: { position: "absolute", top: 0, left: 0, zIndex: 10, pointerEvents: "none" }
126
134
  }
127
135
  ),
128
- _.map((o) => /* @__PURE__ */ w(
136
+ q.map((s) => /* @__PURE__ */ x(
129
137
  "div",
130
138
  {
131
- "data-om-widget-corner": o,
132
- ref: K[o],
133
- style: re(o)
139
+ "data-om-widget-corner": s,
140
+ ref: w[s],
141
+ style: le(s)
134
142
  },
135
- o
143
+ s
136
144
  )),
137
- V && /* @__PURE__ */ w(A.Provider, { value: V, children: C })
145
+ _ && /* @__PURE__ */ x(H.Provider, { value: _, children: R })
138
146
  ] });
139
- }), oe = /* @__PURE__ */ new Set([
147
+ }), ae = /* @__PURE__ */ new Set([
140
148
  "id",
141
149
  "type",
142
150
  "data",
@@ -155,10 +163,10 @@ const fe = Q(function(t, n) {
155
163
  "onClick",
156
164
  "onHover"
157
165
  ]);
158
- function de(e) {
159
- const { controller: t, layers: n } = F("OmLayer"), { id: r } = e, l = {};
160
- for (const [i, y] of Object.entries(e))
161
- !oe.has(i) && y !== void 0 && (l[i] = y);
166
+ function ge(e) {
167
+ const { controller: t, layers: n } = K("OmLayer"), { id: r } = e, l = {};
168
+ for (const [c, y] of Object.entries(e))
169
+ !ae.has(c) && y !== void 0 && (l[c] = y);
162
170
  const a = {
163
171
  id: r,
164
172
  type: e.type,
@@ -177,32 +185,32 @@ function de(e) {
177
185
  updateTriggers: e.updateTriggers,
178
186
  props: l
179
187
  };
180
- O(() => {
188
+ g(() => {
181
189
  n.upsert(a);
182
- }), O(() => () => n.remove(r), [n, r]);
183
- const u = d(e.onClick);
190
+ }), g(() => () => n.remove(r), [n, r]);
191
+ const u = f(e.onClick);
184
192
  u.current = e.onClick;
185
- const c = d(e.onHover);
186
- return c.current = e.onHover, O(
187
- () => t.watchPicks((i) => {
188
- var y, x, C;
189
- if (i === null) {
190
- (y = c.current) == null || y.call(c, null);
193
+ const i = f(e.onHover);
194
+ return i.current = e.onHover, g(
195
+ () => t.watchPicks((c) => {
196
+ var y, m, O;
197
+ if (c === null) {
198
+ (y = i.current) == null || y.call(i, null);
191
199
  return;
192
200
  }
193
- i.layerId === r && (i.type === "click" ? (x = u.current) == null || x.call(u, i) : (C = c.current) == null || C.call(c, i));
201
+ c.layerId === r && (c.type === "click" ? (m = u.current) == null || m.call(u, c) : (O = i.current) == null || O.call(i, c));
194
202
  }),
195
203
  [t, r]
196
204
  ), null;
197
205
  }
198
- function ye({ position: e = "top-left", className: t, style: n, children: r }) {
199
- const { corners: l } = F("OmWidget"), a = l[e];
200
- return a ? $(
201
- /* @__PURE__ */ w("div", { className: t, style: { pointerEvents: "auto", ...n }, children: r }),
206
+ function ve({ position: e = "top-left", className: t, style: n, children: r }) {
207
+ const { corners: l } = K("OmWidget"), a = l[e];
208
+ return a ? B(
209
+ /* @__PURE__ */ x("div", { className: t, style: { pointerEvents: "auto", ...n }, children: r }),
202
210
  a
203
211
  ) : null;
204
212
  }
205
- const se = {
213
+ const ce = {
206
214
  "top-left": "translate(0, 0)",
207
215
  "top-center": "translate(-50%, 0)",
208
216
  "top-right": "translate(-100%, 0)",
@@ -213,32 +221,32 @@ const se = {
213
221
  "bottom-center": "translate(-50%, -100%)",
214
222
  "bottom-right": "translate(-100%, -100%)"
215
223
  };
216
- function he(e) {
217
- const { controller: t, overlayPane: n } = F("OmOverlay"), r = d(null), l = d(void 0), [a, u] = p(null), c = d(e);
218
- c.current = e, O(
219
- () => t.registerOverlay((s, f) => {
220
- const h = r.current;
221
- if (!h) return;
222
- const { anchor: I, anchorFrom: P, layer: k, visible: z = !0, anchorOffset: K } = c.current;
223
- let g;
224
- if (P === "selection" ? ((!k || (f == null ? void 0 : f.layerId) === k) && (l.current = (f == null ? void 0 : f.coordinate) ?? void 0, u(f)), g = l.current) : g = I, !z || !g || !s) {
225
- h.style.visibility = "hidden";
224
+ function be(e) {
225
+ const { controller: t, overlayPane: n } = K("OmOverlay"), r = f(null), l = f(void 0), [a, u] = k(null), i = f(e);
226
+ i.current = e, g(
227
+ () => t.registerOverlay((R, d) => {
228
+ const o = r.current;
229
+ if (!o) return;
230
+ const { anchor: T, anchorFrom: C, layer: M, visible: j = !0, anchorOffset: L } = i.current;
231
+ 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) {
233
+ o.style.visibility = "hidden";
226
234
  return;
227
235
  }
228
- const [v, b, S] = s.project([g[0], g[1], 0]);
229
- if (!(v >= 0 && v <= s.width && b >= 0 && b <= s.height) || S !== void 0 && S < 0) {
230
- h.style.visibility = "hidden";
236
+ const [w, S, b] = R.project([v[0], v[1], 0]);
237
+ if (!(w >= 0 && w <= R.width && S >= 0 && S <= R.height) || b !== void 0 && b < 0) {
238
+ o.style.visibility = "hidden";
231
239
  return;
232
240
  }
233
- h.style.visibility = "visible", h.style.transform = `translate(${v}px, ${b}px) ${se[K ?? "bottom-center"]}`;
241
+ o.style.visibility = "visible", o.style.transform = `translate(${w}px, ${S}px) ${ce[L ?? "bottom-center"]}`;
234
242
  }),
235
243
  [t]
236
- ), O(() => {
244
+ ), g(() => {
237
245
  t.refreshOverlays();
238
246
  });
239
- const { children: i, className: y, style: x, interactive: C = !0 } = e, E = typeof i == "function" ? i(a) : i;
240
- return $(
241
- /* @__PURE__ */ w(
247
+ const { children: c, className: y, style: m, interactive: O = !0 } = e, z = typeof c == "function" ? c(a) : c;
248
+ return B(
249
+ /* @__PURE__ */ x(
242
250
  "div",
243
251
  {
244
252
  ref: r,
@@ -250,23 +258,23 @@ function he(e) {
250
258
  visibility: "hidden",
251
259
  // hidden until the first flush positions it — never flashes at the origin
252
260
  willChange: "transform",
253
- pointerEvents: C ? "auto" : "none",
254
- ...x
261
+ pointerEvents: O ? "auto" : "none",
262
+ ...m
255
263
  },
256
- children: E
264
+ children: z
257
265
  }
258
266
  ),
259
267
  n
260
268
  );
261
269
  }
262
- function me(e = []) {
263
- const { controller: t } = F("useOmMap"), n = e.join(" "), r = j(() => n ? n.split(" ") : [], [n]), [l, a] = p(() => t.buildCtx());
264
- return O(() => t.watch(r, a), [t, r]), l;
270
+ function Oe(e = []) {
271
+ const { controller: t } = K("useOmMap"), n = e.join(" "), r = I(() => n ? n.split(" ") : [], [n]), [l, a] = k(() => t.buildCtx());
272
+ return g(() => t.watch(r, a), [t, r]), l;
265
273
  }
266
274
  export {
267
- de as OmLayer,
268
- fe as OmMap,
269
- he as OmOverlay,
270
- ye as OmWidget,
271
- me as useOmMap
275
+ ge as OmLayer,
276
+ he as OmMap,
277
+ be as OmOverlay,
278
+ ve as OmWidget,
279
+ Oe as useOmMap
272
280
  };
@@ -56,6 +56,13 @@ export interface HeadlessOptions {
56
56
  width: number;
57
57
  height: number;
58
58
  }
59
+ /** Basemap-preset options (spec: "Basemap presets & switching") — the publishable provider key and the attribution opt-out. */
60
+ export interface BasemapRuntimeOptions {
61
+ /** `basemap-key` — substituted into keyed presets' `{key}` placeholder (falls back to configureBasemap). */
62
+ key?: string;
63
+ /** `attribution="false"` opt-out — default true: the compact attribution control renders whenever a basemap is active. */
64
+ attribution?: boolean;
65
+ }
59
66
  export declare class RuntimeCore {
60
67
  private deck?;
61
68
  private basemap?;
@@ -77,7 +84,33 @@ export declare class RuntimeCore {
77
84
  private destroyed;
78
85
  private viewState;
79
86
  private callbacks;
80
- constructor(parent: HTMLElement, initialView: InitialView, callbacks?: RuntimeCoreCallbacks, basemapAttr?: string, headless?: HeadlessOptions);
87
+ private parent;
88
+ /** The live `basemap` attribute value — re-read by the lazy adapter chunk so a switch during its load lands the LATEST style. */
89
+ private basemapAttr?;
90
+ private basemapOptions?;
91
+ /** Bumped on every renderer (re)init — a superseded lazy chunk load must not install its adapter over a newer renderer. */
92
+ private rendererGeneration;
93
+ constructor(parent: HTMLElement, initialView: InitialView, callbacks?: RuntimeCoreCallbacks, basemapAttr?: string, headless?: HeadlessOptions, basemapOptions?: BasemapRuntimeOptions);
94
+ /** `basemap` attribute → concrete style, logging resolution problems (unknown preset / missing key) — the demo-style fallback still renders. */
95
+ private resolveBasemap;
96
+ /**
97
+ * Constructs the renderer for the requested mode — the constructor's
98
+ * body, extracted so setBasemap() can remount on a renderer-MODE change
99
+ * (none ↔ maplibre). Reads/seeds the camera from `this.viewState`.
100
+ */
101
+ private initRenderer;
102
+ /**
103
+ * Live basemap change (spec: "Basemap presets & switching"), both paths:
104
+ * - maplibre → maplibre: `map.setStyle()` — deck layers survive (the
105
+ * overlay is a control, not style layers) and the camera is untouched.
106
+ * - renderer-MODE change (none ↔ maplibre, or a mapbox fallback): full
107
+ * remount seeded with the current camera; the retained IRs are re-handed
108
+ * to the new renderer, so the scene carries over.
109
+ * Headless ignores it (no renderer to switch); `om-map-ready` semantics
110
+ * are unaffected (readiness settles once, at initial load).
111
+ */
112
+ setBasemap(attr: string | null): void;
113
+ private teardownRenderer;
81
114
  /**
82
115
  * Rebuilds every deck.gl Layer instance from the current IR and hands the
83
116
  * full array to deck.gl (or the basemap's MapboxOverlay) on every
@@ -11,6 +11,8 @@ export interface WidgetHost {
11
11
  $$(sel: string): NodeListOf<Element>;
12
12
  getAttribute(name: string): string | null;
13
13
  hasAttribute(name: string): boolean;
14
+ /** Element.closest — satisfied structurally by the widget element itself; widgets needing owning-map attributes use closest("om-map") (e.g. basemap-switcher's current selection). */
15
+ closest(selector: string): Element | null;
14
16
  /** Per-instance mutable stash for state that must survive across renders (e.g. a Vega view to `.finalize()` before re-embedding). */
15
17
  state: Record<string, unknown>;
16
18
  }
@@ -0,0 +1,89 @@
1
+ # Basemaps
2
+
3
+ The `basemap` attribute on `<om-map>` selects what renders under your data layers. It accepts a **preset name**, a **MapLibre style URL**, `"maplibre"` (the demo style), or `"none"` (standalone deck.gl canvas). No preset needs a token except the MapTiler ones.
4
+
5
+ ```html
6
+ <om-map center="[-122.42, 37.77]" zoom="12" basemap="positron">
7
+ …layers, widgets…
8
+ <om-widget type="basemap-switcher" position="top-right"></om-widget>
9
+ </om-map>
10
+ ```
11
+
12
+ ## Built-in presets
13
+
14
+ | Name | Provider | Key | Notes |
15
+ |---|---|---|---|
16
+ | `liberty` | [OpenFreeMap](https://openfreemap.org) | none | Full-detail OSM vector style. No registration, no limits — production use invited. |
17
+ | `bright` | OpenFreeMap | none | High-contrast OSM vector style. |
18
+ | `positron` | OpenFreeMap | none | Ultra-clean light style — the classic dataviz backdrop. |
19
+ | `dark-matter` | [CARTO](https://github.com/CartoDB/basemap-styles) | none | Dark dataviz style. Attribution required (rendered automatically). |
20
+ | `voyager` | CARTO | none | Balanced light style with labels. |
21
+ | `osm` | [openstreetmap.org](https://operations.osmfoundation.org/policies/tiles/) | none | The classic raster look. **Policy-constrained**: donation-funded, no uptime guarantee; commercial apps should prefer a vector preset or a paid provider. |
22
+ | `maptiler-streets` / `maptiler-dataviz` / `maptiler-satellite` | [MapTiler](https://docs.maptiler.com/cloud/api/maps/) | **required** | Free tier available; the satellite preset is the one imagery option here. |
23
+ | `maplibre` | MapLibre demo tiles | none | Country-level outlines only — fine for smoke tests, not production. |
24
+
25
+ ## Switching at runtime
26
+
27
+ The `basemap` attribute is **live** — write it and the map switches in place, keeping the camera and every deck.gl layer:
28
+
29
+ ```js
30
+ document.querySelector("om-map").setAttribute("basemap", "dark-matter");
31
+ ```
32
+
33
+ Three equivalent surfaces, same as every action:
34
+
35
+ ```html
36
+ <om-widget type="basemap-switcher" options="positron dark-matter liberty osm"></om-widget>
37
+ <button data-emit="set-basemap" data-basemap="dark-matter">Dark</button>
38
+ <om-behavior on="load" action="set-basemap" basemap="voyager"></om-behavior>
39
+ ```
40
+
41
+ Omit `options` and the switcher lists every registered keyless preset. Switching between `none` and a basemap changes the renderer, so it remounts under the hood — still seamless (camera and layers carry over), just heavier than a style swap.
42
+
43
+ ## MapTiler: keys and custom-edited styles
44
+
45
+ MapTiler keys are **publishable, origin-restricted client keys** — they belong in page source (restrict them to your domains in the MapTiler dashboard). Two ways to supply one:
46
+
47
+ ```html
48
+ <om-map basemap="maptiler-dataviz" basemap-key="YOUR_KEY">
49
+ ```
50
+ ```js
51
+ OmMap.configureBasemap({ maptilerKey: "YOUR_KEY" }); // once, covers every maptiler-* preset
52
+ ```
53
+
54
+ To **customize a basemap visually**, use MapTiler's Customize editor: pick a base style, edit colors/fonts/labels in the browser (it's editing MapLibre style JSON under the hood), save — you get a style URL for your edited style. Use it directly, or give it a name:
55
+
56
+ ```html
57
+ <om-map basemap="https://api.maptiler.com/maps/YOUR_STYLE_ID/style.json?key=YOUR_KEY">
58
+ ```
59
+ ```js
60
+ OmMap.registerBasemap("brand", {
61
+ style: "https://api.maptiler.com/maps/YOUR_STYLE_ID/style.json?key={key}",
62
+ label: "Brand",
63
+ requiresKey: "maptiler",
64
+ });
65
+ ```
66
+
67
+ ## Register your own
68
+
69
+ `OmMap.registerBasemap(name, preset)` takes any MapLibre style URL **or inline style object** — self-hosted styles, other providers, or JSON-level edits of a preset:
70
+
71
+ ```js
72
+ const style = await (await fetch("https://tiles.openfreemap.org/styles/positron")).json();
73
+ style.layers.find((l) => l.id === "background").paint["background-color"] = "#f4efe9";
74
+ OmMap.registerBasemap("warm-positron", { style, label: "Warm Positron" });
75
+ ```
76
+
77
+ Registered names show up in the switcher widget, validation, and (after `npx @nika-js/onlymap init` regeneration in your own tooling) autocomplete.
78
+
79
+ ## Attribution
80
+
81
+ Map data providers require attribution — OSM, OpenFreeMap, and CARTO all carry it in their style sources, and OnlyMapJS renders MapLibre's compact attribution control automatically whenever a basemap is active. Preset-level `attribution` text is appended. If you render your own credits, opt out with `attribution="false"` on `<om-map>` — but make sure your replacement satisfies the provider's terms.
82
+
83
+ ## Validation
84
+
85
+ A typo'd preset name or a keyed preset without a key is a **validation error** (the live map falls back to the demo style so it still renders): `OmMap.validate(html)` lists the registered names or the two key routes in the `fix`.
86
+
87
+ ## React
88
+
89
+ Same contract on the adapter: `<OmMap basemap="positron" basemapKey="…">` — changing the `basemap` prop switches live (style-only changes keep camera + layers). There's no switcher widget in React; it's a `useState` + the prop. `set-basemap` is not dispatched on the programmatic front-end — use the prop or `controller.setBasemap(name)`.
package/docs/react.md CHANGED
@@ -54,7 +54,7 @@ Actions that mutate manifest attributes (`toggle-layer`, `show-overlay`, `fade`,
54
54
  | Prop | Notes |
55
55
  |---|---|
56
56
  | `center` `zoom` `pitch` `bearing` | Initial camera; later prop changes move the camera (instant). While they're unchanged, user panning is never fought. |
57
- | `basemap` | Same contract as the attribute — `"maplibre"`, a style URL, or omit for standalone deck.gl. |
57
+ | `basemap` | Same contract as the attribute — a preset name (`"positron"`, `"dark-matter"`, `"osm"`, …), a style URL, or omit for standalone deck.gl. Changing the prop switches live (camera + layers survive); `basemapKey` supplies the MapTiler key. See [docs/basemaps.md](basemaps.md). |
58
58
  | `headless` | `true` or `{ width, height }` — no renderer, real projection math (tests under jsdom/happy-dom). |
59
59
  | `onReady` | Renderer up + first commit + no data URL still loading. |
60
60
  | `onViewStateChange` | Every camera change, with the current `CameraState`. |
package/llms.txt CHANGED
@@ -16,11 +16,11 @@ 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="maplibre">` — the root. `basemap` accepts `maplibre` (default style, no token), a style URL, or `none` (standalone canvas). `pitch`/`bearing` tilt the initial camera (use for 3D content). `validate` attribute enables live validation + on-page error panel.
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
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|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.
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.
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`. 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`. One payload contract everywhere: `{ layer, target, feature, featureId, coordinate }`.
24
24
  - 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`.
25
25
  - `<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).
26
26
  - 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="...">`.