@nika-js/onlymap 0.5.2 → 0.5.6

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 (35) hide show
  1. package/README.md +17 -5
  2. package/THIRD-PARTY-LICENSES.md +93 -0
  3. package/dist/{Arrow.dom-BNjbQ9jb.js → Arrow.dom-BFGShwSA.js} +854 -1427
  4. package/dist/{LercDecode.es-ReXBgmrK.js → LercDecode.es-5mr_B7pP.js} +1 -1
  5. package/dist/{basemap-DF_rK3Aa.js → basemap-DJjFEgA9.js} +1 -1
  6. package/dist/builder-CqJZRFaq.js +580 -0
  7. package/dist/date-format.d.ts +18 -0
  8. package/dist/draw-controller.d.ts +10 -1
  9. package/dist/flatgeobuf-Cx0IwxhV.js +6332 -0
  10. package/dist/geodesy.d.ts +47 -0
  11. package/dist/geoparquet-AL10HAyd.js +35 -0
  12. package/dist/geoparquet.d.ts +33 -0
  13. package/dist/html-data.d.ts +2 -2
  14. package/dist/{index-DWXl9Ykk.js → index-BQaxwlV3.js} +1 -1
  15. package/dist/index-BYbMtNVH.js +2301 -0
  16. package/dist/{index-B87QAKKr.js → index-BtPO4p2v.js} +2 -2
  17. package/dist/index-CgPV7QlM.js +4019 -0
  18. package/dist/{index-BYW4X7IK.js → index-DqE9di9E.js} +1 -1
  19. package/dist/{index-ytnYzWRW.js → index-G3hmIs2P.js} +47016 -46502
  20. package/dist/{index-DcFZsjQm.js → index-iwo3R3LT.js} +1 -1
  21. package/dist/index.d.ts +4 -0
  22. package/dist/{lerc-C_VBg2kh.js → lerc-Dx3uB2j-.js} +2 -2
  23. package/dist/measure-controller.d.ts +41 -0
  24. package/dist/onlymap.standalone.js +108100 -94902
  25. package/dist/onlymapjs.js +82 -67
  26. package/dist/{raster-Unx9530f.js → raster-BPn_dJpK.js} +2 -2
  27. package/dist/units.d.ts +40 -0
  28. package/dist/validation.d.ts +10 -0
  29. package/dist/version.d.ts +1 -1
  30. package/llms.txt +8 -5
  31. package/onlymapjs.html-data.json +141 -62
  32. package/package.json +4 -1
  33. package/skills/onlymapjs/SKILL.md +4 -1
  34. package/skills/onlymapjs/references/syntax.md +103 -19
  35. package/skills/onlymapjs/references/testing.md +11 -2
@@ -16,8 +16,8 @@ Vite/npm project:
16
16
  Static CDN page (raw-file CDNs only — unpkg/jsDelivr; never esm.sh or another rebundling CDN, which duplicates the WebGL runtime and breaks layer shaders):
17
17
 
18
18
  ```html
19
- <link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.5.2/dist/onlymapjs.css">
20
- <script type="module" src="https://unpkg.com/@nika-js/onlymap@0.5.2"></script>
19
+ <link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.5.6/dist/onlymapjs.css">
20
+ <script type="module" src="https://unpkg.com/@nika-js/onlymap@0.5.6"></script>
21
21
  ```
22
22
 
23
23
  Always include `onlymapjs.css` — it carries the MapLibre basemap styles and the no-JS fallback rules (`<om-fallback>` / default banner). For the fallback to work in script-disabled previews it must load without JavaScript: a real `<link rel="stylesheet">` or inlined `<style>` on no-build pages (a bundler-emitted stylesheet is fine in npm projects).
@@ -81,10 +81,21 @@ get-position="[$lon, $lat]"
81
81
  get-radius="$population * 0.001"
82
82
  get-fill-color="$value > 100 ? [255,0,0] : [0,128,255]"
83
83
  get-fill-color="scale($depth, sequential, ['#ffffcc','#800026'], domain=[0,700])"
