@nika-js/onlymap 0.5.3 → 0.5.7

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 (38) hide show
  1. package/README.md +20 -7
  2. package/THIRD-PARTY-LICENSES.md +120 -0
  3. package/dist/{Arrow.dom-BNjbQ9jb.js → Arrow.dom-BFGShwSA.js} +854 -1427
  4. package/dist/{LercDecode.es-S6KKhkuA.js → LercDecode.es-Bp2rG98n.js} +1 -1
  5. package/dist/{basemap-BSCq9hHZ.js → basemap-CfOA0HWY.js} +1 -1
  6. package/dist/builder-CqJZRFaq.js +580 -0
  7. package/dist/data-layer.d.ts +4 -0
  8. package/dist/date-format.d.ts +18 -0
  9. package/dist/flatgeobuf-Cx0IwxhV.js +6332 -0
  10. package/dist/full.esm-Cl_Dig1y.js +1467 -0
  11. package/dist/geoparquet-kW7KUrwB.js +35 -0
  12. package/dist/geoparquet.d.ts +33 -0
  13. package/dist/image-overlay.d.ts +83 -0
  14. package/dist/{index-BCgjJcZn.js → index-BLy1eTWe.js} +1 -1
  15. package/dist/index-BYbMtNVH.js +2301 -0
  16. package/dist/{index-BoAq5XsU.js → index-CUKgoX_E.js} +13291 -12728
  17. package/dist/index-CgPV7QlM.js +4019 -0
  18. package/dist/{index-D1swbykc.js → index-CnY_rdB8.js} +1 -1
  19. package/dist/{index-DfNGlG_k.js → index-MRS7UAHr.js} +1 -1
  20. package/dist/{index-BTcRCrfX.js → index-wZ6n6ggy.js} +2 -2
  21. package/dist/index.d.ts +5 -1
  22. package/dist/ir-snapshot.d.ts +3 -1
  23. package/dist/ir.d.ts +18 -0
  24. package/dist/layer-registry.d.ts +2 -0
  25. package/dist/{lerc-n0GaWEmL.js → lerc-DWbkmLYR.js} +2 -2
  26. package/dist/onlymap.standalone.js +59664 -44945
  27. package/dist/onlymapjs.js +76 -74
  28. package/dist/{raster-DO-Ex2Lb.js → raster-5jEwGISN.js} +2 -2
  29. package/dist/validation.d.ts +1 -1
  30. package/dist/version.d.ts +1 -1
  31. package/docs/3d-assets.md +1 -1
  32. package/docs/image-overlays.md +75 -0
  33. package/llms.txt +7 -4
  34. package/onlymapjs.html-data.json +147 -86
  35. package/package.json +5 -1
  36. package/skills/onlymapjs/SKILL.md +4 -1
  37. package/skills/onlymapjs/references/syntax.md +120 -19
  38. 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.3/dist/onlymapjs.css">
20
- <script type="module" src="https://unpkg.com/@nika-js/onlymap@0.5.3"></script>
19
+ <link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.5.7/dist/onlymapjs.css">
20
+ <script type="module" src="https://unpkg.com/@nika-js/onlymap@0.5.7"></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,13 +113,24 @@ 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
108
130
 
109
131
  Use the `type` value exactly:
110
132
 
