@nika-js/onlymap 0.6.1 → 0.6.2
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 +49 -1
- package/README.md +28 -15
- package/dist/{LercDecode.es-CJnypw8j.js → LercDecode.es-BOJJr6Gx.js} +1 -1
- package/dist/{basemap-DrQ0-eyR.js → basemap-BofXgXxy.js} +13 -3
- package/dist/basemap.d.ts +10 -1
- package/dist/clip-box-controller.d.ts +94 -0
- package/dist/clip-box.d.ts +109 -0
- package/dist/data-layer.d.ts +11 -3
- package/dist/draw-controller.d.ts +75 -1
- package/dist/draw.d.ts +22 -4
- package/dist/elements/om-map.d.ts +19 -0
- package/dist/elements/om-overlay.d.ts +12 -0
- package/dist/geodesy.d.ts +29 -3
- package/dist/{geoparquet-Dix4lTNy.js → geoparquet-DapHATA0.js} +1 -1
- package/dist/html-data.d.ts +2 -2
- package/dist/{index-1UgNlfGR.js → index-7B-6Cbzu.js} +30958 -27910
- package/dist/{index-B_1PPJgC.js → index-BF9iO8Tq.js} +1 -1
- package/dist/{index-BQMjW5w0.js → index-BmX6IId3.js} +1 -1
- package/dist/{index-CXPaeisL.js → index-SPJVn_n_.js} +1 -1
- package/dist/{index-CAuT5j9Y.js → index-xmJjZxQJ.js} +2 -2
- package/dist/index.d.ts +5 -2
- package/dist/ir-snapshot.d.ts +3 -1
- package/dist/layers/bim-layer.d.ts +30 -7
- package/dist/layers/feature-mesh-layer.d.ts +1 -1
- package/dist/layers/popup-layer.d.ts +13 -0
- package/dist/legend-spec.d.ts +1 -1
- package/dist/{lerc-CbTjQ7uI.js → lerc-C6k7EzSN.js} +2 -2
- package/dist/license.d.ts +12 -5
- package/dist/measure-controller.d.ts +458 -4
- package/dist/onlymap.standalone.js +54016 -50958
- package/dist/onlymapjs.js +18 -17
- package/dist/parse-manifest.d.ts +3 -0
- package/dist/programmatic.d.ts +17 -3
- package/dist/{raster-0b0nSHUh.js → raster-Cuzhibe2.js} +2 -2
- package/dist/{raster-pipeline-D8siq7-4.js → raster-pipeline-DLrnJK8y.js} +1 -1
- package/dist/react/om-layer.d.ts +6 -1
- package/dist/react.js +167 -160
- package/dist/region-export-controller.d.ts +37 -0
- package/dist/region-export.d.ts +56 -0
- package/dist/runtime-core.d.ts +150 -0
- package/dist/selection.d.ts +11 -1
- package/dist/site-placement.d.ts +22 -0
- package/dist/snapping.d.ts +88 -0
- package/dist/terrain-heightfield.d.ts +53 -0
- package/dist/terrain-sample.d.ts +30 -0
- package/dist/terrain.d.ts +6 -1
- package/dist/units.d.ts +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/volumetrics-run.d.ts +13 -0
- package/dist/volumetrics-worker.d.ts +1 -0
- package/dist/volumetrics.d.ts +201 -0
- package/dist/{zarr-DCYro_Vs.js → zarr-CUzEX8fB.js} +2 -2
- package/docs/3d-assets.md +48 -0
- package/docs/live-data.md +9 -1
- package/docs/testing.md +2 -0
- package/llms.txt +10 -5
- package/onlymapjs.html-data.json +161 -2
- package/package.json +4 -2
- package/skills/onlymapjs/SKILL.md +11 -1
- package/skills/onlymapjs/references/react.md +1 -1
- package/skills/onlymapjs/references/syntax.md +20 -8
package/CHANGELOG.md
CHANGED
|
@@ -8,9 +8,57 @@ Note: npm collapsed a few closely-spaced releases — the GPX/FlatGeobuf (0.5.4)
|
|
|
8
8
|
and GeoParquet (0.5.5) work shipped to npm together as **0.5.6**, so npm's
|
|
9
9
|
version list jumps 0.5.3 → 0.5.6. Each logical version is listed here regardless.
|
|
10
10
|
|
|
11
|
-
## 0.6.
|
|
11
|
+
## 0.6.2 — 2026-08-12
|
|
12
12
|
|
|
13
13
|
### Added
|
|
14
|
+
- **Region 3D export** (issue #34 — ported from a sibling project's proven implementation): the `draw` widget's `export-3d` attribute adds an "Export 3D" button that clips loaded `Tile3DLayer`/`BIMLayer` triangle geometry to the drawn footprint (a plain 2D ring — no elevation picking involved) and downloads the result as a portable GLB (positions re-framed to a local coordinate frame at the footprint's own centroid, so it opens correctly in Blender/three.js/etc. without needing ECEF-scale support), or `export-3d="b3dm"` for the same mesh wrapped for Cesium/3D-Tiles pipelines (with a `_BATCHID` attribute + a feature table, namespaced per source tileset so two different tilesets' local batch ids don't collide). Reuses the existing `om-tileset-load` consumer event — its own doc comment already anticipated "tools (e.g. region export) that need the real tileset" — to resolve the live `Tileset3D` registry; no new picking/loader plumbing needed. Each triangle exports with its own source color (standard glTF `pbrMetallicRoughness.baseColorFactor`, baked as per-vertex `COLOR_0`) — no textures (BIM/IFC materials are flat colors, not textured meshes — dropped rather than carried for a case this library doesn't have). Region export also only pulls in currently-**visible** 3D Tiles/BIM layers — a layer hidden via `visible="false"` (or the `toggle-layer` action) is skipped, with distinct console warnings for "nothing loaded yet" vs. "everything loaded but hidden."
|
|
15
|
+
- **Clip box** (issue #34 — the geometric complement to the export above): a new `<om-widget type="clip-box">` widget plus `<om-map clip-box-min="[lng,lat,elev]" clip-box-max="[lng,lat,elev]" clip-box-invert clip-box-highlight>` scene-state attributes cut a real axis-aligned 3D box through the whole scene — every layer is clipped by default, opt a layer out with `clip="off"`. `clip-box-invert` shows what's outside the box instead of inside; `clip-box-highlight` dims clipped-out geometry instead of discarding it (a non-destructive preview — nothing disappears). Works on any layer type, including georeferenced `Tile3DLayer`/`BIMLayer` content (3D Tiles, IFC/BIM models) — cutting into a dense BIM scene, not just flat `GeoJsonLayer` extrusions, was the whole point. New `set-clip-box {min, max, invert?, highlight?}` action (`{clear: true}` removes the box); attribute-backed, so changes are undoable and story-steppable. v1 is axis-aligned only — rotated/oriented boxes are a documented follow-up. The clip box itself renders as a visible, draggable solid cuboid with a double-headed-arrow gizmo on each of its 6 faces (pointing along that face's own normal, pixel-locked size so it stays grabbable at any zoom) — drag a face to resize the box directly, gated behind an explicit "Show face gizmos" toggle so the handles don't sit on top of ordinary map panning when not in use.
|
|
16
|
+
- **`BIMLayer` now lands on real terrain, not just near it** (issue #34 Part B groundwork): a georeferenced IFC file's `IfcMapConversion.OrthogonalHeight` (its real absolute elevation) was computed but never applied to the rendered model — correct on a flat basemap (a local Z≈0 model sits right on a Z=0 ground plane), wrong once real `terrain` is active (the model stayed at Z≈0 while the real ground elevation at any inhabited location is essentially never 0, burying it inside the terrain mesh). `BIMLayer` now applies its own `OrthogonalHeight` automatically whenever terrain is active — no attribute to author — reusing the same `tilesetPlacementMatrix` ENU re-anchoring the BIM workbench's `ifc-loader` widget already used manually for models it loads via drag-and-drop.
|
|
17
|
+
- **Depth-aware picking (`pickable="3d"`) + a real z on hover/click**: any `pickable` layer can opt into deck's depth-pick render pass with `pickable="3d"` instead of a bare `pickable` — terrain now sets this on itself — so a click/hover's resolved coordinate carries a real 3rd (elevation) component instead of the ray∩z=0-plane guess a flat pick gives you. `ctx.selection.coordinate` (and `<om-overlay>`/`show-tooltip` template interpolation, now `{{z}}`) carries it through end to end; absent (not `0`) when no layer in the scene ran the depth pass for that pick.
|
|
18
|
+
- **New example: `dev/examples/features/widgets/3d-snapping-cutting-tools.html`** (replaces the earlier `bim-snap-export-clip.html`) — region export, clip box, XY snapping, and z-aware hover picking together against the buildingSMART Medical-Dental Clinic (IFC 2x3, CC-BY 4.0), the same public sample the BIM workbench demo uses. A real multi-storey building is what these tools need: dozens of walls give the snap agents genuine corners and edges to lock onto, and a dense interior is the point of cutting a box through it. The clinic georeferences itself through `IfcSite` and declares no `OrthogonalHeight`, so the page authors `terrain="off"` — a model with no vertical datum placed over a real DEM renders buried inside the terrain mesh.
|
|
19
|
+
- **XY snapping** (issue #34 Part A): `<om-map snap="vertex edge midpoint" snap-tolerance="12">` refines a click/hover to the nearest vertex, edge, or edge midpoint of whichever feature deck already picked under the cursor — a two-stage design (deck's own pick narrows to one feature per frame for free; only that feature's own geometry is searched, on the CPU, when snapping is on), not a spatial index. Applies to every vector layer by default (`snap="off"` opts a layer out, mirroring clip box's own default) and to a `BIMLayer`'s own edge/crease overlay — its real wall corners/edges, converted from the model's local mesh coordinates back to real `[lng, lat]` automatically (`site-placement.ts`'s new `localOffsetToLngLat`, fed the model's own reported georeference). Vertex beats midpoint beats edge in range-conflicts. Space suppresses snapping momentarily; a "Snapped: …" tip shows which agent fired. New `src/snapping.ts` (pure resolver, 18 unit tests). XY only for now — a snapped vertex takes the matched feature's horizontal position; threading its exact elevation through the shared draw/measure position type is a documented follow-up.
|
|
20
|
+
- **"Flat target plane" toggle for volume measurement** (`base-surface="custom"` only) — now a real semantic switch, not a rendering preference. Default (toggle off): the target surface is the terrain itself offset by the dragged distance — a slope-parallel prism, volume = |offset| × area, pure cut OR pure fill, and exactly **zero at rest** (previously the readout reported "grade everything to the centroid's elevation" numbers before the gizmo was ever touched). Toggled on: the target is one genuinely level plane, per-cell integrated — mixed cut AND fill on sloped ground. Toggling re-integrates the cached grid, so the rendered prism and the reported numbers are always the same shape. The drawn footprint ring itself always hugs real per-vertex terrain in both modes — only the target face changes. Also fixes the flat prism's broken rendering (missing side walls, mis-triangulated caps): its wall quads are vertical polygons, which the default XY-plane earcut triangulates to zero triangles — the explicit prism now renders through its own dedicated `SolidPolygonLayer` with `full3d` (the exact CityJSON-surfaces recipe), instead of mode-flipping the live extruded layer. The prism's ground edge is now DENSIFIED (~192 perimeter samples, each with its own terrain elevation from the same commit-time batch) instead of using only the drawn corners — corner-only geometry drew straight bottom edges across every groove between vertices — and the terrain-draped footprint stays visible through fills as the prism's conforming interior ground face.
|
|
21
|
+
- **Real per-cell volumetric integration** (issue #35 — replaces the v1 flat single-elevation approximation): closing a volume footprint now bulk-loads the DEM tiles covering its bbox (`loadHeightfieldForBounds`, budget-capped with honest zoom coarsening), lays a metric grid over the polygon in a local tangent-plane frame at the ring centroid (cell size = the DEM's ground-sample distance at that latitude — never silently finer than the data; Web Mercator's sec²(φ) area distortion avoided by construction), fills it by scanline point-in-polygon, and integrates terrain-vs-base per cell with bilinear, tile-seam-correct sampling — all in a Web Worker (inline-blob, CDN-safe, same pattern as the COG decoder; synchronous fallback when workers are unavailable). One footprint on undulating ground now reports mixed Cut AND Fill simultaneously, each with a published ±error (per-cell `cellArea × 1.5 × GSD`, summed per side — quoted against the SOURCE's GSD, never the grid spacing), plus cell size, GSD, and a no-data fraction, all on the `om-measure` readout (`cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3`/`nodataFraction`) and in the widget panel (± on the Cut/Fill rows, Base surface/Cell size rows, an under-reporting warning when data is missing). Without terrain, the flat fallback still runs — with no error figures, since there'd be nothing honest to quote.
|
|
22
|
+
- **`base-surface` attribute on the measure widget** (issue #35 — "Multiple Base Surfaces"): `custom` (default) keeps the draggable gizmo's target plane — drag frames re-sum the cached per-cell grid synchronously (`reintegrateCustomBase`), so the numbers stay real integration all through the gesture; `triangulated` (Delaunay TIN over densely-resampled boundary elevations — the drone-survey stockpile default, following a pile's toe all the way around), `plane` (least-squares fit), and `lowest`/`highest`/`average` derive the base from the footprint's own boundary — stockpile-style: no gizmo, the volume reports immediately on close. Switching strategies re-integrates against cached tiles (no re-fetch). Validation warns on unknown strategies and on `base-surface` without `"volume"` in `modes`.
|
|
23
|
+
- **`terrain-heightfield.ts` is now bilinear and seam-correct**: `elevationAt` interpolates over the four surrounding DEM pixels — across tile boundaries, with the neighbor tiles a tile-edge point needs auto-prefetched — upgrading the elevation profile and vertex sampling for free (previously nearest-pixel). Missing corners renormalize rather than failing the whole sample.
|
|
24
|
+
- **Cut/fill volume measurement**: the `measure` widget gains a third mode, `modes="distance area volume"`. Outline a footprint (closes like `area` — double-click or Enter); while flat (no drag yet) it drapes onto the terrain surface exactly like the sketch preview that drew it, then a real 3D double-headed arrow gizmo (an orange cylinder shaft with a cone at each end, one inverted) appears at its centroid — drag it up to fill or down to cut, and it switches to a solid extruded prism at that instant. Reads out `cutMeters3`/`fillMeters3`/`netMeters3`/`totalMeters3` (Net signed fill−cut, Total unsigned cut+fill, each labelled with its own convention) alongside area/perimeter via the `om-measure` event. The gizmo is a FIXED SCREEN-PIXEL size (derived from the current zoom/latitude, not footprint- or drag-relative), so it stays a constant, grabbable size regardless of zoom or how far it's been dragged; the drag distance itself is unbounded — v1 has no real bulk elevation surface to derive a legitimate "how deep can this go" limit from, so there's nothing to clamp against. Requires `terrain` on `<om-map>` — validation warns when `volume` is authored with none. Cut/fill are computed by the real per-cell grid integrator (see "Real per-cell volumetric integration" below) — mixed cut AND fill within one footprint on undulating ground, not a flat single-elevation approximation.
|
|
25
|
+
- **Elevation profile, as a volume-mode `profile` attribute** (not a mode of its own): `<om-widget type="measure" modes="distance area volume" profile>` — closing a volume footprint also samples elevation around that same footprint's own perimeter (no separate line to draw) and dispatches it on the `om-measure` event's `profileSeries` field for a `dynamic-chart` widget to plot. Updates LIVE while still sketching, from the very first vertex — resampling immediately on each new vertex and, debounced (250ms), while just hovering the cursor around — not only once the footprint is closed. Validation warns if `profile` is authored without `"volume"` in `modes` (nothing for it to sample a perimeter around).
|
|
26
|
+
- **Cut/fill reporting factors** (`density`, `swell`, `shrink`, `deadband` — all volume-mode `measure` widget attributes). `cutMeters3`/`fillMeters3`/`netMeters3`/`totalMeters3` stay RAW geometric volumes — `deadband` (m³) zeroes out a figure below the threshold (drag noise near zero height), but `swell`/`shrink` never touch them, so these four keep answering "does this reach target elevation" regardless of what material's configured. `swell`/`shrink` (default 1×) instead populate two SEPARATE fields, `cutAdjustedMeters3`/`fillAdjustedMeters3`, on the standard Bank/Loose/Compacted earthworks convention: Adjusted Cut = raw × swell (loose/haul volume — excavating adds air voids, so it's bigger than the bank volume removed); Adjusted Fill = raw ÷ shrink (raw fill is already a compacted target-void volume, so the loose/borrow material actually needed is likewise bigger). `density` (t/m³ metric, lb/yd³ imperial) adds `cutMassKg`/`fillMassKg` — computed from the RAW volume, not the adjusted one, since swell/shrink change volume via air voids, not the actual mass of material; a second "adjusted tonnage" would misrepresent the same dirt as weighing differently depending on how it's packed. The widget's readout gets a separate "Material" section for the adjusted/tonnage figures, showing only when at least one of `density`/`swell`/`shrink` is actually configured — no separate on/off toggle, the attribute's presence is the opt-in. A new `stale` boolean on the readout flags the brief window between a footprint committing and its elevation sample resolving, so a consumer never mistakes a prior footprint's leftover numbers for current ones. Validation warns on a non-positive `density`/`swell`/`shrink`, a negative `deadband`, or any of the four authored without `"volume"` in `modes`.
|
|
27
|
+
- **`dynamic-chart` widget**: the same Vega-Lite rendering as the existing `vega-lite` widget, but data-driven by a live DOM event instead of a layer — `on="<event-name>"` + `series-field="<name>"` reads `event.detail[seriesField]` as the chart's data on every matching event and redraws at a fixed `width`. A feature "freezes" the chart for free by simply omitting that field on a later event (no separate pause API) — built for the elevation-profile attribute above (a feature that computes its own series live as the user draws and has no layer of its own to bind to), but intentionally generic (no elevation/measurement-specific code) so it's reusable for anything that needs a chart fed programmatically.
|
|
28
|
+
- **`geodesy.ts` gains `intermediate(a, b, fraction)` and `resamplePath(points, count)`**: `intermediate` is great-circle interpolation (spherical slerp) between two points; `resamplePath` walks a multi-segment polyline's cumulative length to place `count` evenly-spaced points along it — the elevation profile's sampling backbone. `midpoint()` is now defined as `intermediate(a, b, 0.5)` (same antimeridian-safe behavior, verified unchanged).
|
|
29
|
+
- **`terrain-heightfield.ts`: bulk terrain elevation sampling**, now wired to both volume mode's per-vertex/centroid sample and the elevation profile above. The existing `sampleTerrainElevation` is one network fetch per point — fine for volume mode's handful of footprint vertices, wrong for a profile's tens-to-hundreds of samples along a line. `loadHeightfield(points, terrain)` resolves every DEM tile the points touch, fetches and decodes each ONCE (deduplicated, in parallel, with an 8s per-tile fetch timeout), and returns a synchronous in-memory `elevationAt()` lookup — no further network access per sample. Tested entirely against synthetic, known-by-construction DEM tiles (no real ground-truth file needed or used), specifically covering the genuinely new risk vs. the single-point sampler: correctness across a tile SEAM.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- **The spacebar is no longer swallowed page-wide.** The snap-suppression key handler (`<om-map>`'s window-level `keydown`) called `preventDefault()` on the spacebar unconditionally — on every page containing a map, whether or not `snap` was configured, and regardless of what had focus. That broke space-to-scroll (the browser default, and how keyboard-only users page down) and space-to-activate on any focused `<button>`, including this library's own widget buttons. It is now gated on a snap resolver actually being configured, and skips space-activated controls (`button`, `summary`, `a[href]`, `[role=button]`, focusable elements) alongside the existing text-input guard. Snapping's own "hold Space to place a point without snapping" behavior is unchanged where snapping is on.
|
|
33
|
+
- **`clip="off"` (the clip box's documented per-layer opt-out) never worked.** It was published in the docs and offered by editor IntelliSense, but was not registered as a layer attribute — so it warned as an unknown-attribute typo, never reached the IR, and the clip box kept clipping the layer the author had explicitly opted out. `snap="off"` had a narrower version of the same problem: registered on only four vector layer types, so it was rejected on a `BIMLayer` — precisely the layer whose edge overlay snapping reads. Both are now registered on **every** layer type, matching their scene-wide defaults, and stripped before deck layer construction like the other OnlyMapJS-only props.
|
|
34
|
+
- **Snapping and clip-box attributes are now validated.** `snap`/`snap-tolerance` on `<om-map>` (an unrecognized agent, e.g. the plural `snap="vertices"`, silently left snapping off) and the per-layer `snap`/`clip` opt-outs (any value other than `"off"` is a silent no-op) now produce warnings naming the fix, closing the gap `snapping.ts`'s own doc comment already promised.
|
|
35
|
+
- **An antimeridian-crossing volume footprint silently reported 0 m³.** `loadHeightfieldForBounds` computed its tile span as `east - west`, which goes negative across ±180°; the span then passed the tile budget at max zoom and the cover loop produced no tiles at all, which the integrator read as "every cell is no-data." The span now wraps around the world and the cover collects tiles on both sides of the seam.
|
|
36
|
+
- **Region export could ship a deleted layer's geometry.** The visible-layer filter excluded a layer only when its `<om-layer>` existed *and* was `visible="false"`; a layer removed from the DOM fell through to *included*, and the tileset registry was append-only so it never aged out. Removal is now detected (and the registry entry dropped) while programmatically-built layers, which legitimately have no element, stay exportable.
|
|
37
|
+
- **Snapping no longer invents edges across a `MultiPoint`.** Its members were held in one shared ring, so the `edge`/`midpoint` agents snapped to segments between unrelated points — empty space where no geometry is drawn. Each member now gets its own ring; vertex snapping is unchanged.
|
|
38
|
+
- **`base-surface="triangulated"` no longer scales as cells × triangles.** The TIN evaluator tested every triangle's bounding box for every grid cell — ~1.4×10⁸ tests per integration at the 750k-cell budget. Triangles are now bucketed into horizontal bands (indexed by overlap, so a triangle spanning several bands is found from all of them) and a cell only tests its own band's. Results are unchanged.
|
|
39
|
+
- **The snap re-pick no longer rebuilds its layer-id list on every pointer move.** It is memoized against deck's layer-array identity, so the filter runs per layer-set change rather than per mousemove.
|
|
40
|
+
- **Region export yields a frame before its (synchronous, main-thread) clip and pack**, so the button's own click feedback paints instead of the tab appearing to ignore it. The work itself still blocks — it consumes live `Tileset3D` objects, which can't be handed to a worker the way the volumetrics integrator's decoded tile buffers can — and that limit is now stated in `docs/3d-assets.md`.
|
|
41
|
+
- **The "Snapped" tip anchors at the snapped point, at its real height**: the tip's overlay anchored at `[lng, lat]` with an implied z=0, so for an elevated vertex (a wall corner 400 m up) it rendered far from the visible point, drifting with camera pitch. `SnapResult` now carries the matched point's elevation and the tip anchors with it (the overlay's existing 3-component-anchor contract). The tip also hides (`clip-to-map`, opt-in per overlay) when its own box would spill past the map viewport — an overhanging absolutely-positioned box inflates the page's scrollable overflow, and the resulting scrollbar → map resize → reprojection loop was visible as view jitter.
|
|
42
|
+
- **`<om-map>` is now its own positioning context** (`position:relative` in the injected defaults layer): overlays and widget chrome are `position:absolute` children positioned in map pixels, and without it they resolved against whatever ancestor happened to be positioned — correct only on pages where that box coincided with the map's own. Author CSS still wins (same `@layer onlymap-defaults`).
|
|
43
|
+
- **XY snapping actually finds vertices now** — three independent faults fixed. (1) The resolver only understood GeoJSON-shaped picks, so flat-row layers (CSV/JSON + `get-position`-style accessors — most data) silently never snapped: the layer's own resolved accessors now synthesize the geometry (`get-polygon` → Polygon, `get-path` → LineString, `get-source/target-position` → LineString, `get-position` → Point). (2) Elevated vertices (a BIM model's wall corners) were projected to screen as if at ground level — hundreds of pixels from where they're actually seen, so never within tolerance: vertex elevations now carry through the screen-space tolerance test. (3) `BIMLayer`'s edge/crease overlay rendered at z≈0 regardless of terrain — buried the model's whole elevation below the mesh (and picked as phantoms where nothing visibly was): the outline now rides the same `OrthogonalHeight` placement as the mesh, via `coordinateOrigin`'s elevation component (the matrix is rotation+scale only and silently drops a z).
|
|
44
|
+
- **Clicks no longer "pass through" tall content onto the ground behind it**: with snapping on, a click/hover that picks nothing snappable but DOES hit a real (non-terrain) feature now resolves at that feature's own coordinate — depth-aware (`unproject3D`) when the 3D pick pass ran — instead of falling back to the original pick's ray∩ground point far behind a building.
|
|
45
|
+
- **Activating the clip box crashed every placed BIM/3D-Tiles sublayer** ("invalid latitude" on each mesh primitive, model gone until reload): `ClipBoxExtension` projected the box's world-anchored lng/lat corners through `Layer#projectPosition`, which applies the layer's own `modelMatrix` — a placed model's meters-scale ENU matrix — to the *input* before interpreting it as lng/lat. The matrix is now suppressed for the corner projection, and a degenerate frame falls back to drawing that layer unclipped (with a one-time console warning) instead of throwing — a draw-time throw makes deck drop the layer entirely.
|
|
46
|
+
- **The `draw`/`measure` widgets' own geometry was invisible under an active `terrain`**: every runtime-internal layer was unconditionally forced to `terrain="off"`, so a sketch/measurement's plain `[lng, lat]` vertices rendered at literal sea level — hidden below or behind a raised terrain mesh — instead of draping onto its surface. An internal layer's own explicit `terrain` attribute now wins over that default (other internal layers that never set one, like trace-temps, are unaffected); the shared draw-preview and measure's committed-geometry/label layers opt into `terrain="drape"`.
|
|
47
|
+
- **Double-click-to-close on a `draw`/`measure` polygon could silently fail to finalize, or reopen a stray one-vertex shape right after closing**: the native `dblclick` event (which triggers the close) can fire before deck.gl's own click-gesture recognition has settled, so the double-click's own two taps were unreliable as vertices and could arrive late as an independent, unwanted click. Closing now uses the already-reliable hover cursor position as the final vertex instead of depending on the taps, and a stray click landing within a small pixel radius of the just-used closing point is swallowed rather than starting a new shape (a real double-click's two physical taps rarely land at the bit-identical pixel, so this is a proximity match, not exact equality).
|
|
48
|
+
- **The volume gizmo could render with its shaft/bottom cone invisible or buried underground**: deck's default lit material shaded faces pointed away from the scene light near-black (fixed via unlit rendering), and the gizmo's rest position extended symmetrically below the surface, occluded by the terrain/fill (fixed by keeping its floor pinned above ground). It's now also sized relative to the footprint instead of a fixed constant, and the extruded prism renders with `wireframe` (vertical struts + top/bottom rings) for both fill and cut.
|
|
49
|
+
- **A cut's extruded prism was invisible, including its own starting footprint**: the whole thing sits entirely below the original ground, so the opaque terrain mesh fully occludes it from a normal view — the `wireframe` struts existed but were behind the terrain, and even the prism's own top face (at ground level) was unreliable to see, being nearly coplanar with the terrain mesh. The original footprint now stays visibly shaded (the same green as a fresh/at-rest footprint) throughout a cut; a dedicated depth-test-disabled line layer draws one vertical strut per footprint vertex from ground down to the cut depth plus the closed target-ring boundary at the bottom (unshaded, just an outline) — visible through the terrain, so a cut's shape and depth read visually instead of only through the numeric Cut readout.
|
|
50
|
+
- **The extruded volume's base could visibly float above or sink below the terrain on sides away from the footprint's center**: v1 samples one flat elevation at the centroid for the cut/fill math (unchanged, still documented as a v1 simplification — see issue #35), but the rendered base ring now additionally samples each vertex's own real ground elevation, so the footprint's corners hug the actual terrain instead of a single flat plane.
|
|
51
|
+
- **The volume readout could claim "sampling elevation…" while the footprint was still an open, unclosed preview**: that state is now distinguished from an actually-closed footprint awaiting its terrain sample; a 3+-vertex preview shows live area/perimeter (matching `area` mode) with no cut/fill numbers until the shape is genuinely closed.
|
|
52
|
+
- **Dragging the volume gizmo far enough could make the whole extrusion (and its target-ring boundary) vanish, and cutting kept the gizmo pinned at the original ground instead of tracking the excavation**: root cause was the gizmo's own shaft length scaling with `|heightOffsetM|` with no upper bound, pushing its Z far enough from the scene to plausibly lose float precision in deck's projection. Fixed at the source — the gizmo is now a FIXED SCREEN-PIXEL size regardless of drag distance or zoom (re-derived from the current zoom/latitude on every viewport change, not just on drag/commit) and tracks the current working face symmetrically on both sides (just above the fill top while filling, just below the cut floor while cutting, rather than staying pinned at the original ground). The drag distance itself is no longer clamped at all (an earlier footprint-scaled clamp, and later a flat numerical backstop, were both removed in turn — neither reflected real ground-truth depth, and the float-precision failure mode they guarded against is now fixed at its actual sources: the fixed-pixel gizmo above, plus the `farZMultiplier` fix below).
|
|
53
|
+
- **The gizmo became invisible while cutting, immediately after the fix above; then became noticeably smaller and self-occluded oddly when made visible again; then its elevation tooltip disappeared too**: tracking the cut floor means the gizmo (and its tooltip, anchored at the gizmo's own vertical midpoint) sits below ground the same way the cut prism/guide lines do, and was occluded by the opaque terrain mesh the same way they were before their own fix. Disabling depth-testing unconditionally (the first attempt) fixed the occlusion but broke the gizmo's OWN internal self-occlusion — with 3 separate mesh instances (shaft + 2 cones) and no depth test, whichever drew last simply painted over the others regardless of true depth, most visible at closer zoom. The depth-test bypass is now conditional (only while actually cutting, re-evaluated every frame) and shared by the tooltip; the gizmo's fixed size was restored to its original scale (an intermediate fix had shrunk it below what it always was, not just capped its growth at extreme drag distances), then later halved again on request now that its size is a deliberate, tunable constant, not a bug; and `PopupLayer` (the tooltip's underlying layer) gained the same `parameters` passthrough as its earlier `pixelOffset` addition, since depth-test toggling wasn't previously plumbed through the composite layer at all. The tooltip text is now also prefixed `"Elevation: "` so the number isn't ambiguous out of context.
|
|
54
|
+
- Added a simple always-on tooltip beside the volume gizmo showing the current drag magnitude as an absolute value (m/ft depending on units) — no more needing to check the widget panel to see how far you've dragged. Cleared by a real screen-pixel offset (a new `PopupLayer` `pixelOffset` override), not a world-space one — a fixed-meters gap shrinks to fewer screen pixels at low zoom than the badge needs, which caused a reported overlap with the gizmo mesh.
|
|
55
|
+
- **The gizmo, its target-ring boundary, and its tooltip could all disappear together at high zoom on a deep cut** — a different bug class than the earlier occlusion fixes above: deck's far clipping plane is computed in units that stay roughly constant across zoom levels, while a fixed real-world depth maps to a progressively larger clip-space distance the closer you zoom in, eventually falling outside the camera's view frustum entirely (culled, not merely hidden behind something). The standalone-mode view (the mode `terrain`, and therefore the volume tool, always runs in) now sets `farZMultiplier: 10` — the effective ceiling for this setting, since deck's own horizon-distance cap clamps any higher value to the same result.
|
|
56
|
+
- **`DrawController.deleteLast()` never notified observers**, unlike its sibling `clear()` — a controller watching a target (e.g. `measure` in volume mode) kept showing numbers for a shape that had just been deleted, with no later mutation guaranteed to correct them. Now mirrors `clear()`'s own `notify()` call.
|
|
57
|
+
|
|
58
|
+
## 0.6.1 — 2026-08-06
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
- **`ylorrd` joins the curated raster colormaps**: the ColorBrewer yellow→orange→red sequential ramp now renders a proper gradient in the legend (any sprite colormap always rendered on the map; only curated names get legend ramps) and is listed in the docs as the gentler alternative to `turbo`/`jet` for heat-like fields.
|
|
14
62
|
- **`selection-type` on `<om-overlay>`** (React: `selectionType` on `<OmOverlay>`): scope a selection-anchored overlay to one pick type — `"click"` or `"hover"`. Fixes the reported ghost-popup bug: a popup wired to open on click re-anchored *and re-interpolated its template* on every hover pick (deck fires hover on all pointer movement), so it followed the pointer onto other features and rendered their objects into the wrong template with blank fields — with no author-side workaround. With `selection-type="click"`, hover picks are inert; a click on empty space still dismisses (empty picks now carry the pointer-event type internally), while hovering empty space no longer does. Absent attribute keeps the old behavior. Validation warns on a typoed value and on `selection-type` without `anchor-from="selection"`; the test harness's `clearSelection()` gains a kind (`"hover"` default, `"click"` for empty-space clicks).
|
|
15
63
|
- **BIM / IFC support**: `BIMLayer` loads `.ifc` files entirely in the browser (web-ifc WASM, CDN-fetched + integrity-pinned on first use, never bundled) and renders them as 3D Tiles; per-element **feature picking** on `Tile3DLayer` (`pick-features`, EXT_structural_metadata property tables, texture-backed IDs included); declarative **isolate / hide / ghost** (`feature-filter-field` + `isolate-features`/`hide-features`/`ghost-features`) and **style-by-property** (`feature-color-by`/`feature-color-scale`/`feature-palette`); **multi-model federation** (`ifc-loader federate`) and an AABB **clash-detection overlay**; widgets `ifc-loader`, `ifc-browser`, `feature-inspector` (alias `ifc-inspector`), `ifc-clash`; IFC georeferencing via `IfcMapConversion` (UTM + non-UTM projected CRS, grid-convergence heading correction) with a structured warning when a model's placement can't be trusted.
|
|
16
64
|
|
package/README.md
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/@nika-js/onlymap)
|
|
9
9
|
[](LICENSE.md)
|
|
10
10
|
[](https://docs.nikaplanet.com/onlymap/overview)
|
|
11
|
+
[](https://onlymap.nikaplanet.com/)
|
|
11
12
|
|
|
12
|
-
**
|
|
13
|
+
**[▶ Browse over 50 runnable examples](https://onlymap.nikaplanet.com/)** — heatmaps, choropleths, 3D terrain, live streams and map stories, each with copyable source.
|
|
14
|
+
|
|
15
|
+
OnlyMapJS is a map rendering and visualization library for web, desktop, and mobile apps. With support for popular frameworks like React, it integrates seamlessly into your existing components via a robust two-way interface for custom map behavior; overlays, charts, and finely controlled story-map animations are all first-class supported alongside 30+ data layer types. Built-in GPU acceleration also means it scales very well for both 2D and 3D use-cases. It is both human and AI agent-readable by design, and troubleshooting is effortless with a compile-time validator and runtime error debugging tools.
|
|
13
16
|
|
|
14
17
|
```html
|
|
15
18
|
<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap/dist/onlymapjs.css">
|
|
@@ -36,7 +39,7 @@ That's a complete app: a no-token MapLibre basemap, data-driven colors and sizes
|
|
|
36
39
|
|
|
37
40
|
It's also designed to be written **by AI agents**: HTML is a reliable generation target, [`llms.txt`](llms.txt) teaches the format, and `OmMap.validate()` returns structured errors **and warnings** with actionable fixes — a real feedback loop instead of a blank canvas (heed both: an "unknown attribute" warning means a prop is being silently dropped).
|
|
38
41
|
|
|
39
|
-
> ⚠️ **Status:
|
|
42
|
+
> ⚠️ **Status: beta.** Proprietary — free for non-commercial use with attribution; commercial licensing terms are in [LICENSE.md](LICENSE.md). APIs may still move before 1.0.
|
|
40
43
|
|
|
41
44
|
## Why not deck.gl directly?
|
|
42
45
|
|
|
@@ -69,7 +72,7 @@ Or with no build step at all, straight from a CDN:
|
|
|
69
72
|
|
|
70
73
|
The bare package URL serves `dist/onlymap.standalone.js`, a single-file bundle built for exactly this (jsDelivr too). Use a CDN that serves the package's raw files — **not** a rebundling CDN like esm.sh, which re-splits the bundle into duplicate copies of the deck.gl/luma.gl runtime and breaks every layer's shader compilation.
|
|
71
74
|
|
|
72
|
-
Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with
|
|
75
|
+
Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with 1,083 unit/behavioral tests and 62 Playwright GPU tests.
|
|
73
76
|
|
|
74
77
|
The [examples](https://github.com/NikaGeospatial/onlymapjs/tree/main/examples) are the best tour: widgets, behaviors & overlays, basemaps, columnar/Arrow data, manual drawing, 3D models, scene lighting (with the native lighting widget), DEM terrain, a live WebSocket ship feed, and a polled driver fleet.
|
|
75
78
|
|
|
@@ -81,8 +84,8 @@ A handful of elements, one rule: **attributes are kebab-case versions of deck.gl
|
|
|
81
84
|
|---|---|
|
|
82
85
|
| `<om-map>` | The map. `center`, `zoom`, `pitch`, `bearing`; `basemap` takes a free preset (`positron`, `liberty`, `dark-matter`, `osm`, …), a style URL, or `"none"` (standalone canvas) — and switches **live**; `validate` for a live on-page error panel. Give it a height: a custom element is `display:inline` by default, so the library injects a `display:block` default (fills a sized parent, else a 400px floor) to keep a bare map visible, but set an explicit height (`om-map { height: 100vh }`) for real layout — any height you set wins outright, including one below the floor. A map that still collapses warns in the console; `hidden` and `display:none` maps stay hidden and stay quiet. |
|
|
83
86
|
| `<om-layer>` | Any of **37 layer types** by name — all of deck.gl's core, geo, aggregation, and mesh layers (Scatterplot, GeoJson, Arc, Path, Heatmap, Hexagon, Trips, Tile, Tile3D, Scenegraph, …) plus the built-in `PopupLayer` for WebGL badges/labels at scale, the native `COGLayer` for GeoTIFF rasters, `ZarrLayer` for chunked Zarr/GeoZarr rasters, `ImageOverlay` for georeferenced drone JPEGs, and `BIMLayer` for BIM source files (`.ifc` today) loaded straight in the browser, no pre-conversion step. `id` required; `label`/`color` feed the legend. |
|
|
84
|
-
| `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `zoom-controls`, `undo-redo`, `scale-bar` (metric/imperial/nautical `units`), `attribution`, `filter`, `draw`, `measure` (geodesic distance + area — live labels, `units` toggle, an `om-measure` readout event), `vega-lite` (live charts), and the BIM set `ifc-browser` / `feature-inspector` / `ifc-loader` / `ifc-clash`. Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all authored chrome without destroying it — provider attribution and the license badge never hide. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). At map widths ≤640px, managed widgets automatically move into accessible top/end/bottom/start drawers; `fold="never"` keeps an essential control out, `widgets-fold="off"` disables folding, and `--om-widget-fold-breakpoint` changes the map-width threshold. Provider attribution is an in-flow member of `bottom-end` and the license badge of `bottom-start`, so neither covers a widget. A slot dims automatically while an open popup covers it (`widgets-dim="off"` to disable), except slots containing required chrome. Themeable from plain page CSS via custom properties: `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` (also `-muted`, `-border`, `-hover-bg`, `-accent`), plus layout tokens (`--om-widget-inset-x/-y`, `--om-widget-gap-x/-y`, `--om-widget-opacity`, `--om-widget-radius`) or the no-CSS sugar `<om-map widget-style="gap:10 opacity:0.9">`. |
|
|
85
|
-
| `<om-overlay>` | Rich HTML anchored to a map location — a static `anchor="[lng, lat]"`, the current selection, or a feature's own geometry via `anchor-layer`/`anchor-feature-id`. Selection-anchored overlays scope with `layer` (one layer's picks) and `selection-type="click"`/`"hover"` (one pick type — give a click-opened popup `selection-type="click"` so hovering elsewhere doesn't drag it along). `{{field}}` interpolates the picked feature, HTML-escaped by default. |
|
|
87
|
+
| `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `clip-box`, `zoom-controls`, `undo-redo`, `scale-bar` (metric/imperial/nautical `units`), `attribution`, `filter`, `draw` (point/line/polygon sketch capture, GeoJSON save/autosave; `export-3d` adds an "Export 3D" button that clips loaded `Tile3DLayer`/`BIMLayer` content to the drawn footprint and downloads it as a portable GLB — re-framed to a local coordinate frame at the footprint's own centroid, each triangle carrying its own source color as vertex colors, no textures — or, with `export-3d="b3dm"`, the same mesh wrapped for Cesium/3D-Tiles pipelines; only currently-visible 3D Tiles/BIM layers are included — a layer hidden via `visible="false"` or the `toggle-layer` action is skipped, with a distinct console warning for "nothing loaded" vs. "everything hidden"), `measure` (geodesic distance + area + cut/fill volume — live labels, `units` toggle, an `om-measure` readout event; `modes="distance area volume"` adds a footprint-then-extrude tool: outline a polygon, double-click to close it — it turns solid teal, ready — then drag the double-headed arrow gizmo that appears at its centroid up to fill or down to cut, reading out Cut/Fill/Net (signed, fill−cut)/Total (unsigned, cut+fill) volume plus Area/Perimeter, each labelled with its own sign convention. The math is a REAL per-cell grid integration: closing a footprint bulk-loads its covering DEM tiles, lays a metric grid in a local tangent frame at the ring centroid (cell size = the DEM's ground-sample distance; scanline point-in-polygon; bilinear, tile-seam-correct sampling; worker-offloaded), and integrates terrain-vs-base per cell — mixed cut AND fill within one footprint on undulating ground, with a published ±error (per-cell cellArea × 1.5 × GSD, summed per side), the cell size used, and a no-data warning all carried on the `om-measure` readout (`cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3`/`nodataFraction`). A `base-surface` attribute picks the reference surface: `custom` (default — the gizmo's draggable target plane), or boundary-derived stockpile strategies with no gizmo (`triangulated` boundary TIN, `plane` least-squares fit, `lowest`/`highest`/`average`). On `custom`, the extruded prism's own boundary hugs each corner's real ground elevation by default (so it doesn't visibly float above or sink below sloped terrain) — a "Flat target plane" toggle button appears above the readout once a footprint closes, switching that rendering to a single level plane instead (a rendering choice only; the underlying cut/fill numbers were already computed against one flat target elevation either way). Requires `terrain` on `<om-map>` — validation warns if `volume` is in `modes` with none (without terrain a flat-plane fallback runs, with no error figures — nothing honest to quote). A `profile` attribute alongside `modes` — not a mode of its own — samples elevation around a volume footprint's own perimeter as it's drawn and closed, live, and dispatches it on `om-measure`'s `profileSeries` field for a `dynamic-chart` widget to plot. Also volume-only: `deadband` (m³) zeroes out a Cut/Fill figure below the threshold, filtering drag noise near zero height (Cut/Fill/Net/Total are always RAW geometric volumes — deliberately unaffected by `swell`/`shrink`, so they keep answering "does this reach target elevation" regardless of what material's configured); `density` (t/m³ metric, lb/yd³ imperial) and `swell`/`shrink` (multipliers, default 1×) instead populate a separate Material section, standard Bank/Loose/Compacted earthworks convention — Adjusted Cut = raw × swell (loose/haul volume, bigger — excavating adds air voids), Adjusted Fill = raw ÷ shrink (loose/borrow volume needed, also bigger — the raw fill is already the compacted target void), plus Cut/Fill tonnage computed from the raw (not adjusted) volume, since swell/shrink change volume, not mass; the section only appears once at least one of `density`/`swell`/`shrink` is actually configured, no separate toggle. A `stale` field on the `om-measure` readout flags the brief window between a footprint committing and its elevation sample resolving, so a consumer doesn't read numbers left over from a prior footprint as current), `vega-lite` (live charts bound to a layer's data), `dynamic-chart` (the same Vega-Lite rendering, but data-driven by a live DOM event instead of a layer — `on="<event-name>"` + `series-field="<name>"` reads `event.detail[seriesField]` as the chart's `values` on every matching event, redrawing at a fixed `width`; a feature "freezes" it for free by simply omitting that field the next time it fires, no separate pause API needed), and the BIM set `ifc-browser` / `feature-inspector` / `ifc-loader` / `ifc-clash`. Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all authored chrome without destroying it — provider attribution and the license badge never hide. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). At map widths ≤640px, managed widgets automatically move into accessible top/end/bottom/start drawers; `fold="never"` keeps an essential control out, `widgets-fold="off"` disables folding, and `--om-widget-fold-breakpoint` changes the map-width threshold. Provider attribution is an in-flow member of `bottom-end` and the license badge of `bottom-start`, so neither covers a widget. A slot dims automatically while an open popup covers it (`widgets-dim="off"` to disable), except slots containing required chrome. Themeable from plain page CSS via custom properties: `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` (also `-muted`, `-border`, `-hover-bg`, `-accent`), plus layout tokens (`--om-widget-inset-x/-y`, `--om-widget-gap-x/-y`, `--om-widget-opacity`, `--om-widget-radius`) or the no-CSS sugar `<om-map widget-style="gap:10 opacity:0.9">`. |
|
|
88
|
+
| `<om-overlay>` | Rich HTML anchored to a map location — a static `anchor="[lng, lat]"`, the current selection, or a feature's own geometry via `anchor-layer`/`anchor-feature-id`. Selection-anchored overlays scope with `layer` (one layer's picks) and `selection-type="click"`/`"hover"` (one pick type — give a click-opened popup `selection-type="click"` so hovering elsewhere doesn't drag it along). `{{field}}` interpolates the picked feature, HTML-escaped by default; `{{z}}` is a real depth-picked elevation when some layer under the cursor set `pickable="3d"` (terrain does this itself, so hovering the ground always has one) — empty otherwise, never a misleading 0. `clip-to-map` (opt-in) hides the overlay when its own box would spill past the map viewport, not just when its anchor leaves — for small transient tips that track the cursor, where an overhanging box would otherwise inflate the page's scroll overflow and cause visible view jitter. |
|
|
86
89
|
| `<om-behavior>` | Declarative interactions: `on="click|hover|drag|load|data-loaded"` → a named action. |
|
|
87
90
|
| `<om-story>` | A storyboard: `<om-step>` children fire actions on a timeline. Controlled by the `player` widget, behaviors, or `storyEl.play()/pause()/seek()`. |
|
|
88
91
|
| `<om-fallback>` | What shows where scripts never run — chat-app/email file previews (iOS QuickLook renders HTML attachments with JS off), file managers, sandboxed webviews. A direct child of `<om-map>`, hidden automatically the moment the map boots; pages without one get a text-only default banner from the stylesheet. Good practice on any page that may travel as a file. The gate is pure CSS (`om-map:not(:defined)`), so `onlymapjs.css` must load without JS — a real `<link>`, a bundler-emitted sheet, or an inlined `<style>`. |
|
|
@@ -127,8 +130,8 @@ Numeric timestamps are always epoch **milliseconds**, never guessed as seconds.
|
|
|
127
130
|
| **GeoTIFF / COG raster** | `<om-layer type="COGLayer" src="./dem.tif" min="0" max="1900" colormap="viridis">` | Cloud-Optimized GeoTIFFs stream tiles by Range request (lazy chunk); min/max restretch + colormap swaps are GPU uniforms, nodata → transparent, legend ramp derives automatically; plain 8-bit RGB COGs need no attributes at all |
|
|
128
131
|
| **Zarr / GeoZarr raster** | `<om-layer type="ZarrLayer" src="./x.zarr" variable="temp" select="time=0" colormap="viridis" min="…" max="…">` | Chunked N-dimensional arrays (climate/weather grids, datacubes) rendered on the GPU (lazy chunk; built on `@developmentseed/deck.gl-zarr` + zarrita). Pin non-spatial dims with `select`; GeoZarr stores georeference themselves, a plain Zarr takes manual `bounds`/`crs`/`spatial-dims`; `min`/`max`/`colormap` reuse the COG pipeline. `src` can be any absolute URL — an external/remote store needs no server setup, but must be CORS-enabled (fetched directly in the browser) and public (authenticated stores are a follow-up) |
|
|
129
132
|
| **Drone JPEG image overlay** | `<om-layer type="ImageOverlay" src="./survey.jpg" georeference="exif">` | Reads GPS, relative altitude, camera, focal length, and DJI gimbal XMP; computes a WGS84 footprint, bakes yaw/roll into the pixels, and renders through `BitmapLayer`. Persist the processed image plus returned bounds for reloads — see [docs/image-overlays.md](docs/image-overlays.md) |
|
|
130
|
-
| **WebSocket stream** | `data="wss://feed" key="id" flush="250ms" source="myFormat"` | upsert-by-key, burst coalescing, auto-reconnect;
|
|
131
|
-
| **Polled REST snapshot** | `data="/api/fleet.json" refresh="5s"` | full-snapshot replace per poll; outages keep the last good data |
|
|
133
|
+
| **WebSocket stream** | `data="wss://feed" key="id" flush="250ms" source="myFormat"` | upsert-by-key, burst coalescing, auto-reconnect; shared while active and closed after the final descriptor owner releases it |
|
|
134
|
+
| **Polled REST snapshot** | `data="/api/fleet.json" refresh="5s"` | full-snapshot replace per poll; outages keep the last good data; removal/disposal stops polling |
|
|
132
135
|
| **Tiled layer (XYZ / vector)** | `<om-layer type="TileLayer" data="…/{z}/{x}/{y}.png">` or `type="MVTLayer"` | a `{z}/{x}/{y}` `data` template is deck's tile URL — passed through, never fetched as rows; raster `TileLayer` gets a built-in `BitmapLayer` renderer, `MVTLayer` self-renders vector tiles (`get-*` accessors apply to tile features) |
|
|
133
136
|
| **Draw store** | `data="draw:sketch"` | live in-memory GeoJSON feature store written by `<om-widget type="draw" target="sketch">` |
|
|
134
137
|
|
|
@@ -268,9 +271,13 @@ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot
|
|
|
268
271
|
|
|
269
272
|
## 3D
|
|
270
273
|
|
|
271
|
-
`ScenegraphLayer` instances glTF/GLB models at coordinates (no three.js, no loader wiring), `Tile3DLayer` consumes OGC 3D Tiles, `GeoJsonLayer` extrudes polygons (`extruded get-elevation="$height"`), and the camera tilts via `pitch`/`bearing` attributes. Terrain is one attribute: `<om-map terrain="terrarium">` raises a real DEM surface (keyless AWS tiles; `mapterhorn` keyless too, paired with a CARTO Positron drape by default; `maptiler-terrain` keyed; or a bring-your-own `{z}/{x}/{y}` DEM URL + `terrain-decoder`), geographic layers drape onto it automatically (per-layer `terrain="drape|offset|off"` overrides; 3D models sit ON the surface), `terrain-exaggeration` scales the relief, and `terrain-texture` drapes imagery. Terrain replaces an active basemap while on (flat canvas vs raised surface) and restores it when off. **BIM models require explicit terrain**: a georeferenced model's absolute elevation only means anything against a DEM surface, and the library never writes attributes for you — a model that resolves real elevation on a map without a `terrain` attribute raises an error through the validation channel at load time (any authored value satisfies it, including an explicit `terrain="off"` for flat-ground siting). Scene lighting is declarative: `<om-map lighting="daylight">` (presets `daylight`/`studio`/`flat`/`custom`, tuned via `lighting-ambient`, `lighting-sun`, `lighting-sun-azimuth`/`-elevation`, or `lighting-sun-date` for a solar-position sun) — attribute-backed, so lighting changes are undoable and story-steppable via the `set-lighting` action. `<om-widget type="lighting">` gives users native preset radios + tuning sliders over the same attributes. Converting IFC/CAD upstream: [docs/3d-assets.md](docs/3d-assets.md).
|
|
274
|
+
`ScenegraphLayer` instances glTF/GLB models at coordinates (no three.js, no loader wiring), `Tile3DLayer` consumes OGC 3D Tiles, `GeoJsonLayer` extrudes polygons (`extruded get-elevation="$height"`), and the camera tilts via `pitch`/`bearing` attributes. Terrain is one attribute: `<om-map terrain="terrarium">` raises a real DEM surface (keyless AWS tiles; `mapterhorn` keyless too, paired with a CARTO Positron drape by default; `maptiler-terrain` keyed; or a bring-your-own `{z}/{x}/{y}` DEM URL + `terrain-decoder`), geographic layers drape onto it automatically (per-layer `terrain="drape|offset|off"` overrides; 3D models sit ON the surface), `terrain-exaggeration` scales the relief, and `terrain-texture` drapes imagery. Terrain replaces an active basemap while on (flat canvas vs raised surface) and restores it when off. **BIM models require explicit terrain**: a georeferenced model's absolute elevation only means anything against a DEM surface, and the library never writes attributes for you — a model that resolves real elevation on a map without a `terrain` attribute raises an error through the validation channel at load time (any authored value satisfies it, including an explicit `terrain="off"` for flat-ground siting). When terrain IS active, `BIMLayer` automatically re-plants itself at its file's own `IfcMapConversion.OrthogonalHeight` (its real absolute elevation) instead of the local Z≈0 a flat basemap expects — no attribute to author, it just lands on the surface. Scene lighting is declarative: `<om-map lighting="daylight">` (presets `daylight`/`studio`/`flat`/`custom`, tuned via `lighting-ambient`, `lighting-sun`, `lighting-sun-azimuth`/`-elevation`, or `lighting-sun-date` for a solar-position sun) — attribute-backed, so lighting changes are undoable and story-steppable via the `set-lighting` action. `<om-widget type="lighting">` gives users native preset radios + tuning sliders over the same attributes. Converting IFC/CAD upstream: [docs/3d-assets.md](docs/3d-assets.md).
|
|
275
|
+
|
|
276
|
+
**Clip box.** `<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 — every layer is clipped by default (`clip="off"` opts a layer out), `clip-box-invert` shows outside the box instead of inside, and `clip-box-highlight` dims clipped-out geometry instead of discarding it (a non-destructive preview). Works on any layer type, including georeferenced `Tile3DLayer`/`BIMLayer` content, so a dense BIM scene can be cut open to see what's inside. Attribute-backed (undoable, story-steppable) via the `set-clip-box` action; `<om-widget type="clip-box">` gives users the same six numeric fields plus invert/highlight toggles. v1 is axis-aligned only — rotated boxes are a documented follow-up.
|
|
272
277
|
|
|
273
|
-
**
|
|
278
|
+
**XY snapping**, for draw/measure vertex capture. `<om-map snap="vertex edge midpoint" snap-tolerance="12">` (pixels, default 12) refines whatever deck already picked under the cursor to the nearest vertex, edge, or edge midpoint of that feature's own geometry — a two-stage design, not a spatial index: deck's own hover/click pick narrows to one feature every frame for free, and only THAT feature's geometry gets searched, on the CPU, when snapping is on. Applies to every vector layer by default (`snap="off"` opts a layer out, mirroring clip box's own default) and to a BIMLayer's own edge/crease overlay — its real wall corners and edges, converted from the model's local mesh coordinates back to `[lng, lat]` automatically, so a drawn footprint can snap onto an actual building rather than free-clicking near it. Vertex beats midpoint beats edge when more than one is in range. Hold Space to place a point nearby without snapping (standard CAD/GIS convention); a small "Snapped: …" tip shows which agent fired. XY only — a snapped vertex takes the tolerance-matched feature's horizontal position, not its exact elevation (drawn geometry still drapes onto terrain the normal way). See `dev/examples/features/widgets/3d-snapping-cutting-tools.html`.
|
|
279
|
+
|
|
280
|
+
**Per-element picking on 3D Tiles (BIM/photogrammetry).** `pickable="3d"` (instead of a bare `pickable`) runs deck's depth-pick pass, so a click/hover's coordinate carries a real 3rd (elevation) component instead of the ray∩z=0-plane guess a flat pick gives you — needed for any tool that reads *where in 3D* you picked, not just *what*; terrain sets this on itself automatically. A `pickable` `Tile3DLayer` picks a whole tile; adding `pick-features` picks an individual ELEMENT — a wall, a window, one IFC product — and the `selection` carries that element's `featureId`, `properties` and `class`, resolved from the tile's own `EXT_mesh_features` + `EXT_structural_metadata` (`feature-id-property` selects the ID set, default `_FEATURE_ID_0`). Isolate/hide/ghost are declarative and mirror the vector filter vocabulary — `feature-filter-field="component"` names the metadata field, then `isolate-features`/`hide-features`/`ghost-features` take JSON value lists (`ghost-opacity` tunes the fade); hiding is a shader discard, so a hidden element also stops being pickable and you can select what's behind it, and `isolate-features` is exclusive (anything unlisted is hidden). Because they're attributes they're undoable and story-steppable. Underneath, `feature-styles` recolours, fades or highlights elements by feature ID — an array indexed by id of `{color, strength, opacity}` — uploaded as a small lookup texture, so restyling costs no refetch and no re-tesselation. IDs stored per texel (how photogrammetry classification ships) are sampled per FRAGMENT, so element boundaries are pixel-sharp rather than snapped to triangle edges. Multi-material models fan out correctly (glTF allows one material per primitive, so a five-material house is five primitives and a real IFC export is often dozens); only genuinely instanced i3dm content falls back to tile-granularity picking. Two limits, stated plainly: texture-backed IDs need `load-options` with `gltf.loadImages`/`loadBuffers` **and** `image: {"type": "data"}` (without it loaders.gl reads the whole ID texture back through a canvas once per vertex and the tileset takes minutes to appear); and `opacity` below 1 currently blanks the model, so use `strength` to tint for now. See `dev/examples/features/terrain-3d/inspect-a-bim-model.html` (per-element picking, isolate/hide/ghost, the model tree) and `dev/examples/features/widgets/3d-snapping-cutting-tools.html` (`pickable="3d"` with `{{z}}`, the clip box, region export and snapping).
|
|
274
281
|
|
|
275
282
|
|
|
276
283
|
**The BIM widgets.** Three built-ins turn a `pick-features` tileset into a usable model browser without page JS. `ifc-browser` (renamed from `ifc-legend`, which still works — it browses a model, it doesn't explain symbology) groups the model by any property-table field (`fields="ifcClass material container spatialPath"`), counts each value, and gives every row I/H/G buttons that write `isolate-features`/`hide-features`/`ghost-features` — so the panel is a UI over the attributes, and everything it does is undoable and story-steppable; it keeps the companion outline layer's `filter-categories` in step too, since hidden elements would otherwise leave their edges behind. It also offers whichever **model trees** the file supports — spatial (IfcSite → IfcBuilding → IfcStorey), type, system, and classification — each expandable, counts aggregated upward, the same I/H/G on every node so isolating a storey, a duct system or a CCS code reaches everything beneath it. Spatial isn't privileged: on a real Danish project the classification tree covered 3,415 elements to spatial's 660. A tree isn't a second mode or a second widget — it's a group-by on a metadata column, which is why it reuses the visibility attributes untouched (and why there's one browser per layer: those attributes are single-valued). `loadIfc` extracts every hierarchy a model has and emits a column only when the file populates it, so a tree that would render empty is never offered. `feature-inspector` (renamed from `ifc-inspector`, which still works — its body is generic property-row rendering with no IFC dependency, so it works unchanged on a non-IFC `pick-features` layer) shows the picked element's properties (`fields="ifcClass material container netVolume"`). `ifc-loader` is a drop zone that parses an `.ifc` in the browser and builds both layers for it; add `federate` and one drop zone accepts SEVERAL models into a co-registered scene, each with its own visibility toggle — the shape coordination tools use, because a real project has architecture, structure, mechanical, electrical and plumbing rather than two. `ifc-clash` compares any two loaded models. Colour is a question, not decoration: `feature-color-by` colours categorically by a field, `feature-color-scale` graduates over a numeric one, `feature-palette` and `feature-color-strength` tune them — and setting *neither* is the default, leaving the model in its own IFC surface colours. Widget scripts reach the same data through `ctx.features(layerId)` (the decoded property table) plus the `features` watch token, which fires when the table decodes off the first tile.
|
|
@@ -279,15 +286,15 @@ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot
|
|
|
279
286
|
|
|
280
287
|
It is deliberately **not** Navisworks. An axis-aligned box test over-reports anything diagonal, and it cannot tell a resolved penetration from a collision: on that same pair, 676 of the results are air diffusers sitting inside ceilings, which is how buildings work. Treat the number as *"here are the places two disciplines occupy the same box — go look"*, not as a defect list. Persisting and sharing results is what BCF exists for and is not attempted here.
|
|
281
288
|
|
|
282
|
-
**In-browser IFC.** `loadIfc(bytes)` parses an `.ifc` with web-ifc (WASM) and returns a 3D Tiles model held entirely in memory — nothing uploaded, nothing written to disk. Because the output *is* a tileset, per-element picking, styling, `site-*` and isolate/hide/ghost work on it unchanged. It returns `tilesetUrl`/`edgesUrl` blob URLs, the property table, the georeferencing (`lonLat`, `heading`, `scale`, and a `headingSource` that distinguishes a real reading from an assumption), per-phase `timings`, and a `revoke()` you must call when swapping models. web-ifc is MPL-2.0 and is *not* a package dependency — it's dynamic-imported from unpkg on first use, so it never reaches the bundle and pages that never open an IFC pay nothing (measured: the IFC chunk is 8.5 KB gzipped and contains only the URL). **For offline, air-gapped or strict-CSP deployments**, `npm run vendor:web-ifc public/vendor/web-ifc` copies the four files you serve yourself, then `wasm-path="/vendor/web-ifc/"` on the loader — 1.37 MB gzipped of *static assets*, with the JS bundle unchanged. Note that web-ifc is only one network dependency: a georeferenced model also switches a basemap on, so an offline page wants `basemap="none"` and `telemetry="off"` too. See `dev/examples/bim-
|
|
283
|
-
**Georeferencing is declarative too.** `site-origin="[lng, lat]"` (optionally `[lng, lat, elevation]`), `site-heading` (a bearing, degrees clockwise from true north) and `site-scale` place a model from the markup instead of baking a position into the tileset — `site-origin` overrides whatever the root transform carries, and rotation/scale pivot on the model's own anchor so a heading change spins the building about itself. They work on `Tile3DLayer` and on `PathLayer` (whose paths are then local east/north/up metres), which is what lets an IFC mesh and its outline overlay move together. This matters more than it sounds: authoring tools ship a default project location and a default is indistinguishable from a survey — the buildingSMART Medical-Dental Clinic sample carries Revit's Boston default, the Duplex a Chicago city-centre point — while `IfcMapConversion` is absent from most IFC2x3 exports and `TrueNorth` is routinely unset. Being attributes, corrections are undoable and story-steppable rather than a reconversion. Whenever a model resolves anything short of a real `IfcMapConversion` — `IfcSite` coordinates or no georeference at all — `BIMLayer` and `ifc-loader` raise a structured `"warning"` through the same validation channel other `om-layer` errors use (the `validate` on-page panel, `om-validation-error`'s `detail.warnings`), rather than only a status-line sentence a human has to notice; it never fails the map (`valid` stays `true`), it just names the layer and points at `site-origin`/`site-heading` as the fix. See `dev/examples/bim-
|
|
289
|
+
**In-browser IFC.** `loadIfc(bytes)` parses an `.ifc` with web-ifc (WASM) and returns a 3D Tiles model held entirely in memory — nothing uploaded, nothing written to disk. Because the output *is* a tileset, per-element picking, styling, `site-*` and isolate/hide/ghost work on it unchanged. It returns `tilesetUrl`/`edgesUrl` blob URLs, the property table, the georeferencing (`lonLat`, `heading`, `scale`, and a `headingSource` that distinguishes a real reading from an assumption), per-phase `timings`, and a `revoke()` you must call when swapping models. web-ifc is MPL-2.0 and is *not* a package dependency — it's dynamic-imported from unpkg on first use, so it never reaches the bundle and pages that never open an IFC pay nothing (measured: the IFC chunk is 8.5 KB gzipped and contains only the URL). **For offline, air-gapped or strict-CSP deployments**, `npm run vendor:web-ifc public/vendor/web-ifc` copies the four files you serve yourself, then `wasm-path="/vendor/web-ifc/"` on the loader — 1.37 MB gzipped of *static assets*, with the JS bundle unchanged. Note that web-ifc is only one network dependency: a georeferenced model also switches a basemap on, so an offline page wants `basemap="none"` and `telemetry="off"` too. See `dev/examples/features/terrain-3d/inspect-a-bim-model.html`.
|
|
290
|
+
**Georeferencing is declarative too.** `site-origin="[lng, lat]"` (optionally `[lng, lat, elevation]`), `site-heading` (a bearing, degrees clockwise from true north) and `site-scale` place a model from the markup instead of baking a position into the tileset — `site-origin` overrides whatever the root transform carries, and rotation/scale pivot on the model's own anchor so a heading change spins the building about itself. They work on `Tile3DLayer` and on `PathLayer` (whose paths are then local east/north/up metres), which is what lets an IFC mesh and its outline overlay move together. This matters more than it sounds: authoring tools ship a default project location and a default is indistinguishable from a survey — the buildingSMART Medical-Dental Clinic sample carries Revit's Boston default, the Duplex a Chicago city-centre point — while `IfcMapConversion` is absent from most IFC2x3 exports and `TrueNorth` is routinely unset. Being attributes, corrections are undoable and story-steppable rather than a reconversion. Whenever a model resolves anything short of a real `IfcMapConversion` — `IfcSite` coordinates or no georeference at all — `BIMLayer` and `ifc-loader` raise a structured `"warning"` through the same validation channel other `om-layer` errors use (the `validate` on-page panel, `om-validation-error`'s `detail.warnings`), rather than only a status-line sentence a human has to notice; it never fails the map (`valid` stays `true`), it just names the layer and points at `site-origin`/`site-heading` as the fix. See `dev/examples/features/terrain-3d/inspect-a-bim-model.html`.
|
|
284
291
|
|
|
285
292
|
## Programmatic surface
|
|
286
293
|
|
|
287
|
-
- **`OmMap.*`** — `validate`, `snapshotIR`, `resolveImageOverlay`, `registerLayer`, `registerWidget`, `registerAction`, `registerSource`, `registerFormat`, `registerBasemap`, `configureBasemap`, `configureData`, `configureTelemetry`, `configureLicense`, `getLayerSchema`
|
|
294
|
+
- **`OmMap.*`** — `validate`, `snapshotIR`, `snapshotDescriptorIR`, `resolveImageOverlay`, `registerLayer`, `registerWidget`, `registerAction`, `registerSource`, `registerFormat`, `registerBasemap`, `configureBasemap`, `configureData`, `configureTelemetry`, `configureLicense`, `getLayerSchema`
|
|
288
295
|
- **`@nika-js/onlymap/deck`** — the bundled deck.gl classes (`CompositeLayer`, `TileLayer`, …) for building custom layer types: shims must extend the same class hierarchy the core renders with, not a second installed deck.gl copy. Recipe: [docs/custom-layers.md](docs/custom-layers.md)
|
|
289
296
|
- **On a `<om-map>` element** — `ready` (promise), `flyTo(coords, zoom?)`, `setLayerVisible(id, bool)`, `getLayers()`, `emit(action, payload)`, `snapshot(opts?)` (canvas-only PNG of basemap + layers at device pixels — DOM widgets/overlays and provider attribution are NOT captured, so exports must render credits themselves; `{as: "blob"}` for files, default dataURL); the `om-view-changed` event fires once the camera settles (debounced; `detail` = `{longitude, latitude, zoom, pitch, bearing, origin}`, where `origin` is `"user"` for gesture-driven bursts vs `"programmatic"` for API/story moves — the echo-suppression signal for state sync) — the camera-persistence hook; the `om-map-point` event (`detail = {coordinate: [lng,lat]|null, kind: "click"|"hover"}`) fires on every click/hover with the map coordinate, including empty-map clicks picks discard — the hook for custom capture tools the built-in draw widget doesn't cover; the `om-tileset-load` event (`detail = {layerId, tileset}`) surfaces a `Tile3DLayer`'s live deck `Tileset3D` for tools that need the real tileset (e.g. region export), not the IR; `document.querySelector("om-map")` is fully typed
|
|
290
|
-
- **`MapController`** — the framework-grade programmatic front-end (typed `LayerDescriptor`s → the same reconcile core, no DOM manifest): `setLayers`, `watch`, `emit`, camera methods, `injectPick`, `ready`, `snapshot`, an `onViewChange(view, origin)` option (the `om-view-changed` twin), plus `onMapPoint` / `onTilesetLoad` options (the `om-map-point` / `om-tileset-load` twins). The React adapter rides
|
|
297
|
+
- **`MapController`** — the framework-grade programmatic front-end (typed `LayerDescriptor`s → the same reconcile core, no DOM manifest): `setLayers`, `watch`, `emit`, camera methods, `injectPick`, `ready`, `snapshot`, `suspend`/`resume`, an `onViewChange(view, origin)` option (the `om-view-changed` twin), plus `onMapPoint` / `onTilesetLoad` options (the `om-map-point` / `om-tileset-load` twins). Active descriptors own reference-counted fetch/poll/socket handles: layer removal, transport-option change, suspend, and destroy release them. Accessor props normally take functions; schema-declared accessors also accept restricted expression strings such as `getPosition: "[$lon, $lat]"`, which makes the descriptor JSON-safe for native/cross-process bridges. `snapshotDescriptorIR(descriptors)` resolves that lane without fetching URL data. The React adapter rides the function form; vanilla/native hosts may use either
|
|
291
298
|
- **`getStore(token)`** — the external-store contract: per-token `{subscribe, getSnapshot}` stores (`viewport`/`selection`/`layers`/`data:<id>`) with cached immutable plain-data snapshots and `origin` tagging — directly consumable by `useSyncExternalStore` (the React adapter's own hooks ride it), MobX autoruns, Redux listeners, Zustand mirrors. ~20-line integration-tested recipes for Redux Toolkit, MobX/mobx-keystone, Zustand, and Jotai: [docs/external-stores.md](docs/external-stores.md)
|
|
292
299
|
- **Testing** — `mountForTest`, and imports are SSR-safe (importing in Node/jsdom never touches browser globals)
|
|
293
300
|
|
|
@@ -304,7 +311,13 @@ A license key lifts all limits and removes the badge:
|
|
|
304
311
|
OmMap.configureLicense("om_live_…"); // or once, in code
|
|
305
312
|
```
|
|
306
313
|
|
|
307
|
-
|
|
314
|
+
Packaged native hosts can verify an app-scoped key by passing platform-derived identity:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
await OmMap.configureLicense("om_live_…", { appId: "com.example.riders" });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The `appId` must come from trusted native build metadata, never page/bridge input. Keys are self-verifying signed tokens (no network round-trip, works offline and in CI) bound to declared domains and/or exact app identifiers. Licensing: https://www.nikaplanet.com/onlymap.
|
|
308
321
|
|
|
309
322
|
## Telemetry
|
|
310
323
|
|
|
@@ -312,7 +325,7 @@ The library reports one **deployment-scoped** usage snapshot per map per page lo
|
|
|
312
325
|
|
|
313
326
|
## Not implemented yet (honestly)
|
|
314
327
|
|
|
315
|
-
Mapbox GL basemaps, depth-interleaved 3D compositing, globe projection, SSE transport, multi-field filters, `dblclick` behaviors, the `transform` data pipeline, the typed fluent builder, and stories/draw as React components (both work via the HTML manifest). On `pick-features`: instanced (i3dm) tiles keep tile-granularity picking, and `feature-styles`' `opacity` below 1 is still being validated. On BIM: clash detection is bounding-box only (no mesh-level test, and it cannot tell a resolved penetration from a collision), there is no BCF export, no equivalent of Navisworks' Auto Reveal, and prepared tilesets converted before the `bbox*` columns existed cannot take part in a clash pass — `strength` tinting and `opacity: 0` (hide, a shader discard) work.
|
|
328
|
+
Mapbox GL basemaps, depth-interleaved 3D compositing, globe projection, SSE transport, multi-field filters, `dblclick` behaviors, the `transform` data pipeline, the typed fluent builder, and stories/draw as React components (both work via the HTML manifest). On `pick-features`: instanced (i3dm) tiles keep tile-granularity picking, and `feature-styles`' `opacity` below 1 is still being validated. On BIM: clash detection is bounding-box only (no mesh-level test, and it cannot tell a resolved penetration from a collision), there is no BCF export, no equivalent of Navisworks' Auto Reveal, and prepared tilesets converted before the `bbox*` columns existed cannot take part in a clash pass — `strength` tinting and `opacity: 0` (hide, a shader discard) work. On the measure widget's `volume` mode: the per-cell grid integration measures against the map's active terrain DEM only — measuring against an imported design surface or a previous survey (period-over-period reconciliation) and a cut/fill heat-map overlay are not implemented yet, and a snapped footprint vertex takes the matched feature's horizontal position only — its exact elevation is not threaded through (see **XY snapping** above).
|
|
316
329
|
|
|
317
330
|
## Going deeper
|
|
318
331
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as ny, L as sy, M as oy, m as ay, c as ka, z as oc, a as Sd, G as sm, V as ly, W as cy, b as uy, g as hy, d as Vu, f as Ed, e as dy, l as py, u as fy, D as Qf, h as my } from "./index-
|
|
1
|
+
import { C as ny, L as sy, M as oy, m as ay, c as ka, z as oc, a as Sd, G as sm, V as ly, W as cy, b as uy, g as hy, d as Vu, f as Ed, e as dy, l as py, u as fy, D as Qf, h as my } from "./index-7B-6Cbzu.js";
|
|
2
2
|
const Cd = Math.PI / 180, gy = 180 / Math.PI;
|
|
3
3
|
function em(Pe, Q = 0) {
|
|
4
4
|
const me = Math.min(180, Pe) * Cd;
|
|
@@ -25200,6 +25200,10 @@ class Ry {
|
|
|
25200
25200
|
setEffects(Q) {
|
|
25201
25201
|
this.overlay.setProps({ effects: Q }), this.map.triggerRepaint();
|
|
25202
25202
|
}
|
|
25203
|
+
/** XY snapping (spec: issue #34 Part A) — deck's own pick tolerance, 0 by default; MapboxOverlay forwards the same DeckProps a standalone Deck instance takes. */
|
|
25204
|
+
setPickingRadius(Q) {
|
|
25205
|
+
this.overlay.setProps({ pickingRadius: Q });
|
|
25206
|
+
}
|
|
25203
25207
|
/**
|
|
25204
25208
|
* Live basemap switch (spec: "Basemap presets & switching"). Deck layers
|
|
25205
25209
|
* survive BY CONSTRUCTION: the MapboxOverlay is a map control, not style
|
|
@@ -25222,9 +25226,15 @@ class Ry {
|
|
|
25222
25226
|
me && (me.setAttribute("data-om-mandated-chrome-item", "attribution"), me.style.margin = "0", this.attributionHost.appendChild(me), this.attributionElement = me);
|
|
25223
25227
|
}
|
|
25224
25228
|
}
|
|
25225
|
-
/**
|
|
25229
|
+
/**
|
|
25230
|
+
* Toggle map interactions (spec: "Manual Drawing", D4 — doubleClickZoom;
|
|
25231
|
+
* issue #34's clip-box gizmo — dragPan) — used to suspend a MapLibre
|
|
25232
|
+
* gesture while a tool needs sole ownership of it (double-click-zoom
|
|
25233
|
+
* while a draw tool is active; drag-pan while a clip-box face handle is
|
|
25234
|
+
* grabbed, or it fights the map's own camera drag for every mousemove).
|
|
25235
|
+
*/
|
|
25226
25236
|
setInteractive(Q) {
|
|
25227
|
-
Q.doubleClickZoom === !1 ? this.map.doubleClickZoom.disable() : Q.doubleClickZoom === !0 && this.map.doubleClickZoom.enable();
|
|
25237
|
+
Q.doubleClickZoom === !1 ? this.map.doubleClickZoom.disable() : Q.doubleClickZoom === !0 && this.map.doubleClickZoom.enable(), Q.dragPan === !1 ? this.map.dragPan.disable() : Q.dragPan === !0 && this.map.dragPan.enable();
|
|
25228
25238
|
}
|
|
25229
25239
|
isLoaded() {
|
|
25230
25240
|
return this.initialized;
|
package/dist/basemap.d.ts
CHANGED
|
@@ -83,6 +83,8 @@ export declare class MapLibreBasemapAdapter {
|
|
|
83
83
|
* map's render pass.
|
|
84
84
|
*/
|
|
85
85
|
setEffects(effects: DeckProps["effects"]): void;
|
|
86
|
+
/** XY snapping (spec: issue #34 Part A) — deck's own pick tolerance, 0 by default; MapboxOverlay forwards the same DeckProps a standalone Deck instance takes. */
|
|
87
|
+
setPickingRadius(pickingRadius: number): void;
|
|
86
88
|
/**
|
|
87
89
|
* Live basemap switch (spec: "Basemap presets & switching"). Deck layers
|
|
88
90
|
* survive BY CONSTRUCTION: the MapboxOverlay is a map control, not style
|
|
@@ -98,9 +100,16 @@ export declare class MapLibreBasemapAdapter {
|
|
|
98
100
|
* remount, since `customAttribution` is fixed at construction.
|
|
99
101
|
*/
|
|
100
102
|
private mountAttribution;
|
|
101
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Toggle map interactions (spec: "Manual Drawing", D4 — doubleClickZoom;
|
|
105
|
+
* issue #34's clip-box gizmo — dragPan) — used to suspend a MapLibre
|
|
106
|
+
* gesture while a tool needs sole ownership of it (double-click-zoom
|
|
107
|
+
* while a draw tool is active; drag-pan while a clip-box face handle is
|
|
108
|
+
* grabbed, or it fights the map's own camera drag for every mousemove).
|
|
109
|
+
*/
|
|
102
110
|
setInteractive(opts: {
|
|
103
111
|
doubleClickZoom?: boolean;
|
|
112
|
+
dragPan?: boolean;
|
|
104
113
|
}): void;
|
|
105
114
|
isLoaded(): boolean;
|
|
106
115
|
getViewport(): ViewportLike;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clip box controller (spec: issue #34 §"Cutting / Clipping" — visible,
|
|
3
|
+
* draggable box). `clip-box.ts` owns the pure shader/patch math; this file
|
|
4
|
+
* owns the DOM-touching half: a wireframe box + 6 face-handle gizmos so the
|
|
5
|
+
* box is something you can SEE and GRAB, not just type numbers into. Same
|
|
6
|
+
* architecture as `measure-controller.ts`'s volume-mode height gizmo (real
|
|
7
|
+
* `<om-layer>` elements driven by `core.patchAnimatedProps` on the per-frame
|
|
8
|
+
* channel) — generalized from that gizmo's one hardcoded vertical axis to 6
|
|
9
|
+
* axis-constrained handles (one per box face), since a face can move along
|
|
10
|
+
* lng, lat, OR elevation depending on which one it is.
|
|
11
|
+
*
|
|
12
|
+
* Owned per `<om-map>` (the draw/measure precedent) and kept alive for as
|
|
13
|
+
* long as a clip box is configured, independent of whether the `clip-box`
|
|
14
|
+
* widget is even mounted — set clip-box-min/max programmatically and the
|
|
15
|
+
* wireframe still shows up, the same way the box itself still clips content
|
|
16
|
+
* with no widget present.
|
|
17
|
+
*/
|
|
18
|
+
import type { PickingInfo } from "@deck.gl/core";
|
|
19
|
+
import type { RuntimeCore, GizmoDragEvent } from "./runtime-core";
|
|
20
|
+
export declare class ClipBoxController {
|
|
21
|
+
private readonly mapEl;
|
|
22
|
+
private readonly core;
|
|
23
|
+
private grab;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the visual box + drag handles are showing. OFF by default and
|
|
26
|
+
* OFF whenever a drag/hover isn't in progress — the box's CLIPPING effect
|
|
27
|
+
* is entirely independent of this (that's driven straight off the
|
|
28
|
+
* clip-box-min/max attributes, always active once set). This is purely
|
|
29
|
+
* about the interactive gizmo: reported that its handles — always
|
|
30
|
+
* pickable once rendered, several of them sitting right around screen
|
|
31
|
+
* center where a normal pan gesture naturally starts — were hijacking
|
|
32
|
+
* ordinary map panning. Gating them behind an explicit "Edit box" toggle
|
|
33
|
+
* (see the clip-box-edit action) means panning is never at risk, and the
|
|
34
|
+
* gizmo only exists on screen when someone actually asked to adjust it.
|
|
35
|
+
*/
|
|
36
|
+
private editing;
|
|
37
|
+
constructor(mapEl: Element, core: RuntimeCore);
|
|
38
|
+
setEditing(active: boolean): void;
|
|
39
|
+
isEditing(): boolean;
|
|
40
|
+
/** Re-reads the current box from `core` and repaints the wireframe + handles — call after ANY change, drag or attribute. */
|
|
41
|
+
syncVisuals(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Repaints the wireframe + handles for a GIVEN box — factored out of
|
|
44
|
+
* `syncVisuals()` so `handleGizmoDrag` can drive this every mousemove
|
|
45
|
+
* frame from its own LOCAL `grab.liveBox` without going anywhere near
|
|
46
|
+
* `core.setClipBox`/DOM attributes (see that method's own doc comment).
|
|
47
|
+
* `patchAnimatedProps` alone is cheap and frame-rate-safe; this is the
|
|
48
|
+
* same reason `measure-controller.ts`'s volume gizmo redraws through
|
|
49
|
+
* `patchAnimatedProps` on every drag frame instead of writing anything
|
|
50
|
+
* to the DOM until the gesture actually ends.
|
|
51
|
+
*/
|
|
52
|
+
private renderBox;
|
|
53
|
+
/**
|
|
54
|
+
* Grab a face handle. Computes a LOCAL screen-space drag basis for this
|
|
55
|
+
* handle's axis by projecting a known small step (`AXIS_EPSILON`) along
|
|
56
|
+
* it — the same linearization trick `measure-controller.ts`'s height
|
|
57
|
+
* gizmo uses for its one hardcoded vertical axis, generalized to a full
|
|
58
|
+
* 2D screen vector so it's correct for lng/lat faces too (which project
|
|
59
|
+
* to some diagonal screen direction depending on the camera's bearing/
|
|
60
|
+
* pitch, not a pure vertical like elevation).
|
|
61
|
+
*/
|
|
62
|
+
handleGizmoDragStart(info: PickingInfo, event: GizmoDragEvent): void;
|
|
63
|
+
/**
|
|
64
|
+
* Every mousemove during a drag updates ONLY `grab.liveBox` and repaints
|
|
65
|
+
* the gizmo via `renderBox` (the cheap `patchAnimatedProps` channel) —
|
|
66
|
+
* it does NOT write clip-box-min/max attributes or call
|
|
67
|
+
* `core.setClipBox` per frame. Those go through `RuntimeCore.applyLayers`,
|
|
68
|
+
* which rebuilds the ENTIRE layer list (every layer, not just this box) —
|
|
69
|
+
* fine for an occasional attribute change, but firing it on every one of
|
|
70
|
+
* a mousemove's ~60 frames/sec froze the whole map (reported: "dragging
|
|
71
|
+
* the gizmos ... the whole map just hangs"). The real content stays
|
|
72
|
+
* clipped to the box's PRE-drag position for the gesture's duration and
|
|
73
|
+
* catches up in ONE `applyLayers` pass at `handleGizmoDragEnd` — the same
|
|
74
|
+
* "cheap live preview, expensive commit once at the end" split the proven
|
|
75
|
+
* volume-measurement gizmo already uses (it never touches the DOM mid-
|
|
76
|
+
* drag either).
|
|
77
|
+
*/
|
|
78
|
+
handleGizmoDrag(info: PickingInfo): void;
|
|
79
|
+
/**
|
|
80
|
+
* The ONE expensive commit for the whole gesture: write the final
|
|
81
|
+
* min/max to the authored attributes — same channel the widget's own
|
|
82
|
+
* number inputs use, so the drag is undoable (history.ts's 500ms
|
|
83
|
+
* coalesce window merges it with any other same-attribute writes into a
|
|
84
|
+
* single step) and reflects live in the widget. The map's own
|
|
85
|
+
* MutationObserver picks this up and calls core.setClipBox, which
|
|
86
|
+
* re-applies the ACTUAL clip to real layers and (via that same observer
|
|
87
|
+
* hook) resyncs this controller's own gizmo from the now-authoritative
|
|
88
|
+
* state.
|
|
89
|
+
*/
|
|
90
|
+
handleGizmoDragEnd(): void;
|
|
91
|
+
private ensureLayers;
|
|
92
|
+
}
|
|
93
|
+
export declare function getClipBoxController(mapEl: Element, core: RuntimeCore): ClipBoxController;
|
|
94
|
+
export declare function peekClipBoxController(mapEl: Element): ClipBoxController | undefined;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clip box (spec: issue #34 §"Cutting / Clipping", tier 2 — the geometric
|
|
3
|
+
* complement to issue #26's clip PLANE). A real oriented... well, AXIS-
|
|
4
|
+
* ALIGNED (v1 scope, confirmed explicitly — rotation is a documented
|
|
5
|
+
* follow-up, not this) 3D box: geometry outside it is discarded (or, in
|
|
6
|
+
* `highlight` mode, dimmed rather than removed — "non-destructive"), so a
|
|
7
|
+
* dense scene (a BIM model, a point cloud, a city block) can be cut open to
|
|
8
|
+
* see what's inside.
|
|
9
|
+
*
|
|
10
|
+
* Map-level scene state (spec: `<om-map clip-box-min="..." clip-box-max="..."
|
|
11
|
+
* clip-box-invert clip-box-highlight>`), same architecture as `lighting`/
|
|
12
|
+
* `terrain`: attribute-backed, undoable, live-editable, applied to every
|
|
13
|
+
* layer by default with a per-layer `clip="off"` escape hatch — see
|
|
14
|
+
* `applyClipBox`'s own doc comment for why that default (not opt-in) is
|
|
15
|
+
* the right one here specifically.
|
|
16
|
+
*
|
|
17
|
+
* Three corrections from the original design review, all reflected below:
|
|
18
|
+
* 1. "Outside the box" is `!inside` (De Morgan's law on the AND of 6
|
|
19
|
+
* half-spaces) — NOT a literal OR of 6 flipped half-space tests, which
|
|
20
|
+
* would (incorrectly) require re-deriving 6 new inequalities; negating
|
|
21
|
+
* the single boolean `inside` result is simpler and exactly equivalent.
|
|
22
|
+
* 2. Plane/box bounds are expressed in deck's COMMON SPACE (via
|
|
23
|
+
* `Layer#projectPosition`, the SAME mechanism the built-in `ClipExtension`
|
|
24
|
+
* uses for its own 2D bounds) — never raw lng/lat — so one authored box
|
|
25
|
+
* clips layers in different coordinate systems (geographic, meter-offset
|
|
26
|
+
* BIM placements, etc.) correctly.
|
|
27
|
+
* 3. Picking (including the `pickable:'3d'` depth pass) is covered for
|
|
28
|
+
* free: `discard` inside `fs:DECKGL_FILTER_COLOR` is the SAME shader
|
|
29
|
+
* stage deck's own picking pass reuses — no separate picking-specific
|
|
30
|
+
* code needed, confirmed by the built-in `ClipExtension` doing nothing
|
|
31
|
+
* extra for picking either.
|
|
32
|
+
*/
|
|
33
|
+
import { LayerExtension } from "@deck.gl/core";
|
|
34
|
+
import type { Layer } from "@deck.gl/core";
|
|
35
|
+
import type { LayerIR } from "./ir";
|
|
36
|
+
export interface ClipBoxIR {
|
|
37
|
+
/** [lng, lat, elevationMeters] — one corner. Order vs. `max` doesn't matter; both get componentwise min/max'd after projecting. */
|
|
38
|
+
min: [number, number, number];
|
|
39
|
+
max: [number, number, number];
|
|
40
|
+
/** Show OUTSIDE the box instead of inside. */
|
|
41
|
+
invert: boolean;
|
|
42
|
+
/** Dim clipped-out geometry instead of discarding it — "non-destructive": nothing disappears, it just de-emphasizes. */
|
|
43
|
+
highlight: boolean;
|
|
44
|
+
}
|
|
45
|
+
/** `<om-map clip-box-min="[...]" clip-box-max="[...]" clip-box-invert clip-box-highlight>` → `ClipBoxIR`, or `null` when min/max aren't both present/valid (no box = no clipping, not a broken one). */
|
|
46
|
+
export declare function parseClipBoxAttrs(getAttr: (name: string) => string | null): ClipBoxIR | null;
|
|
47
|
+
export interface ClipBoxExtensionProps {
|
|
48
|
+
clipBoxMin?: [number, number, number];
|
|
49
|
+
clipBoxMax?: [number, number, number];
|
|
50
|
+
clipBoxInvert?: boolean;
|
|
51
|
+
clipBoxHighlight?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** Fragment-level box clip (targets extruded/mesh geometry — GeoJsonLayer extrusions, Tile3DLayer/BIMLayer triangles — not point/icon anchors, unlike the built-in ClipExtension's dual vertex/fragment modes; this library's clip-box use case is "cut into a 3D scene," not "hide points outside a rect"). */
|
|
54
|
+
export declare class ClipBoxExtension extends LayerExtension {
|
|
55
|
+
static extensionName: string;
|
|
56
|
+
static defaultProps: {
|
|
57
|
+
clipBoxMin: number[];
|
|
58
|
+
clipBoxMax: number[];
|
|
59
|
+
clipBoxInvert: boolean;
|
|
60
|
+
clipBoxHighlight: boolean;
|
|
61
|
+
};
|
|
62
|
+
getShaders(): {
|
|
63
|
+
modules: {
|
|
64
|
+
readonly name: "clipBox";
|
|
65
|
+
readonly fs: "\nlayout(std140) uniform clipBoxUniforms {\n vec3 boxMin;\n vec3 boxMax;\n float invert;\n float highlight;\n vec4 highlightTint;\n} clipBox;\n\nbool clipBox_isInside(vec3 commonPos) {\n return commonPos.x >= clipBox.boxMin.x && commonPos.x <= clipBox.boxMax.x\n && commonPos.y >= clipBox.boxMin.y && commonPos.y <= clipBox.boxMax.y\n && commonPos.z >= clipBox.boxMin.z && commonPos.z <= clipBox.boxMax.z;\n}\n";
|
|
66
|
+
readonly uniformTypes: {
|
|
67
|
+
readonly boxMin: "vec3<f32>";
|
|
68
|
+
readonly boxMax: "vec3<f32>";
|
|
69
|
+
readonly invert: "f32";
|
|
70
|
+
readonly highlight: "f32";
|
|
71
|
+
readonly highlightTint: "vec4<f32>";
|
|
72
|
+
};
|
|
73
|
+
}[];
|
|
74
|
+
inject: {
|
|
75
|
+
"vs:#decl": string;
|
|
76
|
+
"vs:DECKGL_FILTER_GL_POSITION": string;
|
|
77
|
+
"fs:#decl": string;
|
|
78
|
+
"fs:DECKGL_FILTER_COLOR": string;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
draw(this: Layer<Required<ClipBoxExtensionProps>>): void;
|
|
82
|
+
}
|
|
83
|
+
export interface ClipBoxLayerPatch {
|
|
84
|
+
/**
|
|
85
|
+
* The deck-layer id to construct with. Suffixed `#c<generation>` while a
|
|
86
|
+
* box is active and this layer is opted in: adding/removing an extension
|
|
87
|
+
* on a LIVE (already-mounted) layer corrupts DataFilterExtension's
|
|
88
|
+
* attribute state for some layer types (ColumnLayer, Tile3DLayer/BIMLayer
|
|
89
|
+
* sublayers — empirically verified: a box present from the FIRST render
|
|
90
|
+
* works fine, but toggling one on live on an already-mounted filtered
|
|
91
|
+
* layer silently blanks it, with no console error) — the exact same
|
|
92
|
+
* "birth-stable extension set" issue `applyTerrain`'s own `deckId`
|
|
93
|
+
* suffixing already works around. A fresh id forces clean teardown +
|
|
94
|
+
* rebuild instead of a live prop update.
|
|
95
|
+
*/
|
|
96
|
+
deckId: string;
|
|
97
|
+
extraProps: Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Per-layer clip-box patches, PURE — mirrors `applyTerrain`'s exact shape
|
|
101
|
+
* (same file this gets merged alongside in runtime-core.ts's `buildLayers`).
|
|
102
|
+
* Default is APPLIED (every layer gets clipped once a box is configured),
|
|
103
|
+
* unlike terrain's per-layer OPT-IN default — the clip box's whole point is
|
|
104
|
+
* cutting into "the scene," and requiring every single layer to opt in
|
|
105
|
+
* individually would make the common case ("cut through everything, show me
|
|
106
|
+
* what's inside") the awkward one; `clip="off"` is the escape hatch for the
|
|
107
|
+
* layers that shouldn't be touched (a basemap, most often).
|
|
108
|
+
*/
|
|
109
|
+
export declare function applyClipBox(irs: readonly LayerIR[], clipBox: ClipBoxIR | null, generation: number): Map<string, ClipBoxLayerPatch>;
|