@nika-js/onlymap 0.6.11 → 0.6.14
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.
- package/CHANGELOG.md +23 -0
- package/README.md +16 -2
- package/dist/{LercDecode.es-C331HMjV.js → LercDecode.es-Bqk5xyNu.js} +1 -1
- package/dist/attribute-resolution.d.ts +4 -0
- package/dist/{basemap-NN5SYEaS.js → basemap-KDsE62YN.js} +1 -1
- package/dist/classify.d.ts +62 -0
- package/dist/clip-box.d.ts +0 -32
- package/dist/data-layer.d.ts +0 -17
- package/dist/draw-controller.d.ts +4 -1
- package/dist/effects.d.ts +1 -0
- package/dist/elements/om-map.d.ts +1 -21
- package/dist/elements/om-story.d.ts +0 -18
- package/dist/expr/errors.d.ts +3 -0
- package/dist/field-access.d.ts +10 -0
- package/dist/geodesy.d.ts +11 -0
- package/dist/{geoparquet-Dmyu5Oou.js → geoparquet-DOexUItM.js} +1 -1
- package/dist/html-data.d.ts +1 -1
- package/dist/{index-D28RTVKt.js → index-BFNjPrc2.js} +1 -1
- package/dist/{index-CSVtCpQL.js → index-D_EyImRx.js} +1 -1
- package/dist/{index-_vNbcONa.js → index-DnsWPk6e.js} +1 -1
- package/dist/{index-BUp4hR6r.js → index-ESUJidEA.js} +21949 -21687
- package/dist/{index-CNxSRD27.js → index-mxTk4d2L.js} +2 -2
- package/dist/layer-registry.d.ts +10 -0
- package/dist/layers/marker-icons.d.ts +0 -2
- package/dist/legend-spec.d.ts +1 -6
- package/dist/{lerc-Cryu98-R.js → lerc-CPb8TKuv.js} +2 -2
- package/dist/onlymap.standalone.js +41385 -41123
- package/dist/onlymapjs.js +1 -1
- package/dist/programmatic.d.ts +5 -16
- package/dist/{raster-BVzTnbhW.js → raster-M2R7FhL7.js} +2 -2
- package/dist/{raster-pipeline-FAPzre9S.js → raster-pipeline-Csa7n5SZ.js} +1 -1
- package/dist/react.js +215 -232
- package/dist/runtime-core.d.ts +35 -1
- package/dist/site-placement.d.ts +0 -2
- package/dist/testing.d.ts +4 -1
- package/dist/version.d.ts +1 -1
- package/dist/{zarr-DCWSzKoG.js → zarr-tHnzENgX.js} +2 -2
- package/docs/testing.md +1 -1
- package/llms.txt +5 -3
- package/onlymapjs.html-data.json +32 -1
- package/package.json +3 -2
- package/skills/onlymapjs/SKILL.md +5 -4
- package/skills/onlymapjs/references/syntax.md +4 -4
package/dist/runtime-core.d.ts
CHANGED
|
@@ -8,6 +8,31 @@ import type { LayerIR } from "./ir";
|
|
|
8
8
|
import { type Selection } from "./selection";
|
|
9
9
|
import type { ValidationEntry } from "./validation";
|
|
10
10
|
import type { MapViewport } from "./basemap";
|
|
11
|
+
/**
|
|
12
|
+
* Composes the terrain + clip-box per-layer patches over a layer's own
|
|
13
|
+
* authored props (issue #36 — the 0.6.2 regression this replaces). The old
|
|
14
|
+
* inline merge fabricated an `extensions` key from the two patches ALONE
|
|
15
|
+
* whenever either patch OBJECT existed — and `applyTerrain` emits an identity
|
|
16
|
+
* patch (`extraProps: {}`) for every layer while terrain is INACTIVE, so on
|
|
17
|
+
* every flat map every layer got `extensions: []` spread over its authored
|
|
18
|
+
* array, silently unmounting PathStyleExtension (dash rendered solid) and
|
|
19
|
+
* DataFilterExtension (GPU filters stopped visually applying; widget stats
|
|
20
|
+
* stayed coherent via the CPU predicate, which is what hid it for six
|
|
21
|
+
* releases). Three rules restore the contract the patch builders' own tests
|
|
22
|
+
* already state:
|
|
23
|
+
*
|
|
24
|
+
* 1. An identity patch (no keys) is NOT a patch — authored props, extensions
|
|
25
|
+
* included, flow through untouched (return undefined; the constructor's
|
|
26
|
+
* `...layerProps` already carries them).
|
|
27
|
+
* 2. A patch that carries extensions already APPENDED them to the authored
|
|
28
|
+
* array (applyTerrain/applyClipBox both do) — never rebuild from scratch.
|
|
29
|
+
* 3. Both active: reference-dedup the concatenation — each patch's array
|
|
30
|
+
* starts with the SAME authored instances, and without the dedup a layer
|
|
31
|
+
* under terrain + clip box mounted every authored extension twice.
|
|
32
|
+
*
|
|
33
|
+
* Exported for direct unit testing (the lerpAngle precedent).
|
|
34
|
+
*/
|
|
35
|
+
export declare function mergePatchExtraProps(authoredExtensions: unknown[] | undefined, terrainProps: Record<string, unknown> | undefined, clipProps: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
11
36
|
/** RouteLayer's own `onRouteResolved` report (spec: "Routing & Tracking") — a normalized Route plus its bounds. `bounds` drives `follow="fit-route"` here; the WHOLE object forwards to `RuntimeCoreCallbacks.onRouteResolved` (the `om-route-resolved` consumer event), so a page can read a provider-resolved route's geometry/distance/duration without re-fetching it. */
|
|
12
37
|
interface RouteResolvedInfo {
|
|
13
38
|
geometry?: {
|
|
@@ -133,7 +158,7 @@ export interface RuntimeCoreCallbacks {
|
|
|
133
158
|
* needs (a vertex dropped in blank space). Coordinate is null only when
|
|
134
159
|
* deck reports none (e.g. off-globe).
|
|
135
160
|
*/
|
|
136
|
-
onMapPoint?: (coordinate: [number, number] | null, kind: "click" | "hover") => void;
|
|
161
|
+
onMapPoint?: (coordinate: [number, number] | null, kind: "click" | "hover", pointerType?: string) => void;
|
|
137
162
|
/**
|
|
138
163
|
* XY snapping (spec: issue #34 Part A) — fires ALONGSIDE onMapPoint on
|
|
139
164
|
* every click/hover, `null` whenever that point ISN'T a snap (no config,
|
|
@@ -369,6 +394,15 @@ export declare class RuntimeCore {
|
|
|
369
394
|
* (none ↔ maplibre). Reads/seeds the camera from `this.viewState`.
|
|
370
395
|
*/
|
|
371
396
|
private initRenderer;
|
|
397
|
+
/**
|
|
398
|
+
* The five pick/drag deck callbacks, shared verbatim by the lazy MapLibre
|
|
399
|
+
* adapter's deckProps and the standalone `new Deck` — one place to edit the
|
|
400
|
+
* pick path (they had drifted into two pasted copies). `onLoad`/`onError`
|
|
401
|
+
* stay per-branch: the basemap path wires load through `basemap.onLoad`.
|
|
402
|
+
* `toSelection`'s `type` param on drag is a required tag, not meaningful —
|
|
403
|
+
* the behavior dispatch (om-map.ts) uses its own "drag" event name.
|
|
404
|
+
*/
|
|
405
|
+
private pickCallbacks;
|
|
372
406
|
/** The current lighting as a deck `effects` array — [] restores deck's default lights. */
|
|
373
407
|
private buildEffects;
|
|
374
408
|
/**
|
package/dist/site-placement.d.ts
CHANGED
|
@@ -103,5 +103,3 @@ export declare function localOffsetToLngLat(local: readonly [number, number], or
|
|
|
103
103
|
*/
|
|
104
104
|
export declare function tilesetPlacementMatrix(baked: Matrix4, p: SitePlacement): Matrix4;
|
|
105
105
|
export declare function tilesetBakedFrame(url: string, onReady: () => void): Matrix4 | null | undefined;
|
|
106
|
-
/** Test seam — the cache is process-wide and would leak between cases. */
|
|
107
|
-
export declare function resetSitePlacementCache(): void;
|
package/dist/testing.d.ts
CHANGED
|
@@ -59,8 +59,11 @@ export interface TestHarness {
|
|
|
59
59
|
* Feeds a map coordinate through the same path a real deck click/hover
|
|
60
60
|
* takes — drives the draw controller and fires the `om-map-point` event.
|
|
61
61
|
* For testing custom capture tools (sketch/AOI) without a GPU.
|
|
62
|
+
* `pointerType` ("touch" | "pen" | "mouse") simulates that input modality —
|
|
63
|
+
* pass "touch" to exercise touch-only behavior like the draw controller's
|
|
64
|
+
* double-tap completion; omitted means a synthetic pick with no modality.
|
|
62
65
|
*/
|
|
63
|
-
mapPoint(coordinate: [number, number] | null, kind?: "click" | "hover"): Promise<void>;
|
|
66
|
+
mapPoint(coordinate: [number, number] | null, kind?: "click" | "hover", pointerType?: string): Promise<void>;
|
|
64
67
|
/**
|
|
65
68
|
* Sets the camera directly (center/zoom/pitch/bearing) — everything
|
|
66
69
|
* viewport-derived reacts for real: `viewport`-watching widgets,
|
package/dist/version.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as ur, d as lr, R as zt, e as fr, m as dr, p as St, f as hr, g as pr, h as mr } from "./raster-pipeline-
|
|
2
|
-
import { ap as gr } from "./index-
|
|
1
|
+
import { A as ur, d as lr, R as zt, e as fr, m as dr, p as St, f as hr, g as pr, h as mr } from "./raster-pipeline-Csa7n5SZ.js";
|
|
2
|
+
import { ap as gr } from "./index-ESUJidEA.js";
|
|
3
3
|
import $t from "./index-CW1n5LdO.js";
|
|
4
4
|
var Et;
|
|
5
5
|
function h(e, t, n) {
|
package/docs/testing.md
CHANGED
|
@@ -113,7 +113,7 @@ it("panning away empties viewport-scoped widgets", async () => {
|
|
|
113
113
|
});
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
The harness API: `pick` (synthetic picks fed through the exact code path real deck.gl picks take — columnar layers pick object-less by index, exactly like live), `clearSelection` (an empty pick — default kind `"hover"`, a hover-off that runs the tooltip auto-hide path; pass `"click"` for a click on empty space, the gesture that dismisses a `selection-type="click"` popup), `mapPoint` (a click/hover map coordinate through the real onMapPoint path — fires `om-map-point`, drives the draw controller), `emit` (any action, same payload contract as `ctx.emit`/`data-emit`), `setView`, `layers()` (the live IR), `flush`, `unmount`. Every verb settles the library's internal batching before resolving — **you never write a sleep**.
|
|
116
|
+
The harness API: `pick` (synthetic picks fed through the exact code path real deck.gl picks take — columnar layers pick object-less by index, exactly like live), `clearSelection` (an empty pick — default kind `"hover"`, a hover-off that runs the tooltip auto-hide path; pass `"click"` for a click on empty space, the gesture that dismisses a `selection-type="click"` popup), `mapPoint` (a click/hover map coordinate through the real onMapPoint path — fires `om-map-point`, drives the draw controller; an optional third `pointerType` argument — `"touch"`/`"pen"`/`"mouse"` — simulates that input modality, e.g. `mapPoint([1,2], "click", "touch")` twice at one spot exercises touch double-tap completion), `emit` (any action, same payload contract as `ctx.emit`/`data-emit`), `setView`, `layers()` (the live IR), `flush`, `unmount`. Every verb settles the library's internal batching before resolving — **you never write a sleep**.
|
|
117
117
|
|
|
118
118
|
**Remote data:** mock `fetch` and the harness waits for it via the readiness signal:
|
|
119
119
|
|
package/llms.txt
CHANGED
|
@@ -27,9 +27,9 @@ Programmatic/native bridge rule: `MapController.setLayers()` accepts normal func
|
|
|
27
27
|
- Full JavaScript in accessor blocks needs the `js` attribute on the layer (`<om-layer js>` + `<script type="om/accessors">`). Without it, blocks are restricted to `export const name = d => <expression>` — no statements, no loops, no nested functions.
|
|
28
28
|
- Dashed lines are a single attribute: `dash="[6, 3]"` (or SVG-style `dash="6 3"`, plus optional `dash-justified`) on a path-stroking layer (`PathLayer`, `GeoJsonLayer`, `PolygonLayer`, `TripsLayer`). Do NOT hand-wire deck's `PathStyleExtension`/`getDashArray` — the attribute mounts it for you. Values are `[dashLength, gapLength]` in the SAME units as the line width; `dash` on a non-path layer (ScatterplotLayer, etc.) is ignored with a warning.
|
|
29
29
|
- To capture where the user CLICKS on the map (a measure tool, drop-a-pin, a custom rectangle/circle AOI, snap-to-feature), listen for the `om-map-point` event on `<om-map>`: `mapEl.addEventListener('om-map-point', e => { const { coordinate, kind } = e.detail; })` — `coordinate` is `[lng,lat]` (or `null` off-globe), `kind` is `"click"`|`"hover"`, and it fires on every click/hover including empty-map clicks. Do NOT reach for deck.gl internals (`mapEl.getMap()`, `.deckInstance`, `.deck.viewManager`) or unproject canvas pixels — those are not exposed on `<om-map>` and return nothing. The built-in `draw` widget handles polygon/line/point sketching; `om-map-point` is for tools it doesn't cover.
|
|
30
|
-
- `<om-widget type="draw" modes="point line polygon" target="sketch" save="both" autosave="<key>">` is the sketch-capture toolbar — `target` binds the store a `data="draw:<target>"` layer reads. `export-3d` (bare = GLB, `="b3dm"` for Cesium/3D-Tiles pipelines) adds an "Export 3D" button (issue #34), separate from `save` (that's the drawn shape's own GeoJSON): outline a polygon over loaded `Tile3DLayer`/`BIMLayer` content, close it, and it clips every loaded tile's triangles to that footprint (a plain 2D clip, no elevation-picking involved), re-frames them to a local coordinate frame at the footprint's centroid, and downloads it, each triangle carrying its own source color (vertex colors) — no textures (BIM/IFC materials are flat colors, not textured meshes). Only currently-VISIBLE 3D Tiles/BIM layers are included — `visible="false"` (or `toggle-layer`) excludes a layer, with distinct console warnings for "nothing loaded" vs. "everything hidden." Validation warns on an unrecognized `export-3d` value.
|
|
30
|
+
- `<om-widget type="draw" modes="point line polygon" target="sketch" save="both" autosave="<key>">` is the sketch-capture toolbar — `target` binds the store a `data="draw:<target>"` layer reads. Lines/polygons complete via double-click, touch double-tap (iOS WebViews never synthesize `dblclick` from taps — the library detects the tap pair itself, so touch parity is native), Enter, or the toolbar's explicit **Finish** button; Escape cancels the in-progress shape. `export-3d` (bare = GLB, `="b3dm"` for Cesium/3D-Tiles pipelines) adds an "Export 3D" button (issue #34), separate from `save` (that's the drawn shape's own GeoJSON): outline a polygon over loaded `Tile3DLayer`/`BIMLayer` content, close it, and it clips every loaded tile's triangles to that footprint (a plain 2D clip, no elevation-picking involved), re-frames them to a local coordinate frame at the footprint's centroid, and downloads it, each triangle carrying its own source color (vertex colors) — no textures (BIM/IFC materials are flat colors, not textured meshes). Only currently-VISIBLE 3D Tiles/BIM layers are included — `visible="false"` (or `toggle-layer`) excludes a layer, with distinct console warnings for "nothing loaded" vs. "everything hidden." Validation warns on an unrecognized `export-3d` value.
|
|
31
31
|
- Clip box (issue #34): `<om-map clip-box-min="[lng,lat,elev]" clip-box-max="[lng,lat,elev]">` cuts a real axis-aligned 3D box through the whole scene — geometry outside it discarded, every layer clipped by default (`clip="off"` on an `<om-layer>` opts out), works on ANY layer type including georeferenced `Tile3DLayer`/`BIMLayer` content (not just flat `GeoJsonLayer` extrusions). `clip-box-invert` shows outside instead of inside; `clip-box-highlight` dims clipped-out geometry instead of discarding it (non-destructive preview). Attribute-backed (undoable, story-steppable) via `set-clip-box {min, max, invert?, highlight?}` (`{clear:true}` removes it) and `<om-widget type="clip-box">` (six number inputs + invert/highlight checkboxes + clear button). v1 is axis-aligned only — rotation is a documented follow-up.
|
|
32
|
-
- `<om-widget type="measure" modes="distance area volume" units="metric|imperial|nautical">` is the geodesic ruler: click to place points, live labels + a totals panel, read the value programmatically via the `om-measure` event (`detail.mode`/`.totalMeters`/`.areaMeters2`/`.perimeterMeters`/`.cutMeters3`/`.fillMeters3`/`.netMeters3`/`.totalMeters3`/`.cutAdjustedMeters3`/`.fillAdjustedMeters3`/`.cutMassKg`/`.fillMassKg`/`.stale`/`.profileSeries` — profile points are `{x: metres from the first vertex, y: elevation}` plus `vertexIndex` on the samples that ARE drawn corners, so a chart can mark them; vertex 0 is the leftmost, and with `profile` on the map badges the first two vertices `1 · Start` and `2` in draw order so the ring's winding direction is readable at constant cost, while the chart marks every corner). `volume` outlines a footprint like `area` (close it with a double-click/Enter — it turns solid teal, "ready"), then a fixed-screen-pixel-size double-headed arrow gizmo appears at the centroid: drag up to fill, down to cut (unbounded distance), reading Cut/Fill/Net (signed, fill−cut)/Total (unsigned, cut+fill) — always RAW geometric volumes, never altered by `swell`/`shrink`. It REQUIRES `terrain` on `<om-map>` (validation warns a `volume` mode with none — cut/fill against flat ground with no elevation surface has nothing to measure against). The math is a REAL per-cell grid integration: closing a footprint bulk-loads its covering DEM tiles and integrates terrain-vs-base per cell on a metric tangent-plane grid (cell size = the DEM's GSD, scanline point-in-polygon, bilinear seam-correct sampling, worker-offloaded) — mixed cut AND fill in one footprint on undulating ground, with `cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3` (± = per-cell cellArea × 1.5 × GSD, per side)/`nodataFraction` published on the readout; without terrain a flat-plane fallback runs with no error figures. `base-surface` picks the reference: `custom` (default — the gizmo's target plane) or boundary-derived stockpile strategies with no gizmo (`triangulated` boundary TIN, `plane`, `lowest`, `highest`, `average`). Five more volume-only attributes (no-ops, and validation warns, without `volume` in `modes`): `base-surface` (above); `profile` (elevation samples around the footprint's own perimeter, live while sketching, dispatched on `profileSeries` for a paired `dynamic-chart`); `deadband` (m³, zeroes a Cut/Fill figure below the threshold); `density` (t/m³ metric, lb/yd³ imperial) and `swell`/`shrink` (multipliers, default 1×) populate a separate Material section instead — Bank/Loose/Compacted convention, `cutAdjustedMeters3` = raw × swell (loose/haul, bigger), `fillAdjustedMeters3` = raw ÷ shrink (loose/borrow needed, also bigger), tonnage from the raw (mass-conserving) volume — shown only once one of the three is actually configured.
|
|
32
|
+
- `<om-widget type="measure" modes="distance area volume" units="metric|imperial|nautical">` is the geodesic ruler: click to place points, live labels + a totals panel, read the value programmatically via the `om-measure` event (`detail.mode`/`.totalMeters`/`.areaMeters2`/`.perimeterMeters`/`.cutMeters3`/`.fillMeters3`/`.netMeters3`/`.totalMeters3`/`.cutAdjustedMeters3`/`.fillAdjustedMeters3`/`.cutMassKg`/`.fillMassKg`/`.stale`/`.profileSeries` — profile points are `{x: metres from the first vertex, y: elevation}` plus `vertexIndex` on the samples that ARE drawn corners, so a chart can mark them; vertex 0 is the leftmost, and with `profile` on the map badges the first two vertices `1 · Start` and `2` in draw order so the ring's winding direction is readable at constant cost, while the chart marks every corner). `volume` outlines a footprint like `area` (close it with a double-click/double-tap/Enter — it turns solid teal, "ready"), then a fixed-screen-pixel-size double-headed arrow gizmo appears at the centroid: drag up to fill, down to cut (unbounded distance), reading Cut/Fill/Net (signed, fill−cut)/Total (unsigned, cut+fill) — always RAW geometric volumes, never altered by `swell`/`shrink`. It REQUIRES `terrain` on `<om-map>` (validation warns a `volume` mode with none — cut/fill against flat ground with no elevation surface has nothing to measure against). The math is a REAL per-cell grid integration: closing a footprint bulk-loads its covering DEM tiles and integrates terrain-vs-base per cell on a metric tangent-plane grid (cell size = the DEM's GSD, scanline point-in-polygon, bilinear seam-correct sampling, worker-offloaded) — mixed cut AND fill in one footprint on undulating ground, with `cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3` (± = per-cell cellArea × 1.5 × GSD, per side)/`nodataFraction` published on the readout; without terrain a flat-plane fallback runs with no error figures. `base-surface` picks the reference: `custom` (default — the gizmo's target plane) or boundary-derived stockpile strategies with no gizmo (`triangulated` boundary TIN, `plane`, `lowest`, `highest`, `average`). Five more volume-only attributes (no-ops, and validation warns, without `volume` in `modes`): `base-surface` (above); `profile` (elevation samples around the footprint's own perimeter, live while sketching, dispatched on `profileSeries` for a paired `dynamic-chart`); `deadband` (m³, zeroes a Cut/Fill figure below the threshold); `density` (t/m³ metric, lb/yd³ imperial) and `swell`/`shrink` (multipliers, default 1×) populate a separate Material section instead — Bank/Loose/Compacted convention, `cutAdjustedMeters3` = raw × swell (loose/haul, bigger), `fillAdjustedMeters3` = raw ÷ shrink (loose/borrow needed, also bigger), tonnage from the raw (mass-conserving) volume — shown only once one of the three is actually configured.
|
|
33
33
|
|
|
34
34
|
## Element vocabulary
|
|
35
35
|
|
|
@@ -45,7 +45,7 @@ Programmatic/native bridge rule: `MapController.setLayers()` accepts normal func
|
|
|
45
45
|
- `<om-fallback>` — static no-JS fallback, direct child of `<om-map>` (one per map, no attributes, plain HTML content — links allowed). Shown ONLY where scripts never run (chat-app/email file previews — iOS QuickLook renders HTML attachments with JS off — file managers, sandboxed webviews); hidden automatically once the map boots. GOOD PRACTICE: include one on every complete page, especially pages that may be shared as a file ("This interactive map requires JavaScript — open this file in a web browser", plus a hosted-version link when one exists). Without one, the stylesheet shows a generic text-only banner. The gate is pure CSS (`om-map:not(:defined)` in onlymapjs.css), so the CSS must load without JS — a real `<link rel="stylesheet">` or inlined `<style>` on no-build pages; a bundler-emitted stylesheet is fine in npm projects.
|
|
46
46
|
- 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`.
|
|
47
47
|
- `<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. Scene actions are story-steppable AND scrub-capturable: `set-basemap`, `set-lighting` (a sunset story: steps walking sun-elevation down; a bare preset step is a clean reset), and `set-terrain` all rewind on seek — the story captures the map's scene attributes before first play. 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).
|
|
48
|
-
- 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="...">`. For an epoch-millisecond field, make the slider labels readable with `<om-widget type="filter" layer="quakes" field="time" format="date" date-style="datetime" time-zone="UTC"></om-widget>`. Up to 4 numeric dimensions at once via `filter-fields='[{"field":"magnitude","range":[4,10]},{"field":"time","range":[…]}]'` (JSON array, additive to filter-field/filter-range — wins if both are authored) — one `<om-widget type="filter">` per field, each moves its own dimension independently (filter-layer merges the range onto the matching field rather than replacing the whole filter); a row must pass every active dimension (AND). A dimension can't be added live — the full set is declared up front in filter-fields. Categorical filtering is a SEPARATE mechanism (deck.gl's own discrete keep-list test, not a range) with its own attributes: `filter-category="fuel" filter-categories='["Coal","Gas"]'` (single) or `filter-category-fields='[{"field":"fuel","categories":[...]},...]'` (up to 4); the SAME `<om-widget type="filter">` auto-renders checkboxes instead of a slider when its `field` is declared categorically (mode is inferred from the layer's own filter, never a separate widget attribute) — one checkbox per distinct value present in the data, with its row count. A category dimension with no keep-list is dropped from the active filter (there is no "matches everything" category the way a numeric range has [-Infinity, Infinity]). Numeric and categorical filters on the same layer combine — a row must pass both. `ctx.stats`/`ctx.dataInViewport` respect whichever kind(s) are active by default (`{filtered:false}` opts out).
|
|
48
|
+
- 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="...">`. For an epoch-millisecond field, make the slider labels readable with `<om-widget type="filter" layer="quakes" field="time" format="date" date-style="datetime" time-zone="UTC"></om-widget>`. Up to 4 numeric dimensions at once via `filter-fields='[{"field":"magnitude","range":[4,10]},{"field":"time","range":[…]}]'` (JSON array, additive to filter-field/filter-range — wins if both are authored) — one `<om-widget type="filter">` per field, each moves its own dimension independently (filter-layer merges the range onto the matching field rather than replacing the whole filter); a row must pass every active dimension (AND). A dimension can't be added live — the full set is declared up front in filter-fields. Categorical filtering is a SEPARATE mechanism (deck.gl's own discrete keep-list test, not a range) with its own attributes: `filter-category="fuel" filter-categories='["Coal","Gas"]'` (single) or `filter-category-fields='[{"field":"fuel","categories":[...]},...]'` (up to 4); the SAME `<om-widget type="filter">` auto-renders checkboxes instead of a slider when its `field` is declared categorically (mode is inferred from the layer's own filter, never a separate widget attribute) — one checkbox per distinct value present in the data, with its row count. A category dimension with no keep-list is dropped from the active filter (there is no "matches everything" category the way a numeric range has [-Infinity, Infinity]). Numeric and categorical filters on the same layer combine — a row must pass both. `ctx.stats`/`ctx.dataInViewport` respect whichever kind(s) are active by default (`{filtered:false}` opts out). CLASSIFIED SYMBOLOGY: `classify-by="<numeric field>"` (+ `classify-scale="quantile|equal-interval|jenks"`, `classify-classes="2-12"` default 5, `classify-ramp="viridis|plasma|inferno|magma|cividis|turbo|blues|greens|oranges|purples|reds|ylorrd|rdbu|spectral"`) computes class breaks FROM THE DATA at reconcile time, installs the fill-color accessor and the auto classes legend — use it when the user asks for graduated/choropleth styling WITHOUT hand-authoring domains; an authored get-fill-color/color always wins (validation warns on the conflict); URL-backed layers classify when their data arrives. TEMPORAL PLAYBACK: `<om-widget type="time-slider" layer="…" field="<numeric/epoch-ms field>" duration="20s" window="<span in field units>" loop format="date" date-style time-zone>` — play/pause/scrub emitting the ordinary filter-layer action (cumulative from the domain start, or a sliding window with `window`); manifest stays the source of truth so undo/story/external filter edits re-sync the thumb — prefer it over hand-rolling a playback loop.
|
|
49
49
|
- Routing & tracking are two library-owned layer types (not `PathLayer`/`IconLayer` hand-wired) that expand into ordinary `PathLayer`/`IconLayer` instances internally, same pattern as `BIMLayer`→`Tile3DLayer`. `<om-layer type="Route" geometry='{"type":"LineString","coordinates":[[lng,lat],...]}'>` draws a styled route (casing + line + origin/destination pins) from geometry you already have — resolves SYNCHRONOUSLY, no network. `<om-layer type="Route" origin="[lng,lat]" destination="[lng,lat]" provider="nika" profile="driving">` (+ optional `waypoints`) resolves one ASYNCHRONOUSLY via a `RoutingProvider` named by `provider` — `"nika"` is registered by default but its endpoint is an UNVERIFIED PLACEHOLDER until NIKA's real routing service ships (register a working one with `OmMap.registerRoutingProvider(name, provider)` — a ~15-line adapter over OSRM's keyless public demo server (`router.project-osrm.org/route/v1/{profile}/{lng},{lat};{lng},{lat}?geometries=geojson&overview=full`, map `distance`/`duration`/`legs` onto `distanceMeters`/`durationSec`/`legs[]`) is the verified keyless real-data recipe; the skill's syntax.md carries it in full). `geometry` wins outright if both are authored (validation warns). `color`/`casing-color` style the line; `follow="fit-route"` auto-fits the camera once resolved. `<om-layer type="Tracking" get-position="[$lng,$lat]">` renders ONE moving entity (v1 — a fleet is one `Tracking` layer per vehicle) with bearing-derived icon rotation; position data arrives through the ORDINARY `data`/`source` mechanism, no separate tracking-subscription API. `bearing-field` (default `"bearing"`) names the plain field to rotate by (checks `properties.<field>` on GeoJSON rows, `<field>` directly on flat rows); `interpolate-ms` (default `1000`) glides the marker between two fixes via the per-frame channel instead of jumping; `follow="follow"` eases the camera along with it, same timing. `color`/`size` style the marker; `icon="arrow|car|motorcycle"` picks the shape (default arrow — all nose-up, baked in `color`, unknown names fall back with a validation warning). Tail modes: on the ROUTE layer, `progress-from="<tracking-layer-id>"` + `tail="none"` (client view — only current position → destination renders, origin pin dropped) or `tail="dim"` (operator view — traveled portion darkened; `tail-color` overrides) split the route at the marker's interpolated position per frame; default full ignores the split; validation warns on partial wiring.
|
|
50
50
|
|
|
51
51
|
## Decision rule for annotations
|
|
@@ -56,6 +56,8 @@ UI panel (legend, chart, stats) → `<om-widget>`. Rich HTML at one map location
|
|
|
56
56
|
|
|
57
57
|
In a React codebase, do NOT render om-* elements from JSX (React and the library would contend over the same DOM). Use the first-party adapter instead: `import { OmMap, OmLayer, OmWidget, OmOverlay, useOmMap } from "@nika-js/onlymap/react"` — camelCase deck.gl props, accessors as plain JS functions (`getFillColor={d => ...}`, no expression language), interactions as `onClick`/`onHover` handlers, widget state via the `useOmMap(watchTokens)` hook (tearing-safe: it rides `useSyncExternalStore` over the controller's per-token stores). To sync map state into Redux/MobX/Zustand/Jotai, use `controller.getStore(token)` — a framework-free `{subscribe, getSnapshot}` per watch token with cached plain-data snapshots and `origin: "user"|"programmatic"` tagging for echo-free two-way camera binding; ~20-line recipes: [docs/external-stores.md](docs/external-stores.md). Guide: [docs/react.md](docs/react.md).
|
|
58
58
|
|
|
59
|
+
If the target is an Expo/React Native MOBILE app, use the separate `@nika-js/onlymap-native` package instead of this one: same layer vocabulary, but accessors are OnlyMap expression STRINGS (`getPosition="[$lon, $lat]"` — functions cannot cross its JSON bridge), descriptors are plain JSON, and native UI goes beside the map (no om-* elements, no `<OmWidget>`/`<OmOverlay>` components). That package ships its own `llms.txt` and skill; follow those for native work.
|
|
60
|
+
|
|
59
61
|
## Docs
|
|
60
62
|
|
|
61
63
|
- [README](README.md): thesis, authoring overview, build/run commands
|
package/onlymapjs.html-data.json
CHANGED
|
@@ -457,6 +457,22 @@
|
|
|
457
457
|
"name": "filter-category-fields",
|
|
458
458
|
"description": "Multi-dimension categorical GPU filter (up to 4) — JSON array of {field, categories} objects, e.g. '[{\"field\":\"fuel\",\"categories\":[\"Coal\",\"Gas\"]}]'. Wins over filter-category/filter-categories if both are set."
|
|
459
459
|
},
|
|
460
|
+
{
|
|
461
|
+
"name": "classify-by",
|
|
462
|
+
"description": "Classified symbology: the numeric field to compute class breaks from (quantile/equal-interval/jenks). Installs a fill-color accessor + auto legend; an authored get-fill-color/color wins."
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"name": "classify-scale",
|
|
466
|
+
"description": "Classification mode: quantile (default), equal-interval, or jenks."
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
"name": "classify-classes",
|
|
470
|
+
"description": "Number of classes, 2–12 (default 5)."
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
"name": "classify-ramp",
|
|
474
|
+
"description": "Named color ramp for classification: viridis (default), plasma, inferno, magma, cividis, turbo, blues, greens, oranges, purples, reds, ylorrd, rdbu, spectral."
|
|
475
|
+
},
|
|
460
476
|
{
|
|
461
477
|
"name": "dash",
|
|
462
478
|
"description": "Dashed line pattern \"[dashLength, gapLength]\" (or \"6 3\") in line-width units. PathLayer/GeoJsonLayer/PolygonLayer/TripsLayer."
|
|
@@ -1969,6 +1985,9 @@
|
|
|
1969
1985
|
{
|
|
1970
1986
|
"name": "widgets-toggle"
|
|
1971
1987
|
},
|
|
1988
|
+
{
|
|
1989
|
+
"name": "time-slider"
|
|
1990
|
+
},
|
|
1972
1991
|
{
|
|
1973
1992
|
"name": "ifc-browser"
|
|
1974
1993
|
},
|
|
@@ -2066,7 +2085,19 @@
|
|
|
2066
2085
|
},
|
|
2067
2086
|
{
|
|
2068
2087
|
"name": "field",
|
|
2069
|
-
"description": "Data field (filter / vega-lite widgets)."
|
|
2088
|
+
"description": "Data field (filter / vega-lite / time-slider widgets)."
|
|
2089
|
+
},
|
|
2090
|
+
{
|
|
2091
|
+
"name": "window",
|
|
2092
|
+
"description": "time-slider widget: sliding-window span in the field's own units (ms for epoch fields). Absent = cumulative reveal from the domain start."
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
"name": "duration",
|
|
2096
|
+
"description": "time-slider widget: wall time of one full sweep, e.g. \"20s\" (default). Player-widget-free playback over a layer's time field."
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
"name": "loop",
|
|
2100
|
+
"description": "time-slider widget: restart the sweep from the domain start when it completes."
|
|
2070
2101
|
},
|
|
2071
2102
|
{
|
|
2072
2103
|
"name": "on",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nika-js/onlymap",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.14",
|
|
4
4
|
"description": "Declarative deck.gl maps for HTML and React — interactive WebGL mapping with GeoJSON/CSV/Arrow data, MapLibre basemaps, widgets, popups, and live streams from a custom-element manifest or typed React components. TypeScript, no build step.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"publishConfig": {
|
|
@@ -111,7 +111,8 @@
|
|
|
111
111
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
112
112
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.e2e.json --noEmit && tsc -p cloud/workers/telemetry/tsconfig.json && tsc -p cloud/workers/examples/tsconfig.json",
|
|
113
113
|
"test": "vitest run",
|
|
114
|
-
"prepublishOnly": "npm run build",
|
|
114
|
+
"prepublishOnly": "node dev/tools/announce-release.mjs --check && npm run build",
|
|
115
|
+
"postpublish": "node dev/tools/announce-release.mjs",
|
|
115
116
|
"try": "vite-node dev/expr-repl.mjs",
|
|
116
117
|
"test:e2e": "playwright test",
|
|
117
118
|
"check-layout": "playwright test e2e/layout-audit.spec.ts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: onlymapjs
|
|
3
|
-
description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), a responsive/mobile map whose controls auto-fold on narrow screens, auditing a map's widget layout with the check-layout tool, syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), BIM/IFC models (loading .ifc files in the browser, 3D Tiles per-element picking, isolate/hide/ghost, clash detection, model federation), routes and directions (a styled A-to-B route line, OSRM or another routing engine, click-to-route), live vehicle/rider/delivery tracking (a moving marker gliding between GPS fixes with a follow camera), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
|
|
3
|
+
description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), a responsive/mobile map whose controls auto-fold on narrow screens, auditing a map's widget layout with the check-layout tool, syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), BIM/IFC models (loading .ifc files in the browser, 3D Tiles per-element picking, isolate/hide/ghost, clash detection, model federation), routes and directions (a styled A-to-B route line, OSRM or another routing engine, click-to-route), live vehicle/rider/delivery tracking (a moving marker gliding between GPS fixes with a follow camera), a React Native or Expo mobile map (route to the separate @nika-js/onlymap-native package), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# OnlyMapJS
|
|
@@ -23,7 +23,7 @@ Use OnlyMapJS as a declarative HTML map library. Write custom elements such as `
|
|
|
23
23
|
</script>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
For no-build CDN pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.6.
|
|
26
|
+
For no-build CDN pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.6.14` (the bare package URL serves `dist/onlymap.standalone.js`) — plus `<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.6.14/dist/onlymapjs.css">`. Never a rebundling CDN (esm.sh, skypack): re-bundling duplicates the deck.gl/luma.gl runtime and every layer fails shader compilation.
|
|
27
27
|
|
|
28
28
|
## React Projects
|
|
29
29
|
|
|
@@ -57,15 +57,16 @@ Load the smallest reference needed for the task:
|
|
|
57
57
|
- Accessor values are expressions: `get-position="[$lon, $lat]"`.
|
|
58
58
|
- `scale()` always needs an explicit `domain=`.
|
|
59
59
|
- Format epoch-millisecond or ISO fields with the safe `formatDate()` built-in, e.g. `get-text="formatDate($time, 'datetime', 'UTC')"`. Do not use `new Date()`, `Intl`, or method calls in restricted expressions.
|
|
60
|
-
- For a built-in filter over epoch milliseconds, add `format="date"` with optional `date-style="date|datetime|time|iso"` and `time-zone="UTC|local|<IANA zone>"`; do not hand-roll a time slider only to format its labels.
|
|
60
|
+
- For a built-in filter over epoch milliseconds, add `format="date"` with optional `date-style="date|datetime|time|iso"` and `time-zone="UTC|local|<IANA zone>"`; do not hand-roll a time slider only to format its labels. For temporal PLAYBACK (play/pause/scrub over a time field, cumulative or a sliding `window`), use `<om-widget type="time-slider" layer="…" field="…" duration="20s" loop>` — never hand-roll the playback loop.
|
|
61
61
|
- ScatterplotLayer points need an explicit size — `radius="6" radius-units="pixels"`, `get-radius="..."`, or `radius-min-pixels="..."`: deck's default is 1 METER, sub-pixel at city zooms, and validation warns on layers with no radius source.
|
|
62
|
-
- Prefer canonical color expressions — a `sequential`/`diverging`/`threshold` `scale()` or an equality ternary chain — over hand-rolled arithmetic: the legend widget parses these shapes and renders a matching gradient ramp / class ranges / category palette automatically.
|
|
62
|
+
- Prefer canonical color expressions — a `sequential`/`diverging`/`threshold` `scale()` or an equality ternary chain — over hand-rolled arithmetic: the legend widget parses these shapes and renders a matching gradient ramp / class ranges / category palette automatically. When the user wants graduated/choropleth classes WITHOUT hand-picking domains, use `classify-by="<field>" classify-scale="quantile|equal-interval|jenks" classify-classes="5" classify-ramp="viridis"` instead — breaks are computed from the data and the classes legend is automatic (an authored get-fill-color/color wins).
|
|
63
63
|
- Inline handlers such as `onclick` are wrong. Use `data-emit`, `<om-behavior>`, or widget scripts.
|
|
64
64
|
- Full JavaScript accessor blocks require the `js` attribute on `<om-layer>`.
|
|
65
65
|
- Do not put secrets in markup. Use `OmMap.configureData({ headers, credentials, fetch })`.
|
|
66
66
|
|
|
67
67
|
## Authoring Decisions
|
|
68
68
|
|
|
69
|
+
- Target is an Expo/React Native MOBILE app -> the separate `@nika-js/onlymap-native` package, not this one: same layer vocabulary, but accessors are OnlyMap expression STRINGS (`getPosition="[$lon, $lat]"` — functions cannot cross its JSON bridge), descriptors are plain JSON, no om-* elements and no widget/overlay components (build native UI beside the map). It ships its own llms.txt and skill; follow those for native work.
|
|
69
70
|
- UI panel, control, chart, legend, stats, filter, or draw toolbar -> `<om-widget>`.
|
|
70
71
|
- Sparse rich HTML at one geographic location -> `<om-overlay>`.
|
|
71
72
|
- Many labels/badges -> `<om-layer type="PopupLayer">`.
|
|
@@ -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.6.
|
|
20
|
-
<script type="module" src="https://unpkg.com/@nika-js/onlymap@0.6.
|
|
19
|
+
<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.6.14/dist/onlymapjs.css">
|
|
20
|
+
<script type="module" src="https://unpkg.com/@nika-js/onlymap@0.6.14"></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).
|
|
@@ -436,7 +436,7 @@ Built-ins:
|
|
|
436
436
|
- `filter`
|
|
437
437
|
- `draw` — sketch-capture toolbar: `modes="point line polygon"` (default all three), `target="<name>"` (default `sketch`, bound via `data="draw:<target>"`), `save="both|download|file-system"`, `autosave="<localStorage key>"`. `export-3d` (bare = GLB default, `="b3dm"` wraps it for Cesium/3D-Tiles pipelines) adds an "Export 3D" button (spec: issue #34 — region export) — deliberately separate from `save` (that's the drawn shape's own GeoJSON; `export-3d` exports the 3D `Tile3DLayer`/`BIMLayer` content found INSIDE the drawn footprint). Outline a polygon over loaded 3D content, close it, click "Export 3D": clips every loaded tile's triangles to the footprint (a plain 2D clip — no elevation-picking involved), re-frames them to a local coordinate frame at the footprint's own centroid (portable — opens correctly in Blender/three.js/etc. without ECEF-scale support), and downloads it, each triangle carrying its own source color (baked as vertex colors). No textures — BIM/IFC materials are flat colors, not textured meshes. The export only pulls in currently-VISIBLE 3D Tiles/BIM layers — one hidden via `visible="false"` (or the `toggle-layer` action) is excluded, with a distinct console warning distinguishing "nothing has loaded yet" from "everything loaded is hidden." Validation warns on an unrecognized `export-3d` value.
|
|
438
438
|
- `clip-box` — native UI over the map's `clip-box-*` scene-state attributes (see the `<om-map>` section above): six number inputs (min/max × lng/lat/elevation), invert/highlight checkboxes, and a clear button, all wired through `set-clip-box`. Manifest is the source of truth — the panel re-syncs from the attributes on every render, so undo/redo and story-scrub move the inputs too.
|
|
439
|
-
- `measure` — geodesic ruler: `modes="distance area volume"` (space-separated; default `distance area`), `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, cutMeters3, fillMeters3, netMeters3, totalMeters3, cutAdjustedMeters3, fillAdjustedMeters3, swell, shrink, cutMassKg, fillMassKg, cellSizeM, gsdM, cutErrorM3, fillErrorM3, nodataFraction, baseSurface, stale, profileSeries}` — `profileSeries` points are `{x: metres from the first vertex, y: elevation m}`, with `vertexIndex` present only on samples that ARE a drawn corner (filter a chart on `isValid(datum.vertexIndex)`; vertex 0 is the leftmost point, and with `profile` on the map badges the first two vertices `1 · Start` and `2` in draw order — 1-based display, field stays 0-based — so clockwise vs counter-clockwise is stated, not inferred; two is the minimum that fixes a direction and stays constant however many corners there are, while the chart marks them all) — everything from `cutMeters3` on is volume-mode-only, populating once a footprint closes). `volume` mode outlines a polygon footprint the same way `area` does — double-click
|
|
439
|
+
- `measure` — geodesic ruler: `modes="distance area volume"` (space-separated; default `distance area`), `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, cutMeters3, fillMeters3, netMeters3, totalMeters3, cutAdjustedMeters3, fillAdjustedMeters3, swell, shrink, cutMassKg, fillMassKg, cellSizeM, gsdM, cutErrorM3, fillErrorM3, nodataFraction, baseSurface, stale, profileSeries}` — `profileSeries` points are `{x: metres from the first vertex, y: elevation m}`, with `vertexIndex` present only on samples that ARE a drawn corner (filter a chart on `isValid(datum.vertexIndex)`; vertex 0 is the leftmost point, and with `profile` on the map badges the first two vertices `1 · Start` and `2` in draw order — 1-based display, field stays 0-based — so clockwise vs counter-clockwise is stated, not inferred; two is the minimum that fixes a direction and stays constant however many corners there are, while the chart marks them all) — everything from `cutMeters3` on is volume-mode-only, populating once a footprint closes). `volume` mode outlines a polygon footprint the same way `area` does — double-click, touch double-tap, or Enter closes it, and it turns solid teal to signal it's ready — then a double-headed arrow gizmo (fixed screen-pixel size, unbounded drag distance) appears at the centroid: drag it up to fill, down to cut, panel reads Cut/Fill/Net (signed, fill−cut)/Total (unsigned, cut+fill) volume + Area/Perimeter live — always RAW geometric figures, never altered by `swell`/`shrink`. REQUIRES `terrain` on `<om-map>` (validation warns a `volume` mode with none): the math is a REAL per-cell grid integration (issue #35) — closing a footprint bulk-loads its covering DEM tiles and integrates terrain-vs-base per cell on a metric tangent-plane grid (cell size = the DEM's GSD at the ring's latitude, scanline point-in-polygon, bilinear tile-seam-correct sampling, worker-offloaded with a synchronous fallback), reporting mixed cut AND fill within one footprint on undulating ground plus `cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3` (± = per-cell cellArea × 1.5 × GSD, summed per side) and `nodataFraction` on the readout. `base-surface` picks the reference surface: `custom` (default — the gizmo's draggable target plane, re-summed live from the cached grid during a drag) or boundary-derived stockpile strategies with NO gizmo (`triangulated` boundary TIN — the drone-survey default, `plane` least-squares, `lowest`/`highest`/`average`). No terrain (or a failed tile fetch) falls back to the flat single-elevation approximation with no error figures rather than erroring. Five more volume-only attributes, all no-ops without `volume` in `modes` (validation warns): `base-surface` (above); `profile` — closing a footprint also samples elevation around its own perimeter, dispatched on `profileSeries` for a paired `dynamic-chart` widget to plot, updating live from the first vertex (debounced on hover, immediate on each new vertex) while sketching, not just on close; `deadband` (m³, default 0) — zeroes a Cut/Fill figure below the threshold; `density` (t/m³ metric, lb/yd³ imperial) and `swell`/`shrink` (multipliers, default 1×) populate a SEPARATE Material section instead of touching Cut/Fill/Net/Total — standard Bank/Loose/Compacted convention: `cutAdjustedMeters3` = raw cut × swell (loose/haul volume, bigger — excavating adds air voids), `fillAdjustedMeters3` = raw fill ÷ shrink (loose/borrow volume needed, also bigger — raw fill is already a compacted target void), `cutMassKg`/`fillMassKg` from the RAW volume (mass-conserving — swell/shrink change volume via air voids, not the mass of material). The widget only renders the Material section once at least one of `density`/`swell`/`shrink` is configured — no separate toggle. `stale` flags the brief window between a footprint committing and its elevation sample resolving.
|
|
440
440
|
- `vega-lite`
|
|
441
441
|
- `dynamic-chart` — same Vega-Lite rendering as `vega-lite`, but data-driven by a live DOM event instead of a layer/`ctx.data`: `on="<event-name>"` (required — the event to listen for on `<om-map>`), `series-field="<name>"` (default `series`) reads `event.detail[seriesField]` as the chart's `data.values` and re-embeds on every event where that field is a present array; `width` (fixed, default 280) and `title` work the same as `vega-lite`. The child `<script type="application/json">` spec is the same Vega-Lite mark/encoding shape, minus `data` (supplied live). A feature "freezes" the chart for free by simply not including the field on a later event (e.g. switching modes) — the widget has no separate pause API, it just does nothing when the field is absent. Built for a feature that computes its own series as the user interacts (a drawn line's elevation profile updating vertex-by-vertex) and has no layer of its own to bind to.
|
|
442
442
|
- `player`
|
|
@@ -712,7 +712,7 @@ Use a normal GeoJSON layer bound to a draw store plus a draw widget:
|
|
|
712
712
|
autosave="my-sketch"></om-widget>
|
|
713
713
|
```
|
|
714
714
|
|
|
715
|
-
The draw widget supports points, lines, polygons, delete-last, clear, save, and autosave. Lines/polygons close with double-click or
|
|
715
|
+
The draw widget supports points, lines, polygons, delete-last, clear, save, and autosave. Lines/polygons close with double-click, touch double-tap (detected by the library — iOS WebViews never synthesize `dblclick`), Enter, or the toolbar's Finish button; Escape cancels the in-progress shape.
|
|
716
716
|
|
|
717
717
|
### 3D
|
|
718
718
|
|