111
- `A5Layer`, `ArcLayer`, `BitmapLayer`, `COGLayer`, `ColumnLayer`, `ContourLayer`, `GeoJsonLayer`, `GeohashLayer`, `GreatCircleLayer`, `GridCellLayer`, `GridLayer`, `H3ClusterLayer`, `H3HexagonLayer`, `HeatmapLayer`, `HexagonLayer`, `IconLayer`, `LineLayer`, `MVTLayer`, `PathLayer`, `PointCloudLayer`, `PolygonLayer`, `PopupLayer`, `QuadkeyLayer`, `S2Layer`, `ScatterplotLayer`, `ScenegraphLayer`, `ScreenGridLayer`, `SimpleMeshLayer`, `SolidPolygonLayer`, `TerrainLayer`, `TextLayer`, `Tile3DLayer`, `TileLayer`, `TripsLayer`.
133
+ `A5Layer`, `ArcLayer`, `BitmapLayer`, `COGLayer`, `ColumnLayer`, `ContourLayer`, `GeoJsonLayer`, `GeohashLayer`, `GreatCircleLayer`, `GridCellLayer`, `GridLayer`, `H3ClusterLayer`, `H3HexagonLayer`, `HeatmapLayer`, `HexagonLayer`, `IconLayer`, `ImageOverlay`, `LineLayer`, `MVTLayer`, `PathLayer`, `PointCloudLayer`, `PolygonLayer`, `PopupLayer`, `QuadkeyLayer`, `S2Layer`, `ScatterplotLayer`, `ScenegraphLayer`, `ScreenGridLayer`, `SimpleMeshLayer`, `SolidPolygonLayer`, `TerrainLayer`, `TextLayer`, `Tile3DLayer`, `TileLayer`, `TripsLayer`.
112
134
 
113
135
  Common choices:
114
136
 
@@ -119,6 +141,7 @@ Common choices:
119
141
  - Tiles: `TileLayer`, `MVTLayer`, `Tile3DLayer`.
120
142
  - 3D models: `ScenegraphLayer`, `SimpleMeshLayer`, `PointCloudLayer`, `Tile3DLayer`.
121
143
  - GeoTIFF/COG rasters: `COGLayer`.
144
+ - Geotagged drone JPEGs: `ImageOverlay`.
122
145
 
123
146
 
124
147
 
@@ -137,6 +160,23 @@ Common choices:
137
160
  - Plain 8-bit RGB COGs (satellite truecolor) need no styling attributes at all.
138
161
  - Restretch/recolor (min/max/colormap edits) are GPU uniform updates — tiles are not refetched. The legend widget renders the colormap ramp automatically when `colormap` + `min`/`max` are authored.
139
162
 
163
+ ### ImageOverlay (drone JPEG)
164
+
165
+ ```html
166
+ <om-layer id="photo" type="ImageOverlay"
167
+ src="./DJI_0123.jpg" georeference="exif"
168
+ opacity="0.8"></om-layer>
169
+ ```
170
+
171
+ - `src` (required) — a JPEG with GPS/relative-altitude/camera/focal-length EXIF and DJI gimbal XMP.
172
+ - `georeference="exif"` — fetches through `OmMap.configureData`, computes a flat-ground WGS84 footprint, and bakes yaw/roll into the pixels. `map.ready` waits for it.
173
+ - Verified camera paths include DJI FC300S and M30T, including M30T JPEGs carrying a 180° gimbal-roll correction.
174
+ - Unknown camera: supply `sensor-width-mm` + `sensor-height-mm` together. If EXIF lacks focal length, also supply `focal-length-mm`. Values are physical millimetres and must be positive.
175
+ - Persisted/preprocessed form: omit `georeference` and set `bounds="[west,south,east,north]"`; `src` may be the processed PNG. Optional JSON `metadata` passes through. Explicit bounds perform no EXIF fetch.
176
+ - `depth-test` defaults false. `opacity`, `visible`, and `pickable` behave like other layers.
177
+ - The public `await OmMap.resolveImageOverlay(fileOrUrl, options?)` returns `{image, bounds, metadata}` for upload/persistence.
178
+ - Visualization-grade only: no terrain, lens-distortion, calibration, or perspective-correct four-corner orthorectification. Use `COGLayer` for large orthomosaics.
179
+
140
180
  External layer classes become manifest types via `OmMap.registerLayer({type, deckClass, props})`. Build them on `@nika-js/onlymap/deck` (the bundled `CompositeLayer`/`TileLayer`/… re-exports — a separately-installed deck.gl is a different class hierarchy and breaks in the renderer); function-valued props ride the subclass's `static defaultProps`; register at module top level before the manifest mounts. Full recipe: docs/custom-layers.md.