84
+ get-text="formatDate($time, 'datetime', 'UTC')"
84
85
  ```
85
86
 
86
87
  `$field` works on flat rows, GeoJSON properties, columnar JSON, CSV/TSV columns, and Arrow point columns.
87
88
 
89
+ Date formatting is a safe built-in rather than an opt-in to arbitrary JavaScript:
90
+
91
+ ```html
92
+ get-text="formatDate($time)"
93
+ get-text="formatDate($time, 'date')"
94
+ get-text="formatDate($time, 'datetime', 'Asia/Singapore')"
95
+ ```
96
+
97
+ `formatDate(value, style?, timeZone?)` accepts epoch-millisecond numbers/numeric strings or ISO date strings. Numeric values are always milliseconds—multiply Unix seconds by 1000 before formatting. Styles are `date`, `datetime` (default), `time`, and `iso`; the default time zone is `UTC`, while `local` opts into the viewer's zone and IANA names select a specific zone. Invalid values produce an empty string. Restricted expressions still reject `new Date()`, `Intl`, and instance-method calls.
98
+
88
99
  Smart shorthands:
89
100
 
90
101
  - `color="#1f9e89"` sets a constant `getFillColor` when no explicit `get-fill-color` exists, and always feeds the legend swatch.
@@ -102,6 +113,17 @@ Filtering:
102
113
  filter-field="magnitude" filter-range="[4, 10]"
103
114
  ```
104
115
 
116
+ For an epoch-millisecond filter, format the built-in widget's numeric labels declaratively:
117
+
118
+ ```html
119
+ <om-layer id="quakes" type="GeoJsonLayer" data="./quakes.geojson"
120
+ filter-field="time" filter-range="[1782889284760, 1785480717910]"></om-layer>
121
+ <om-widget type="filter" layer="quakes" field="time"
122
+ format="date" date-style="datetime" time-zone="UTC"></om-widget>
123
+ ```
124
+
125
+ `format` is `number` (default) or `date`. With `format="date"`, `date-style` uses the same four styles as `formatDate()` and `time-zone` defaults to `UTC`.
126
+
105
127
 
106
128
 
107
129
  ### Built-In Layer Types
@@ -151,6 +173,9 @@ External layer classes become manifest types via `OmMap.registerLayer({type, dec
151
173
  | Arrow / GeoArrow IPC | `data="./big.arrow"` | Points stay columnar; lines/polygons become GeoJSON features. |
152
174
  | Shapefile | `data="./countries.shp"` | Loads sidecars and joins `.dbf` attributes. |
153
175
  | KML | `data="./tour.kml"` | Placemarks become GeoJSON features. |
176
+ | GPX | `data="./hike.gpx"` (opt. `#waypoints`/`#tracks`/`#routes`) | Waypoints/tracks/routes → GeoJSON features, each tagged `_gpxKind`; a URL fragment selects one part (no fragment = all). |
177
+ | FlatGeobuf | `data="./cities.fgb"` | Cloud-native binary vector — whole-file decode to GeoJSON features (bbox-streaming is a later phase). |
178
+ | GeoParquet | `data="./data.parquet"` (or `.geoparquet`) | Cloud-native columnar vector — all-Point files stay columnar, lines/polygons become GeoJSON features; needs the `geo` metadata (WKB geometry) and CRS84/EPSG:4326 (a projected CRS errors — reproject first). |
154
179
  | CityJSON | `data="./tile.city.json"` | 3D city models → extruded footprints, or `?om-surfaces=1` for real per-face roof geometry; see below. |
155
180
  | CityJSONSeq | `data="./tile.city.jsonl"` | Same, streamed line by line as it downloads. |
156
181
  | WebSocket | `data="wss://feed" key="id" flush="250ms" source="decoder"` | Upsert-by-key stream. |
@@ -266,10 +291,11 @@ Built-ins:
266
291
  - `legend` — symbology-aware by default: it parses each layer's `get-fill-color`. A `sequential`/`diverging` `scale()` renders as a gradient ramp with the domain ends labeled; a `threshold` scale as discrete class ranges (`< b1`, `b1 – b2`, `≥ bN`); an equality ternary chain (`$f == 'a' ? '#c1' : $f == 'b' ? '#c2' : '#fallback'`) as a category palette with an "other" row. Any other expression falls back to the single `color` swatch — so writing the canonical shapes buys a self-describing legend for free.
267
292
  - `layer-switcher`
268
293
  - `zoom-controls`
269
- - `scale-bar`
294
+ - `scale-bar` — `units="metric|imperial|nautical"` (default metric) picks the length system; snaps to a nice round distance.
270
295
  - `attribution`
271
296
  - `filter`
272
297
  - `draw`