141
181
 
142
182
  ### Data Sources
@@ -151,6 +191,9 @@ External layer classes become manifest types via `OmMap.registerLayer({type, dec
151
191
  | Arrow / GeoArrow IPC | `data="./big.arrow"` | Points stay columnar; lines/polygons become GeoJSON features. |
152
192
  | Shapefile | `data="./countries.shp"` | Loads sidecars and joins `.dbf` attributes. |
153
193
  | KML | `data="./tour.kml"` | Placemarks become GeoJSON features. |
194
+ | 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). |
195
+ | FlatGeobuf | `data="./cities.fgb"` | Cloud-native binary vector — whole-file decode to GeoJSON features (bbox-streaming is a later phase). |
196
+ | 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
197
  | CityJSON | `data="./tile.city.json"` | 3D city models → extruded footprints, or `?om-surfaces=1` for real per-face roof geometry; see below. |
155
198
  | CityJSONSeq | `data="./tile.city.jsonl"` | Same, streamed line by line as it downloads. |
156
199
  | WebSocket | `data="wss://feed" key="id" flush="250ms" source="decoder"` | Upsert-by-key stream. |
@@ -296,11 +339,30 @@ Examples:
296
339
  ```html
297
340
  <om-widget type="legend" position="bottom-right" title="Layers" interactive></om-widget>
298
341
  <om-widget type="filter" layer="quakes" field="magnitude" position="top-left"></om-widget>
342
+ <om-widget type="filter" layer="quakes" field="time" format="date"
343
+ date-style="datetime" time-zone="UTC" position="bottom-center"></om-widget>
299
344
  <om-widget type="draw" target="sketch" modes="point line polygon" save="both"></om-widget>
300
345
  <om-widget type="basemap-switcher" options="positron dark-matter liberty osm" position="top-right"></om-widget>
301
346
  ```
302
347
 
303
- Custom widget:
348
+ ### Custom widgets & event emission
349
+
350
+ **Reach for a built-in first.** A plain value/time slider is a built-in:
351
+ `<om-widget type="filter" layer="quakes" field="time" format="date">` — it
352
+ renders readable date labels AND wires the `filter-layer` action for you (pair
353
+ it with the layer's `filter-field`). Author a custom widget ONLY for bespoke UI
354
+ or logic the built-ins don't cover — most "it looked right but didn't work"
355
+ widgets should have been a `type="filter"`/`legend`/`vega-lite` built-in.
356
+
357
+ A custom widget is an `<om-widget>` with **no `type`** plus inline HTML and a
358
+ `<script type="om/widget">` block. The script is full JS, evaluated ONCE at
359
+ connect with `this` bound to the `<om-widget>` element: set `this.watch` (a
360
+ list of re-render triggers) and `this.render = (ctx) => {…}`. `vegaEmbed`/`d3`
361
+ are available. Content renders in shadow DOM — reach it with `this.$(selector)`
362
+ and `this.root`.
363
+
364
+ **Read map state** through `ctx` (the `render` argument), re-rendering when a
365
+ watched token fires:
304
366
 
305
367
  ```html
306
368
  <om-widget position="top-left">
@@ -315,20 +377,60 @@ Custom widget:
315
377
  </om-widget>
316
378
  ```
317
379
 
318
- Widget context:
319
-
320
- - `ctx.layers`
321
- - `ctx.data(id)`
322
- - `ctx.dataInViewport(id)`
323
- - `ctx.stats(id, field, { scope: "viewport" })`
324
- - `ctx.selection`
325
- - `ctx.viewport`
326
- - `ctx.history` `{ canUndo, canRedo }`; re-render on changes via the `history` watch token
327
- - `ctx.emit(action, payload)`
328
-
329
- 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).
330
-
331
- Use `this.$()` and `this.root`; widgets render in shadow DOM.
380
+ - `ctx.layers`, `ctx.data(id)`, `ctx.dataInViewport(id)`,
381
+ `ctx.stats(id, field, { scope: "viewport" })`, `ctx.selection`,
382
+ `ctx.viewport`, `ctx.history` (`{ canUndo, canRedo }`).
383
+ - Watch tokens: `data:<layerId>`, `viewport`, `selection`, `layers` (add/remove,
384
+ visibility, filter changes), `basemap`, `lighting`, `terrain`, `history`,
385
+ `widgets` (`widgets-hidden` hide-all toggle).
386
+
387
+ **Drive the map — the emission contract.** This is the part authors get wrong.
388
+ A widget NEVER mutates the map directly and NEVER dispatches its own
389
+ `CustomEvent` hoping the map listens. It **emits a registered action**, exactly
390
+ two ways:
391
+
392
+ 1. **Declarative `data-emit`** (no script): `data-emit="<action>"` + `data-*`
393
+ payload keys on an element. Fires on **click** (non-form elements) or
394
+ **change** (form controls — `input`/`select`/`textarea`, whose `.value` is
395
+ auto-merged into the payload). `data-*` keys camelCase (`data-feature-id` →
396
+ `featureId`) and arrive as **strings** — fine for `toggle-layer`/`fly-to`,
397
+ but a numeric/array payload (a slider's `range: [min, max]`) needs `ctx.emit`.
398
+ ```html
399
+ <button data-emit="toggle-layer" data-layer="quakes">Toggle quakes</button>
400
+ ```
401
+
402
+ 2. **Programmatic `ctx.emit(action, payload)`** — for typed payloads. Wire it
403
+ INSIDE `render` so `ctx` is in scope, assigning `.oninput`/`.onclick`
404
+ (idempotent across re-renders; prefer over `addEventListener`, which stacks a
405
+ fresh listener every render):
406
+ ```html
407
+ <om-widget position="bottom-center">
408
+ <input id="day" type="range" min="1" max="14" step="1" value="1">
409
+ <script type="om/widget">
410
+ this.render = (ctx) => {
411
+ this.$("#day").oninput = (e) => {
412
+ const d = Number(e.target.value);
413
+ ctx.emit("filter-layer", { layer: "quakes", field: "day", range: [d, d] });
414
+ };
415
+ };
416
+ </script>
417
+ </om-widget>
418
+ ```
419
+
420
+ **NEVER inline handlers** (`onclick="…"`, `oninput="ctx.emit(…)"`): `ctx` is not
421
+ a global, they execute in page scope, and CSP blocks them — the widget looks
422
+ right and silently emits nothing. This is the #1 custom-widget failure, and
423
+ validation errors on it.
424
+
425
+ **Actions a widget can emit** (payload keys): `filter-layer`
426
+ `{layer, field?, range:[min,max]}` (value/time sliders), `toggle-layer`
427
+ `{layer, visible?}`, `fly-to` `{center:[lng,lat], zoom?, pitch?, bearing?,
428
+ duration?}`, `zoom-to-feature` `{layer, featureId, duration?}`, `set-basemap`
429
+ `{basemap}`, `set-lighting`/`set-terrain`, `highlight-feature`
430
+ `{layer, featureId}`, `show-overlay`/`hide-overlay` `{target}`,
431
+ `story-play`/`story-pause`/`story-seek` `{story, t?}`, `undo`/`redo`,
432
+ `zoom-in`/`zoom-out`, `set-widgets-visible` `{visible}`. Register your own with
433
+ `OmMap.registerAction(name, (payload, mapEl) => …)`.
332
434
 
333
435
  ### `<om-overlay>`
334
436
 
@@ -494,4 +596,3 @@ For 3D Tiles LOD/refinement experiments, use:
494
596
  maximum-memory-usage="256"
495
597
  view-distance-scale="0.85"></om-layer>
496
598
  ```
497
-
@@ -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