298
+ - `measure` — geodesic ruler: `modes="distance area"` (space-separated; default both), `units="metric|imperial|nautical"`. Click the map to place points; live per-segment + total labels render on the map, and a totals panel + a `units` toggle sit in the widget. Distance is haversine on the WGS84 mean sphere (≤0.56% vs. the true geodesic); area is the spherical-excess integral. Nautical shows nmi for length and falls back to metric for area. Reuses the draw capture stack (measure and draw are mutually exclusive); the geometry is ephemeral (never saved, never an undo step). Consume the reading programmatically via the `om-measure` event on `<om-map>` (`detail = {mode, units, totalMeters, segments, areaMeters2, perimeterMeters, poleWarning}`).
273
299
  - `vega-lite`
274
300
  - `player`
275
301
  - `basemap-switcher` — radio list of presets; `options="positron dark-matter osm"` (default: every keyless registered preset)
@@ -295,11 +321,30 @@ Examples:
295
321
  ```html
296
322
  <om-widget type="legend" position="bottom-right" title="Layers" interactive></om-widget>
297
323
  <om-widget type="filter" layer="quakes" field="magnitude" position="top-left"></om-widget>
324
+ <om-widget type="filter" layer="quakes" field="time" format="date"
325
+ date-style="datetime" time-zone="UTC" position="bottom-center"></om-widget>
298
326
  <om-widget type="draw" target="sketch" modes="point line polygon" save="both"></om-widget>
299
327
  <om-widget type="basemap-switcher" options="positron dark-matter liberty osm" position="top-right"></om-widget>
300
328
  ```
301
329
 
302
- Custom widget:
330
+ ### Custom widgets & event emission
331
+
332
+ **Reach for a built-in first.** A plain value/time slider is a built-in:
333
+ `<om-widget type="filter" layer="quakes" field="time" format="date">` — it
334
+ renders readable date labels AND wires the `filter-layer` action for you (pair
335
+ it with the layer's `filter-field`). Author a custom widget ONLY for bespoke UI
336
+ or logic the built-ins don't cover — most "it looked right but didn't work"
337
+ widgets should have been a `type="filter"`/`legend`/`vega-lite` built-in.
338
+
339
+ A custom widget is an `<om-widget>` with **no `type`** plus inline HTML and a
340
+ `<script type="om/widget">` block. The script is full JS, evaluated ONCE at
341
+ connect with `this` bound to the `<om-widget>` element: set `this.watch` (a
342
+ list of re-render triggers) and `this.render = (ctx) => {…}`. `vegaEmbed`/`d3`
343
+ are available. Content renders in shadow DOM — reach it with `this.$(selector)`
344
+ and `this.root`.
345
+
346
+ **Read map state** through `ctx` (the `render` argument), re-rendering when a
347
+ watched token fires:
303
348
 
304
349
  ```html
305
350
  <om-widget position="top-left">
@@ -314,20 +359,60 @@ Custom widget:
314
359
  </om-widget>
315
360
  ```
316
361
 
317
- Widget context:
318
-
319
- - `ctx.layers`
320
- - `ctx.data(id)`
321
- - `ctx.dataInViewport(id)`
322
- - `ctx.stats(id, field, { scope: "viewport" })`
323
- - `ctx.selection`
324
- - `ctx.viewport`
325
- - `ctx.history` `{ canUndo, canRedo }`; re-render on changes via the `history` watch token
326
- - `ctx.emit(action, payload)`
327
-
328
- Watch tokens: `data:<layerId>`, `viewport`, `selection`, `layers` (fires on layer add/remove, visibility, and filter changes), `basemap`, `lighting`, `terrain`, `history`, `widgets` (fires on a `widgets-hidden` hide-all toggle).
329
-
330
- Use `this.$()` and `this.root`; widgets render in shadow DOM.
362
+ - `ctx.layers`, `ctx.data(id)`, `ctx.dataInViewport(id)`,
363
+ `ctx.stats(id, field, { scope: "viewport" })`, `ctx.selection`,
364
+ `ctx.viewport`, `ctx.history` (`{ canUndo, canRedo }`).
365
+ - Watch tokens: `data:<layerId>`, `viewport`, `selection`, `layers` (add/remove,
366
+ visibility, filter changes), `basemap`, `lighting`, `terrain`, `history`,
367
+ `widgets` (`widgets-hidden` hide-all toggle).
368
+
369
+ **Drive the map — the emission contract.** This is the part authors get wrong.
370
+ A widget NEVER mutates the map directly and NEVER dispatches its own
371
+ `CustomEvent` hoping the map listens. It **emits a registered action**, exactly
372
+ two ways:
373
+
374
+ 1. **Declarative `data-emit`** (no script): `data-emit="<action>"` + `data-*`
375
+ payload keys on an element. Fires on **click** (non-form elements) or
376
+ **change** (form controls — `input`/`select`/`textarea`, whose `.value` is
377
+ auto-merged into the payload). `data-*` keys camelCase (`data-feature-id` →
378
+ `featureId`) and arrive as **strings** — fine for `toggle-layer`/`fly-to`,
379
+ but a numeric/array payload (a slider's `range: [min, max]`) needs `ctx.emit`.
380
+ ```html
381
+ <button data-emit="toggle-layer" data-layer="quakes">Toggle quakes</button>
382
+ ```
383
+
384
+ 2. **Programmatic `ctx.emit(action, payload)`** — for typed payloads. Wire it
385
+ INSIDE `render` so `ctx` is in scope, assigning `.oninput`/`.onclick`
386
+ (idempotent across re-renders; prefer over `addEventListener`, which stacks a
387
+ fresh listener every render):
388
+ ```html
389
+ <om-widget position="bottom-center">
390
+ <input id="day" type="range" min="1" max="14" step="1" value="1">
391
+ <script type="om/widget">
392
+ this.render = (ctx) => {
393
+ this.$("#day").oninput = (e) => {
394
+ const d = Number(e.target.value);
395
+ ctx.emit("filter-layer", { layer: "quakes", field: "day", range: [d, d] });
396
+ };
397
+ };
398
+ </script>
399
+ </om-widget>
400
+ ```
401
+
402
+ **NEVER inline handlers** (`onclick="…"`, `oninput="ctx.emit(…)"`): `ctx` is not
403
+ a global, they execute in page scope, and CSP blocks them — the widget looks
404
+ right and silently emits nothing. This is the #1 custom-widget failure, and
405
+ validation errors on it.
406
+
407
+ **Actions a widget can emit** (payload keys): `filter-layer`
408
+ `{layer, field?, range:[min,max]}` (value/time sliders), `toggle-layer`
409
+ `{layer, visible?}`, `fly-to` `{center:[lng,lat], zoom?, pitch?, bearing?,
410
+ duration?}`, `zoom-to-feature` `{layer, featureId, duration?}`, `set-basemap`
411
+ `{basemap}`, `set-lighting`/`set-terrain`, `highlight-feature`
412
+ `{layer, featureId}`, `show-overlay`/`hide-overlay` `{target}`,
413
+ `story-play`/`story-pause`/`story-seek` `{story, t?}`, `undo`/`redo`,
414
+ `zoom-in`/`zoom-out`, `set-widgets-visible` `{visible}`. Register your own with
415
+ `OmMap.registerAction(name, (payload, mapEl) => …)`.
331
416
 
332
417
  ### `<om-overlay>`
333
418
 
@@ -493,4 +578,3 @@ For 3D Tiles LOD/refinement experiments, use:
493
578
  maximum-memory-usage="256"
494
579
  view-distance-scale="0.85"></om-layer>
495
580
  ```
496
-
@@ -10,11 +10,20 @@ Use `OmMap.validate(htmlString)` before finalizing agent-written maps.
10
10
  import { OmMap } from "@nika-js/onlymap";
11
11
 
12
12
  const result = OmMap.validate(html);
13
- if (!result.valid) {
14
- console.log(result.errors);
13
+ // `valid` reflects ERRORS ONLY. Warnings (e.g. `Unknown attribute "get-radius"
14
+ // on GeoJsonLayer — likely a typo`) mean a prop is being silently dropped and
15
+ // the layer renders wrong-but-"valid", so act on BOTH lists — never stop at
16
+ // `valid: true`.
17
+ for (const entry of [...result.errors, ...result.warnings]) {
18
+ console.log(entry.severity, entry.attribute, entry.message, "→", entry.fix);
15
19
  }
16
20
  ```
17
21
 
22
+ An `Unknown attribute` warning is usually a wrong-for-this-layer prop — check
23
+ `onlymapjs.html-data.json` for the layer's real attributes (e.g. `GeoJsonLayer`
24
+ points use `get-point-radius`/`point-radius-units`, not `ScatterplotLayer`'s
25
+ `get-radius`/`radius-units`).
26
+
18
27
  Errors and warnings have:
19
28
 
20
29
  ```ts