@nika-js/onlymap 0.4.4 → 0.5.0
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/README.md +10 -7
- package/THIRD-PARTY-LICENSES.md +2 -2
- package/bin/onlymapjs.mjs +14 -1
- package/dist/{LercDecode.es-CgN9Gb9e.js → LercDecode.es-B-OFS9hR.js} +1 -1
- package/dist/{basemap-Bfn5Z__c.js → basemap-CXVNH22A.js} +3 -3
- package/dist/cityjson-urQeujQv.js +407 -0
- package/dist/cityjson.d.ts +138 -0
- package/dist/data-layer.d.ts +10 -0
- package/dist/elements/om-map.d.ts +70 -0
- package/dist/{index-lXrP3rPo.js → index-BonJP_St.js} +1 -1
- package/dist/index-CW1n5LdO.js +4006 -0
- package/dist/{index-CnitG1VX.js → index-CjZMcGou.js} +2 -2
- package/dist/{index-M8KfTTol.js → index-DE7T4lfa.js} +1 -1
- package/dist/{index-KgO0MBqA.js → index-DiYFebYs.js} +11514 -11259
- package/dist/{index-C4cWRigY.js → index-DuY3Zn6r.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/layer-registry.d.ts +22 -0
- package/dist/{lerc-l-QFh62d.js → lerc-BhtFlCQ3.js} +2 -2
- package/dist/license.d.ts +34 -8
- package/dist/onlymap.standalone.js +23608 -22941
- package/dist/onlymapjs.js +69 -65
- package/dist/programmatic.d.ts +4 -0
- package/dist/quota-notice.d.ts +43 -0
- package/dist/raster-BpVAEzbU.js +4842 -0
- package/dist/react.js +162 -161
- package/dist/runtime-core.d.ts +9 -0
- package/dist/testing.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/widget-layout.d.ts +20 -0
- package/docs/3d-assets.md +99 -1
- package/docs/testing.md +4 -2
- package/llms.txt +2 -2
- package/onlymapjs.html-data.json +12 -8
- package/package.json +3 -2
- package/skills/onlymapjs/SKILL.md +4 -2
- package/skills/onlymapjs/references/patterns.md +4 -0
- package/skills/onlymapjs/references/syntax.md +70 -17
- package/skills/onlymapjs/references/testing.md +2 -1
- package/dist/raster-BBA_oI-d.js +0 -8843
package/docs/3d-assets.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
OnlyMapJS renders 3D models **in geographic context**: buildings, equipment, vehicles — anything you can place at a longitude/latitude. The architecture rule that makes this simple is the same one the library applies to data everywhere:
|
|
4
4
|
|
|
5
5
|
> **Domain 3D formats stay upstream; the manifest ingests web standards.**
|
|
6
|
-
> IFC, CAD, mesh formats → convert on your server to GLB. GLB (binary glTF)
|
|
6
|
+
> IFC, CAD, mesh formats → convert on your server to GLB. GLB (binary glTF), 3D Tiles and CityJSON → the manifest renders directly. (2D vector formats — GeoJSON, CSV, Shapefile, KML, GeoArrow — load directly too; see the README's data-formats table.)
|
|
7
7
|
|
|
8
8
|
## The manifest half (this library)
|
|
9
9
|
|
|
@@ -69,6 +69,104 @@ For LOD experiments, common loaders.gl tileset options have first-class attribut
|
|
|
69
69
|
|
|
70
70
|
Upstream converters exist for IFC/CityGML → 3D Tiles (e.g. Cesium ion, `py3dtiles`, FME). Same rule: convert upstream, ingest the standard.
|
|
71
71
|
|
|
72
|
+
## Semantic city models: CityJSON
|
|
73
|
+
|
|
74
|
+
3D Tiles is the right target for *visual* city models. It is the wrong one for **CityJSON**, because converting flattens the per-building semantics — and semantics are the whole reason municipal programmes publish it: the Netherlands' [3DBAG](https://3dbag.nl) (~10M buildings), Japan's [PLATEAU](https://www.mlit.go.jp/plateau/) (250+ cities), swisstopo, several German states. Rooftop-solar, shadow, zoning and noise studies all read those attributes.
|
|
75
|
+
|
|
76
|
+
So CityJSON is ingested natively — point `data` at it, no converter, in either of two rendering modes:
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<om-map center="[4.3679, 52.0029]" zoom="18" pitch="60" basemap="positron" lighting="daylight">
|
|
80
|
+
<!-- Real per-surface geometry: one row per FACE, each at its own true
|
|
81
|
+
height — a pitched LoD2.2 roof actually looks pitched. NO
|
|
82
|
+
get-fill-color needed: it defaults to a ninja-viewer-style palette
|
|
83
|
+
the decoder derives automatically (see "Default coloring" below). -->
|
|
84
|
+
<om-layer id="roofs" type="SolidPolygonLayer"
|
|
85
|
+
data="./9-284-556.city.json?om-surfaces=1"
|
|
86
|
+
get-polygon="$polygon" full3d
|
|
87
|
+
pickable></om-layer>
|
|
88
|
+
<!-- This mode is unlit, so face edges need a companion PathLayer tracing
|
|
89
|
+
`outline` (see "Visible edges in surfaces mode" below) — SolidPolygonLayer's
|
|
90
|
+
own `wireframe` prop is a no-op here. -->
|
|
91
|
+
<om-layer id="roof-outlines" type="PathLayer"
|
|
92
|
+
data="./9-284-556.city.json?om-surfaces=1"
|
|
93
|
+
get-path="$outline" get-color="[0, 0, 0]" width-min-pixels="1"
|
|
94
|
+
pickable="false"></om-layer>
|
|
95
|
+
<om-widget type="legend" position="bottom-right"></om-widget>
|
|
96
|
+
</om-map>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<om-map center="[4.3679, 52.0029]" zoom="18" pitch="60" basemap="positron" lighting="daylight">
|
|
101
|
+
<!-- Footprint extrusion: one row per BUILDING, flat-topped at a single
|
|
102
|
+
derived height. Simpler, lit, terrain-aware. -->
|
|
103
|
+
<om-layer id="buildings" type="GeoJsonLayer"
|
|
104
|
+
data="./9-284-556.city.json"
|
|
105
|
+
extruded get-elevation="$roof_height"
|
|
106
|
+
get-fill-color="$b3_dak_type == 'slanted' ? '#d6604d' : '#4393c3'"
|
|
107
|
+
filter-field="roof_height" filter-range="[0, 30]"
|
|
108
|
+
pickable></om-layer>
|
|
109
|
+
<om-widget type="legend" position="bottom-right"></om-widget>
|
|
110
|
+
</om-map>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Runnable, both modes toggled side by side: [`dev/examples/cityjson.html`](../dev/examples/cityjson.html).
|
|
114
|
+
|
|
115
|
+
### What you get
|
|
116
|
+
|
|
117
|
+
CityJSON stores a building as a soup of 3D surfaces — roof planes, walls, ground — each optionally tagged with a semantic type. That soup decodes to one of two shapes, chosen by the `data` URL:
|
|
118
|
+
|
|
119
|
+
- **Default — one GeoJSON `Polygon` per CityObject.** A 2D ground footprint plus heights measured off the real 3D geometry, rendered through `GeoJsonLayer`'s `extruded` + `get-elevation`: lit, pickable, GPU-filterable and terrain-aware. The tradeoff: extrusion is a flat-topped prism, so no matter how accurate the derived height is, this mode can never draw the actual shape of a pitched or hipped LoD2.2 roof.
|
|
120
|
+
- **`?om-surfaces=1` — one flat row per FACE**, for `SolidPolygonLayer` with `get-polygon="$polygon"` and `full3d`. `extruded` stays at its ordinary `false` default — no need to author it — since the height already lives in the geometry, not in an extrusion accessor. `polygon` carries that face's real rings (outer + holes) with the ORIGINAL per-vertex height kept as-is — not flattened to one number — so a sloped roof plane renders sloped and a near-vertical wall renders vertical. `full3d` IS required (deck's default is `false`): without it, earcut works in the flat xy plane, where a vertical polygon's projected area is ~zero and the face silently vanishes. The tradeoff: deck's solid-polygon vertex shader computes lighting only inside its `extruded` branch, so this mode is unavoidably flat-shaded — no shading from surface orientation. Each row also carries `surface_type` (`"RoofSurface"` / `"WallSurface"` / `"GroundSurface"` / undefined) for per-surface styling, `fill_color` (see below), `cityobject_id` to correlate faces back to their building, and the SAME derived metrics below as the footprint mode.
|
|
121
|
+
|
|
122
|
+
### Default coloring (surfaces mode)
|
|
123
|
+
|
|
124
|
+
A face needs no authored `get-fill-color` at all to look reasonable: `SolidPolygonLayer`'s curated schema defaults that accessor to a reserved `fill_color` field, and surfaces-mode rows populate it automatically — RoofSurface red, WallSurface white, Building blue, Water light blue, and so on. The palette is verified against the actual default colors [`cityjson-threejs-loader`](https://github.com/cityjson/cityjson-threejs-loader) (the rendering engine behind the [ninja](https://ninja.cityjson.org) reference viewer) ships, not guessed: its `defaults/colors.js` module's `defaultSemanticsColors` (keyed by semantic surface type — `RoofSurface`, `WallSurface`, `Window`, `Door`, …) wins when a face's `surface_type` is one of those; otherwise `defaultObjectColors` (keyed by CityObject type — `Building`, `Bridge`, `Railway`, `WaterBody`, `TINRelief`, …) covers object types with no semantic tagging at all; a neutral gray is the last resort. Author `get-fill-color` yourself to override it — an authored accessor always wins over the default, same as any other layer.
|
|
125
|
+
|
|
126
|
+
This convention (a `defaultExpr` on the `get-fill-color` `PropDescriptor`, resolved only when the attribute is absent) is format-agnostic, not CityJSON-specific: any data source that populates a row's `fill_color` field gets the same free default on `SolidPolygonLayer`. The plain `color="…"` shorthand overrides it too — anything you actually author beats the default, whichever way you write it.
|
|
127
|
+
|
|
128
|
+
### Row budget (surfaces mode)
|
|
129
|
+
|
|
130
|
+
Surfaces mode multiplies the row count by however many faces a building has: the 3DBAG tile in `dev/examples/cityjson.html` decodes to 120 rows as footprints and **3,940 rows** as surfaces — roughly 33×. That lands against the free tier's 25,000-row cap (see [Monetization Gates](../agent-map-library-architecture.md)) at around 750 buildings, where the footprint mode would still be nowhere near it. Past the cap the layer renders its first 25,000 rows — an arbitrary subset in source order, with a dismissible on-map notice — rather than going blank, so a slightly-too-big scene still draws. Because faces are emitted per building, the cut lands mid-building. To show a whole scene rather than part of one: pin a lower LoD with `?om-lod=` (1.3 is ~15× instead of ~33×), tile the source, or extrude footprints instead.
|
|
131
|
+
|
|
132
|
+
Face counts are long-tailed, so the building count you can fit is not predictable from the average: in that same tile the median building is 17 faces but the largest is 963 — one building, 24% of the tile's rows.
|
|
133
|
+
|
|
134
|
+
### Visible edges in surfaces mode
|
|
135
|
+
|
|
136
|
+
Surfaces mode is flat-shaded, so adjacent faces at similar heights (e.g. a LoD2.2 hip roof's planes) can be hard to tell apart by color alone. Each row carries an `outline` field — the same face's outer ring, flattened and closed — for exactly this: give a `PathLayer` `get-path="$outline"` and it traces real per-face edges. Always pair one with a surfaces-mode `SolidPolygonLayer`; deck's `wireframe` prop on `SolidPolygonLayer` only builds wireframe geometry when `extruded: true`, so it does nothing in this unextruded mode. Point the `PathLayer` at the same `data` URL (the cache is keyed by URL, so this doesn't cost a second fetch) and mirror the fill layer's `filter-field`/`filter-range` so filtered-out buildings' outlines disappear too.
|
|
137
|
+
|
|
138
|
+
The semantics still do work in both modes: they decide which surfaces count as roof, so these are real roof measurements rather than bounding-box numbers. Derived names win over a same-named source attribute, so manifests can rely on them.
|
|
139
|
+
|
|
140
|
+
| Property | Meaning |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `roof_height` | **Area-weighted mean roof height above ground** — bind `get-elevation` to this in footprint mode |
|
|
143
|
+
| `eaves_height` / `ridge_height` | Lowest / highest roof point above ground |
|
|
144
|
+
| `ground_height` | Absolute elevation of the lowest point, in the file's vertical datum |
|
|
145
|
+
| `roof_area` | True 3D roof area in m² (not the footprint's) |
|
|
146
|
+
| `surface_count`, `lod` | Source geometry size, and which LoD was used (e.g. `"2.2"`) |
|
|
147
|
+
| `cityobject_id`, `cityobject_type`, `parent_id` | Identity and hierarchy |
|
|
148
|
+
| `polygon` *(surfaces mode only)* | This face's rings, `[[lng, lat, elevation], …]` per ring — bind `get-polygon` to this |
|
|
149
|
+
| `outline` *(surfaces mode only)* | This face's outer ring only, flattened and closed — bind a `PathLayer`'s `get-path` to this for visible edges (see above) |
|
|
150
|
+
| `surface_type` *(surfaces mode only)* | `"RoofSurface"` / `"WallSurface"` / `"GroundSurface"` / undefined, per face |
|
|
151
|
+
| `fill_color` *(surfaces mode only)* | Default per-face color (ninja-viewer palette) — `get-fill-color` reads this automatically when unauthored |
|
|
152
|
+
|
|
153
|
+
`roof_height` is area-weighted because a pitched-roof mesh carries many vertices along its eaves edge — an unweighted median measures ~1–2 m low, which renders as visibly squat buildings.
|
|
154
|
+
|
|
155
|
+
**Parents and parts:** geometry usually lives on `BuildingPart` children while attributes live on the parent `Building`. Rows are emitted for whatever carries geometry, with the parent's attributes inherited and `parent_id` kept — so `$oorspronkelijkbouwjaar` works on the feature (or face) you clicked.
|
|
156
|
+
|
|
157
|
+
### LoD, streaming, and coordinate systems
|
|
158
|
+
|
|
159
|
+
- **LoD** — the highest available is used (better footprints, real roof geometry to measure). Pin one with `data="./tile.city.json?om-lod=1.2"`; different values cache independently.
|
|
160
|
+
- **Surfaces mode** — `data="./tile.city.json?om-surfaces=1"` switches to the per-face `SolidPolygonLayer` rows above; combine with a LoD pin as `?om-lod=1.2&om-surfaces=1`. Like `om-lod`, it's a separate cache entry — one manifest can point a `GeoJsonLayer` at the plain URL and a `SolidPolygonLayer` at the `?om-surfaces=1` counterpart of the SAME file.
|
|
161
|
+
- **CityJSONSeq** — `.city.jsonl` is one JSON object per line (header, then a feature per line). Buildings appear *as they download*; nothing else changes, and both output modes stream. Recognized: `.city.json`, `.cityjson`, `.city.jsonl`, `.cityjsonl`, `.jsonl`, `.ndjson`, and the `application/city+json` content type. A `.jsonl` whose first line isn't a CityJSON header fails explicitly — claim it with `OmMap.registerFormat()` if it's some other newline-delimited format.
|
|
162
|
+
- **Reprojection** to lon/lat is automatic for the national grids these datasets ship in: **28992** and compound **7415** (Netherlands), **2056** (Switzerland), **25832**/**25833** and compound **5555**/**5556** (Germany), **6668**/**6697** and the plane-rectangular zones **6669–6687** (Japan), **31254**/**31255**/**31256** (Austria GK West/Central/East), **3414** (Singapore SVY21), plus 4326/3857. Axis order is handled per system — Japan's grids are northing-first and JGD2011 geographic is latitude-first, and CityJSON stores vertices in the CRS's own order. Any other code fails with an error naming it rather than placing the city silently in the wrong country; reproject upstream with `cjio input.city.json reproject 4326 save output.city.json`.
|
|
163
|
+
|
|
164
|
+
The decoder and proj4 are a lazy chunk — maps that never load CityJSON pay nothing, and proj4 is skipped entirely for files already in lon/lat.
|
|
165
|
+
|
|
166
|
+
### Not supported yet
|
|
167
|
+
|
|
168
|
+
Textures/materials (`appearance`) are ignored regardless of mode. `GeometryInstance` and point/line-only CityObjects produce no rows. CityJSON Extensions pass through as ordinary attributes without interpretation. Surfaces mode does not merge adjacent faces into one lit mesh — a normal-based lighting extension over the per-face geometry (so surfaces mode could be lit too) is a possible follow-on, not implemented.
|
|
169
|
+
|
|
72
170
|
## Scope boundary, stated honestly
|
|
73
171
|
|
|
74
172
|
OnlyMapJS is for assets **in geographic context** — models on a map, camera pitching down at the world. It is not a model *inspector*: orbiting freely around a single non-geo-referenced model (the classic three.js/CAD-viewer use case) is a different product with a different camera. If your models have no real-world coordinates and never will, a plain glTF viewer is the right tool; the moment they belong somewhere on Earth, this pipeline is.
|
package/docs/testing.md
CHANGED
|
@@ -111,7 +111,7 @@ it("panning away empties viewport-scoped widgets", async () => {
|
|
|
111
111
|
});
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
The harness API: `pick` (synthetic picks fed through the exact code path real deck.gl picks take — columnar layers pick object-less by index, exactly like live), `clearSelection` (hover-off; runs the tooltip auto-hide path), `emit` (any action, same payload contract as `ctx.emit`/`data-emit`), `setView`, `layers()` (the live IR), `flush`, `unmount`. Every verb settles the library's internal batching before resolving — **you never write a sleep**.
|
|
114
|
+
The harness API: `pick` (synthetic picks fed through the exact code path real deck.gl picks take — columnar layers pick object-less by index, exactly like live), `clearSelection` (hover-off; runs the tooltip auto-hide path), `mapPoint` (a click/hover map coordinate through the real onMapPoint path — fires `om-map-point`, drives the draw controller), `emit` (any action, same payload contract as `ctx.emit`/`data-emit`), `setView`, `layers()` (the live IR), `flush`, `unmount`. Every verb settles the library's internal batching before resolving — **you never write a sleep**.
|
|
115
115
|
|
|
116
116
|
**Remote data:** mock `fetch` and the harness waits for it via the readiness signal:
|
|
117
117
|
|
|
@@ -137,7 +137,9 @@ npx playwright install chromium
|
|
|
137
137
|
npx onlymapjs check-layout public/dashboard.html
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
-
`check-layout` serves the manifest over loopback HTTP (so relative data URLs work), executes it in an isolated headless Chromium context, opens responsive drawers, and checks real geometry/hit-testing at 360, 640, 768, and 1024px. It prints one JSON diagnostic per problem and exits 0 when clean or 1 on layout errors. Because it executes the page, only run it on manifests you trust.
|
|
140
|
+
`check-layout` serves the manifest over loopback HTTP (so relative data URLs work), executes it in an isolated headless Chromium context, opens responsive drawers, and checks real geometry/hit-testing at 360, 640, 768, and 1024px. It prints one JSON diagnostic per problem and exits 0 when clean or 1 on layout errors. Because it executes the page, only run it on manifests you trust. Point it at a **browser-runnable** manifest — one that imports the built library (`@nika-js/onlymap`) — not a framework dev-server page that relies on on-the-fly transpilation (a page importing raw `.ts` will fail to render a canvas, and the tool reports exactly that).
|
|
141
|
+
|
|
142
|
+
To run the same audit inside your own Playwright suite instead of the CLI, import it: `auditLayout(page, { widths })` returns the `LayoutDiagnostic[]` (the CLI is a thin wrapper over it), and `settleLayout(page)` awaits the render/fonts/attribution settle contract. Both come from `@nika-js/onlymap`.
|
|
141
143
|
|
|
142
144
|
1. **`await mapEl.ready`** — resolves when the renderer initialized *and* the first reconcile ran *and* every declared `data` URL settled. Never `waitForTimeout`.
|
|
143
145
|
2. **`mapEl.projectInternal([lng, lat])`** — derive click/hover pixels from the map's own projection instead of hardcoding coordinates.
|
package/llms.txt
CHANGED
|
@@ -24,8 +24,8 @@ OnlyMapJS is NOT raw deck.gl and NOT generic HTML/JSX. The rules below are the d
|
|
|
24
24
|
|
|
25
25
|
## Element vocabulary
|
|
26
26
|
|
|
27
|
-
- `<om-map center="[lng, lat]" zoom="11" pitch="55" bearing="20" basemap="positron">` — the root. `basemap` accepts a free preset (`liberty`, `bright`, `positron`, `dark-matter`, `voyager`, `osm` — no keys; or `maptiler-streets`/`maptiler-dataviz`/`maptiler-satellite` with `basemap-key="…"` or `OmMap.configureBasemap({ maptilerKey })`), `maplibre` (bare demo style), a style URL (e.g. a MapTiler-customized style; any scheme fetch supports, including desktop asset protocols like Tauri's, query strings fine), or `none` (standalone canvas). The attribute is LIVE — writing it switches the basemap in place (camera + layers survive); the `set-basemap {basemap}` action and `<om-widget type="basemap-switcher" options="positron dark-matter osm">` do the same. Register more via `OmMap.registerBasemap(name, { style })`. Required provider attribution renders automatically (`attribution="false"` opts out). `pitch`/`bearing` tilt the initial camera (use for 3D content). Terrain: `terrain="terrarium"` (keyless AWS DEM; also `maptiler-terrain` with a key, a raw `{z}/{x}/{y}` DEM URL + `terrain-decoder="terrarium|mapbox-rgb|<JSON>"`, or `off`) raises a 3D surface — geographic layers DRAPE onto it automatically (per-layer `terrain="drape|offset|off"` overrides; 3D-model layers sit ON it), `terrain-exaggeration` scales relief, `terrain-texture` drapes imagery; terrain REPLACES an active basemap while on (restored when off); `set-terrain` action, `terrain` watch token, `OmMap.registerTerrain(name, {...})` for more sources. Scene lighting for 3D content: `lighting="daylight|studio|flat|custom"` (absent = deck defaults) with `lighting-ambient`/`lighting-sun`/`lighting-sun-azimuth`/`lighting-sun-elevation`/`lighting-camera` overrides and `lighting-sun-date` (ISO/epoch — solar-position sun computed at the map center, wins over azimuth/elevation); attribute-backed (undoable, live-editable), story-steppable via the `set-lighting {lighting, sunAzimuth, ...}` action (`lighting="default"` removes the attribute set; a bare preset is a clean reset — stale overrides clear), native UI via `<om-widget type="lighting">` (preset radios + tuning sliders), widget watch token `lighting`. GeoJsonLayer extrudes polygons declaratively: `extruded get-elevation="$height"` (+ `wireframe`). `widgets-hidden` attribute (or the `set-widgets-visible {visible}` action / `<om-widget type="widgets-toggle">` button) hides every widget WITHOUT destroying state — attribution never hides (license); transient (not an undo step) but story-steppable, so a step can clear chrome for a cinematic take. Slots auto-dim while an open `<om-overlay>` popup covers them (position stability over the popup dodging; `widgets-dim="off"` disables). `validate` attribute enables live validation + on-page error panel. Include a `map-id="<random UUID>"` on complete pages (identifies the map artifact for usage telemetry — not a visitor id; the page author deletes it to opt out); `telemetry="off"` disables usage telemetry + library-error reporting for the map (global: `OmMap.configureTelemetry({ disabled: true })`; schema: docs/telemetry.md). Free plan limits: 5 layers, 25k rows per layer — layers past a limit don't render and validation says why; a `license-key="om_live_…"` attribute (publishable, origin-restricted) or `OmMap.configureLicense(key)` lifts limits and removes the corner badge. When a user's map exceeds free limits, say so and point at the validation message rather than silently restructuring their data. Events on the element: `om-map-ready` (boot complete; `await mapEl.ready` is the promise twin), `om-validation-error`, `om-view-changed` (camera settled after a move, debounced; `detail = {longitude, latitude, zoom, pitch, bearing, origin}` where `origin` is `"user"` for gesture bursts vs `"programmatic"` for API/story moves — the camera-persistence hook, and the echo-suppression signal when syncing camera state to an app store). `await mapEl.snapshot()` returns a canvas-only PNG dataURL of the scene (basemap + layers at device pixels; `{as:"blob"}` for files) — DOM widgets/overlays/attribution are NOT captured, so exports must render provider credits themselves.
|
|
28
|
-
- `<om-layer id="..." type="ScatterplotLayer" data="./points.json">` — any deck.gl layer class by `type` (all 33 bundled, plus the native `COGLayer` raster type), plus `PopupLayer` (WebGL badges/labels at scale: `layout="badge|pin-label|card"`, `min-zoom`/`max-zoom`). Every type's full attribute list ships in the package's `onlymapjs.html-data.json` — consult it instead of guessing attribute names or reading the minified dist. TextLayer's default font atlas covers ASCII only: set `character-set` when label text carries other glyphs (`—`, `·`, accents) or deck warns and renders them blank. External layer classes register via `OmMap.registerLayer({type, deckClass, props})` — build them on `@nika-js/onlymap/deck` (the bundled `CompositeLayer`/`TileLayer`/… re-exports), never a separately-installed deck.gl (different class hierarchy, breaks in the renderer); function-valued props ride the subclass's `static defaultProps`; register at module top level BEFORE the manifest mounts (see docs/custom-layers.md). Data: `data` URL (JSON, GeoJSON, CSV/TSV `.csv` — parsed to typed columns — Shapefile `.shp` (+`.dbf` attributes) and KML `.kml` as GeoJSON features, or Arrow IPC `.arrow`/`.feather` — large point datasets stay columnar, GeoArrow line/polygon geometry becomes GeoJSON features, zstd-compressed IPC is handled; other formats plug in via `OmMap.registerFormat({match, parse})`; data URLs accept any scheme the runtime's fetch supports — desktop webviews (Tauri, Electron) pass asset-protocol URLs straight in), inline `<script type="application/json">` (row arrays or column-oriented `{"columns": {"lon": [...], "lat": [...]}}`; must be a DIRECT child of the `<om-layer>`, and when present it wins — omit the `data` attribute), or `wss://` streaming (`key="mmsi"` upserts entities in place, `flush="250ms"` coalesces bursts, `source="name"` selects a `OmMap.registerSource` decoder plugin), or a polled REST snapshot (`refresh="5s"` re-fetches and replaces — for live endpoints that return the full current state). Authenticated endpoints: call `OmMap.configureData({ headers: {...} })` in a script — never put tokens in attributes. `$field` accessors work identically on all of them — never write column-index code yourself. One columnar restriction: the `js` full-JS opt-in is not allowed on Arrow/columnar layers (validation will tell you; use `$field` accessors instead). For 3D models use `type="ScenegraphLayer"` with `scenegraph="./model.glb"` (required) and `get-orientation="[0, $heading, 90]"` — the roll of 90 stands Y-up glTF models upright; see docs/3d-assets.md. GeoTIFF/COG rasters use `type="COGLayer"` with `src="./dem.tif"` (NOT `data` — rasters stream tiles by Range request, they are not parsed rows): `min`/`max` set the rescale window (default 0–255; ALWAYS set them for float/16-bit data like DEMs), `colormap` picks a bundled ramp for single-band sources (gray, viridis, plasma, inferno, magma, cividis, rdylgn, rdbu, spectral, terrain, jet, turbo), `nodata` overrides the source sentinel (renders transparent); plain 8-bit RGB COGs need no styling attributes; restretch/recolor are GPU uniforms (no refetch) and the legend ramp derives from colormap+min/max automatically. Sources must be Cloud-Optimized (`gdal_translate -of COG` otherwise).
|
|
27
|
+
- `<om-map center="[lng, lat]" zoom="11" pitch="55" bearing="20" basemap="positron">` — the root. Give it a height (`om-map { display:block; height:100vh }` with `html,body{height:100%}`, or a sized container) — a custom element is display:inline by default and collapses to zero size; with none set, the library falls back to display:block + a 400px floor so a bare map still shows (any height you DO set wins over the floor, including one below 400px), and a collapsed map logs a console warning. A map hidden on purpose (`hidden`, or inside a `display:none` panel) stays hidden and does not warn. `basemap` accepts a free preset (`liberty`, `bright`, `positron`, `dark-matter`, `voyager`, `osm` — no keys; or `maptiler-streets`/`maptiler-dataviz`/`maptiler-satellite` with `basemap-key="…"` or `OmMap.configureBasemap({ maptilerKey })`), `maplibre` (bare demo style), a style URL (e.g. a MapTiler-customized style; any scheme fetch supports, including desktop asset protocols like Tauri's, query strings fine), or `none` (standalone canvas). The attribute is LIVE — writing it switches the basemap in place (camera + layers survive); the `set-basemap {basemap}` action and `<om-widget type="basemap-switcher" options="positron dark-matter osm">` do the same. Register more via `OmMap.registerBasemap(name, { style })`. Required provider attribution renders automatically (`attribution="false"` opts out). `pitch`/`bearing` tilt the initial camera (use for 3D content). Terrain: `terrain="terrarium"` (keyless AWS DEM; also `maptiler-terrain` with a key, a raw `{z}/{x}/{y}` DEM URL + `terrain-decoder="terrarium|mapbox-rgb|<JSON>"`, or `off`) raises a 3D surface — geographic layers DRAPE onto it automatically (per-layer `terrain="drape|offset|off"` overrides; 3D-model layers sit ON it), `terrain-exaggeration` scales relief, `terrain-texture` drapes imagery; terrain REPLACES an active basemap while on (restored when off); `set-terrain` action, `terrain` watch token, `OmMap.registerTerrain(name, {...})` for more sources. Scene lighting for 3D content: `lighting="daylight|studio|flat|custom"` (absent = deck defaults) with `lighting-ambient`/`lighting-sun`/`lighting-sun-azimuth`/`lighting-sun-elevation`/`lighting-camera` overrides and `lighting-sun-date` (ISO/epoch — solar-position sun computed at the map center, wins over azimuth/elevation); attribute-backed (undoable, live-editable), story-steppable via the `set-lighting {lighting, sunAzimuth, ...}` action (`lighting="default"` removes the attribute set; a bare preset is a clean reset — stale overrides clear), native UI via `<om-widget type="lighting">` (preset radios + tuning sliders), widget watch token `lighting`. GeoJsonLayer extrudes polygons declaratively: `extruded get-elevation="$height"` (+ `wireframe`). `widgets-hidden` attribute (or the `set-widgets-visible {visible}` action / `<om-widget type="widgets-toggle">` button) hides every widget WITHOUT destroying state — attribution never hides (license); transient (not an undo step) but story-steppable, so a step can clear chrome for a cinematic take. Slots auto-dim while an open `<om-overlay>` popup covers them (position stability over the popup dodging; `widgets-dim="off"` disables). `validate` attribute enables live validation + on-page error panel. Include a `map-id="<random UUID>"` on complete pages (identifies the map artifact for usage telemetry — not a visitor id; the page author deletes it to opt out); `telemetry="off"` disables usage telemetry + library-error reporting for the map (global: `OmMap.configureTelemetry({ disabled: true })`; schema: docs/telemetry.md). Free plan limits: 5 layers, 25k rows per layer — layers past a limit don't render and validation says why; a `license-key="om_live_…"` attribute (publishable, origin-restricted) or `OmMap.configureLicense(key)` lifts limits and removes the corner badge. When a user's map exceeds free limits, say so and point at the validation message rather than silently restructuring their data. Events on the element: `om-map-ready` (boot complete; `await mapEl.ready` is the promise twin), `om-validation-error`, `om-view-changed` (camera settled after a move, debounced; `detail = {longitude, latitude, zoom, pitch, bearing, origin}` where `origin` is `"user"` for gesture bursts vs `"programmatic"` for API/story moves — the camera-persistence hook, and the echo-suppression signal when syncing camera state to an app store), `om-map-point` (`detail = {coordinate: [lng,lat]|null, kind}` — every click/hover map coordinate incl. empty-map clicks, for custom capture tools beyond the draw widget), `om-tileset-load` (`detail = {layerId, tileset}` — a Tile3DLayer's live deck Tileset3D, for tools needing the real tileset like region export). `MapController` mirrors these as `onViewChange`/`onMapPoint`/`onTilesetLoad` options. `await mapEl.snapshot()` returns a canvas-only PNG dataURL of the scene (basemap + layers at device pixels; `{as:"blob"}` for files) — DOM widgets/overlays/attribution are NOT captured, so exports must render provider credits themselves.
|
|
28
|
+
- `<om-layer id="..." type="ScatterplotLayer" data="./points.json">` — any deck.gl layer class by `type` (all 33 bundled, plus the native `COGLayer` raster type), plus `PopupLayer` (WebGL badges/labels at scale: `layout="badge|pin-label|card"`, `min-zoom`/`max-zoom`). Every type's full attribute list ships in the package's `onlymapjs.html-data.json` — consult it instead of guessing attribute names or reading the minified dist. TextLayer's default font atlas covers ASCII only: set `character-set` when label text carries other glyphs (`—`, `·`, accents) or deck warns and renders them blank. External layer classes register via `OmMap.registerLayer({type, deckClass, props})` — build them on `@nika-js/onlymap/deck` (the bundled `CompositeLayer`/`TileLayer`/… re-exports), never a separately-installed deck.gl (different class hierarchy, breaks in the renderer); function-valued props ride the subclass's `static defaultProps`; register at module top level BEFORE the manifest mounts (see docs/custom-layers.md). Data: `data` URL (JSON, GeoJSON, CSV/TSV `.csv` — parsed to typed columns — Shapefile `.shp` (+`.dbf` attributes) and KML `.kml` as GeoJSON features, CityJSON `.city.json` / CityJSONSeq `.city.jsonl` (semantic 3D city models — 3DBAG, PLATEAU — decoded to one of two shapes by the `data` URL, no CityJSON layer type: default → extruded footprints, `type="GeoJsonLayer" extruded get-elevation="$roof_height"`; `?om-surfaces=1` → one row PER FACE at its own real per-vertex height so a pitched LoD2.2 roof actually looks pitched, `type="SolidPolygonLayer" get-polygon="$polygon" full3d` (`extruded` stays at its ordinary `false` default) (flat-shaded — deck.gl only lights the `extruded` shader path — each row also carrying `surface_type`: RoofSurface/WallSurface/GroundSurface, and `fill_color`: a ninja-viewer-style default color per surface_type/cityobject_type, verified against cityjson-threejs-loader's own default palette — `get-fill-color` on `SolidPolygonLayer` reads it automatically when left unauthored, no color attribute required, and an authored `get-fill-color` still overrides it); derived properties (both modes) `roof_height` (area-weighted mean roof height above ground), `eaves_height`, `ridge_height`, `ground_height`, `roof_area`, `surface_count`, `lod`, `cityobject_id`, `cityobject_type`, `parent_id` win over same-named source attributes, plus surfaces-mode-only `polygon`/`outline`/`surface_type`/`fill_color` (`outline` is the face's outer ring flattened and closed — bind a companion `type="PathLayer" get-path="$outline"` layer to it for visible face edges, since surfaces mode is flat-shaded and `SolidPolygonLayer`'s own `wireframe` prop is a no-op when unextruded — always pair one, matching `filter-field`/`filter-range` to the fill layer), and a parent Building's attributes are inherited by its BuildingPart rows; national grids NL/CH/DE/JP/AT/SG reproject automatically including axis order, other EPSG codes fail with an error naming the code; highest LoD wins, pin one with `?om-lod=1.2` (combine as `?om-lod=1.2&om-surfaces=1`, cached independently); `.city.jsonl` fills in as it downloads in either mode — see docs/3d-assets.md), or Arrow IPC `.arrow`/`.feather` — large point datasets stay columnar, GeoArrow line/polygon geometry becomes GeoJSON features, zstd-compressed IPC is handled; other formats plug in via `OmMap.registerFormat({match, parse})`; data URLs accept any scheme the runtime's fetch supports — desktop webviews (Tauri, Electron) pass asset-protocol URLs straight in), inline `<script type="application/json">` (row arrays or column-oriented `{"columns": {"lon": [...], "lat": [...]}}`; must be a DIRECT child of the `<om-layer>`, and when present it wins — omit the `data` attribute), or `wss://` streaming (`key="mmsi"` upserts entities in place, `flush="250ms"` coalesces bursts, `source="name"` selects a `OmMap.registerSource` decoder plugin), or a polled REST snapshot (`refresh="5s"` re-fetches and replaces — for live endpoints that return the full current state). Authenticated endpoints: call `OmMap.configureData({ headers: {...} })` in a script — never put tokens in attributes. `$field` accessors work identically on all of them — never write column-index code yourself. One columnar restriction: the `js` full-JS opt-in is not allowed on Arrow/columnar layers (validation will tell you; use `$field` accessors instead). For 3D models use `type="ScenegraphLayer"` with `scenegraph="./model.glb"` (required) and `get-orientation="[0, $heading, 90]"` — the roll of 90 stands Y-up glTF models upright; see docs/3d-assets.md. GeoTIFF/COG rasters use `type="COGLayer"` with `src="./dem.tif"` (NOT `data` — rasters stream tiles by Range request, they are not parsed rows): `min`/`max` set the rescale window (default 0–255; ALWAYS set them for float/16-bit data like DEMs), `colormap` picks a bundled ramp for single-band sources (gray, viridis, plasma, inferno, magma, cividis, rdylgn, rdbu, spectral, terrain, jet, turbo), `nodata` overrides the source sentinel (renders transparent); plain 8-bit RGB COGs need no styling attributes; restretch/recolor are GPU uniforms (no refetch) and the legend ramp derives from colormap+min/max automatically. Sources must be Cloud-Optimized (`gdal_translate -of COG` otherwise).
|
|
29
29
|
- `<om-widget type="legend|layer-switcher|basemap-switcher|lighting|zoom-controls|undo-redo|scale-bar|attribution|filter|vega-lite" position="bottom-end">` — static UI panels. `position` takes one of 8 managed slots (logical, RTL-aware: `top-start|top-center|top-end|center-start|center-end|bottom-start|bottom-center|bottom-end`; legacy corners `top-left` etc. alias) — same-slot widgets stack with flush edges and a shared gap (never overlap); provider attribution joins `bottom-end` and the license badge joins `bottom-start` as in-flow members, so required chrome never covers a widget; `order="1"` orders within a slot; adjacent compact button widgets (zoom-controls, undo-redo, widgets-toggle) auto-merge into ONE control group with dividers (`cluster="false"` opts a widget out); `position="manual"` renders a plain block you place with your own CSS (even outside the map). At map widths ≤640px managed widgets auto-fold into one accessible drawer per map side; `fold="never"` exempts an essential widget, `widgets-fold="off"` opts the map out, `--om-widget-fold-breakpoint` changes the threshold. Layout tokens on `<om-map>`: `widget-style="gap:10 opacity:0.9 inset:16"` (keys inset/gap/inset-x/-y/gap-x/-y/opacity/radius/size, px except opacity) or the `--om-widget-inset-x/-y/-gap-x/-gap-y/-opacity/-radius/-fold-breakpoint` custom properties. Built-ins are themeable from page CSS via custom properties (they inherit through the shadow root): `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` — full set: `--om-widget-bg/-fg/-muted/-border/-hover-bg/-accent`; scope to a single widget with an `om-widget[type=legend]` selector instead. No `type` + HTML + `<script type="om/widget">` = custom widget with `ctx` (`ctx.layers`, `ctx.data(id)`, `ctx.dataInViewport(id)`, `ctx.stats(id, field)`, `ctx.viewport`, `ctx.selection`, `ctx.emit(action, payload)`), `this.watch = ['data:<layerId>', 'viewport', 'selection', 'layers', 'history', 'basemap']` (`layers` also fires on visibility/filter changes; `basemap` on basemap switches; `history` on undo/redo availability), `this.$(sel)`, `vegaEmbed`/`d3` as globals.
|
|
30
30
|
- `<om-overlay id="..." anchor-from="selection">` — rich geo-anchored HTML (≤ ~20 per map). Anchors: `anchor="[lng, lat]"` (static), `anchor-from="selection"` (follows picks), or `anchor-layer="regions" anchor-feature-id="mission"` (anchored to a feature's own geometry — bbox center — no coordinates in markup; `{{field}}` interpolates that feature's attributes). `{{field}}` interpolates the picked feature HTML-escaped; `{{{field}}}` is raw (avoid). For labels on many features use `PopupLayer`, not overlays.
|
|
31
31
|
- `<om-behavior on="click|hover|drag|load|data-loaded" layer="..." action="...">` — declarative interaction. Built-in actions: `show-overlay`, `hide-overlay`, `show-tooltip`, `hide-tooltip`, `toggle-layer`, `filter-layer`, `highlight-feature`, `zoom-to-feature`, `set-basemap`, `undo`, `redo`. One payload contract everywhere: `{ layer, target, feature, featureId, coordinate }`.
|
package/onlymapjs.html-data.json
CHANGED
|
@@ -788,6 +788,14 @@
|
|
|
788
788
|
"name": "point-type",
|
|
789
789
|
"description": "deck.gl pointType."
|
|
790
790
|
},
|
|
791
|
+
{
|
|
792
|
+
"name": "get-polygon",
|
|
793
|
+
"description": "Accessor for deck.gl getPolygon — expression language: $field, scale(), arithmetic."
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
"name": "full3d",
|
|
797
|
+
"description": "deck.gl _full3d."
|
|
798
|
+
},
|
|
791
799
|
{
|
|
792
800
|
"name": "get-subtext",
|
|
793
801
|
"description": "Accessor for deck.gl getSubtext — expression language: $field, scale(), arithmetic."
|
|
@@ -1052,14 +1060,6 @@
|
|
|
1052
1060
|
"name": "_winding-order",
|
|
1053
1061
|
"description": "deck.gl _windingOrder."
|
|
1054
1062
|
},
|
|
1055
|
-
{
|
|
1056
|
-
"name": "get-polygon",
|
|
1057
|
-
"description": "Accessor for deck.gl getPolygon — expression language: $field, scale(), arithmetic."
|
|
1058
|
-
},
|
|
1059
|
-
{
|
|
1060
|
-
"name": "_full3d",
|
|
1061
|
-
"description": "deck.gl _full3d."
|
|
1062
|
-
},
|
|
1063
1063
|
{
|
|
1064
1064
|
"name": "background",
|
|
1065
1065
|
"description": "deck.gl background."
|
|
@@ -1208,6 +1208,10 @@
|
|
|
1208
1208
|
"name": "visible-max-zoom",
|
|
1209
1209
|
"description": "deck.gl visibleMaxZoom."
|
|
1210
1210
|
},
|
|
1211
|
+
{
|
|
1212
|
+
"name": "_full3d",
|
|
1213
|
+
"description": "deck.gl _full3d."
|
|
1214
|
+
},
|
|
1211
1215
|
{
|
|
1212
1216
|
"name": "unique-id-property",
|
|
1213
1217
|
"description": "deck.gl uniqueIdProperty."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nika-js/onlymap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Declarative deck.gl maps for HTML and React — interactive WebGL mapping with GeoJSON/CSV/Arrow data, MapLibre basemaps, widgets, popups, and live streams from a custom-element manifest or typed React components. TypeScript, no build step.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"publishConfig": {
|
|
@@ -156,6 +156,7 @@
|
|
|
156
156
|
"maplibre-gl": "^5.24.0",
|
|
157
157
|
"mobx": "^6.16.1",
|
|
158
158
|
"playwright": "^1.61.1",
|
|
159
|
+
"proj4": "^2.20.8",
|
|
159
160
|
"react": "^19.2.7",
|
|
160
161
|
"react-dom": "^19.2.7",
|
|
161
162
|
"typescript": "^5.6.0",
|
|
@@ -166,5 +167,5 @@
|
|
|
166
167
|
"ws": "^8.21.0",
|
|
167
168
|
"zustand": "^5.0.14"
|
|
168
169
|
},
|
|
169
|
-
"comment:deck-pin": "deck.gl/luma.gl devDeps are EXACT pins: they get BUNDLED into dist, and both libraries hard-throw on duplicate-version detection — consumers that also ship their own deck.gl (nika-agent) must match these versions exactly, so
|
|
170
|
+
"comment:deck-pin": "deck.gl/luma.gl devDeps are EXACT pins: they get BUNDLED into dist, and both libraries hard-throw on duplicate-version detection — consumers that also ship their own deck.gl (nika-agent) must match these versions exactly. CONTRACT: the bundled deck.gl/luma.gl version is part of the release surface and only changes at the SemVer boundary a caret does NOT cross — a MAJOR onlymap release (a 0.x MINOR while pre-1.0). Anything a caret floats (minor+patch once >=1.0; patch while 0.x) must NEVER move deck.gl, so a consumer can caret onlymap safely. Bumping deck is a breaking-coordination event, never a side effect of a minor/patch release or a reinstall."
|
|
170
171
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: onlymapjs
|
|
3
|
-
description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
|
|
3
|
+
description: Build, edit, debug, or review OnlyMapJS declarative HTML maps and dashboards, or React maps via the @nika-js/onlymap/react adapter. Use when a user asks for an interactive map, deck.gl-style visualization, geospatial dashboard, live fleet/telemetry map, choropleth, popup/tooltip map, map story/tour, manual drawing/sketch map, 3D map assets, a React map component, a map page shared as a single HTML file (incl. no-JS fallbacks for chat/email previews), a responsive/mobile map whose controls auto-fold on narrow screens, auditing a map's widget layout with the check-layout tool, syncing OnlyMapJS map/camera state into an app state store (Redux, MobX, Zustand, Jotai — the getStore contract), or help with OnlyMapJS syntax, validation, widgets, data formats, testing, or publishing examples.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# OnlyMapJS
|
|
@@ -23,7 +23,7 @@ Use OnlyMapJS as a declarative HTML map library. Write custom elements such as `
|
|
|
23
23
|
</script>
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
For no-build CDN pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.
|
|
26
|
+
For no-build CDN pages, use the single-file standalone bundle from a raw-file CDN — `https://unpkg.com/@nika-js/onlymap@0.5.0` (the bare package URL serves `dist/onlymap.standalone.js`) — plus `<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.5.0/dist/onlymapjs.css">`. Never a rebundling CDN (esm.sh, skypack): re-bundling duplicates the deck.gl/luma.gl runtime and every layer fails shader compilation.
|
|
27
27
|
|
|
28
28
|
## React Projects
|
|
29
29
|
|
|
@@ -47,6 +47,7 @@ Load the smallest reference needed for the task:
|
|
|
47
47
|
## Non-Negotiable Syntax Rules
|
|
48
48
|
|
|
49
49
|
- Always use explicit closing tags: `<om-layer ...></om-layer>`, not `<om-layer ... />`.
|
|
50
|
+
- Give `<om-map>` a height. A custom element is `display:inline` by default and collapses to zero size; the library injects a `display:block` default (fills a sized parent, else a 400px floor) so a bare map is still visible, but set a real height — full page: `om-map { display:block; height:100vh }` with `html,body { height:100% }`, or a sized container. Any height you set wins over the floor, including one below 400px. A still-collapsed map logs a console warning naming the fix.
|
|
50
51
|
- Every `<om-layer>` needs a stable `id`.
|
|
51
52
|
- Attribute names are kebab-case: `get-fill-color`, `radius-units`, `line-width-min-pixels`.
|
|
52
53
|
- Accessor values are expressions: `get-position="[$lon, $lat]"`.
|
|
@@ -69,6 +70,7 @@ Load the smallest reference needed for the task:
|
|
|
69
70
|
- Keep mobile chrome usable -> rely on the default map-width auto-fold into per-side drawers; mark only essential controls `fold="never"`. Use `widgets-fold="off"` only when the user explicitly wants fixed wide-layout chrome.
|
|
70
71
|
- Group adjacent map buttons (zoom + undo + toggle into one control group) -> just place compact button widgets in the same `position` slot; they auto-cluster. `cluster="false"` opts one out. Do NOT build a wrapper widget.
|
|
71
72
|
- GeoTIFF/COG raster (DEM, satellite imagery, NDVI) -> `<om-layer type="COGLayer" src="…tif">` with `min`/`max`/`colormap` for single-band data (see syntax.md — `src`, not `data`).
|
|
73
|
+
- CityJSON/CityJSONSeq per-face surfaces mode (`?om-surfaces=1`) -> always pair the `SolidPolygonLayer` with a companion `<om-layer type="PathLayer">` using `get-path="$outline"` to make roof/wall edges visible. This mode is unlit and `SolidPolygonLayer`'s own `wireframe` prop does nothing here (deck only builds wireframe geometry when `extruded: true`); the `PathLayer` is the only way to see face edges. Give it the same `filter-field`/`filter-range` as the fill layer so filtered-out buildings' outlines disappear too. See syntax.md.
|
|
72
74
|
- Live entity updates -> `wss://` stream with `key` and optional `source` decoder.
|
|
73
75
|
- REST snapshot that changes over time -> `refresh="5s"`.
|
|
74
76
|
- User sketching -> `data="draw:sketch"` layer plus `<om-widget type="draw" target="sketch">`.
|
|
@@ -15,6 +15,10 @@ Use these patterns as starting points. Replace data URLs, layer ids, fields, cen
|
|
|
15
15
|
import "@nika-js/onlymap/onlymapjs.css";
|
|
16
16
|
</script>
|
|
17
17
|
<style>
|
|
18
|
+
/* Always give om-map a height — a custom element is display:inline by
|
|
19
|
+
default. Omit this and the library injects a display:block default that
|
|
20
|
+
fills a sized parent, else a 400px floor; set a real height for
|
|
21
|
+
full-page layout. */
|
|
18
22
|
html, body { margin: 0; height: 100%; }
|
|
19
23
|
om-map { display: block; height: 100vh; }
|
|
20
24
|
</style>
|
|
@@ -16,14 +16,16 @@ Vite/npm project:
|
|
|
16
16
|
Static CDN page (raw-file CDNs only — unpkg/jsDelivr; never esm.sh or another rebundling CDN, which duplicates the WebGL runtime and breaks layer shaders):
|
|
17
17
|
|
|
18
18
|
```html
|
|
19
|
-
<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.
|
|
20
|
-
<script type="module" src="https://unpkg.com/@nika-js/onlymap@0.
|
|
19
|
+
<link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap@0.5.0/dist/onlymapjs.css">
|
|
20
|
+
<script type="module" src="https://unpkg.com/@nika-js/onlymap@0.5.0"></script>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Always include `onlymapjs.css` — it carries the MapLibre basemap styles and the no-JS fallback rules (`<om-fallback>` / default banner). For the fallback to work in script-disabled previews it must load without JavaScript: a real `<link rel="stylesheet">` or inlined `<style>` on no-build pages (a bundler-emitted stylesheet is fine in npm projects).
|
|
24
24
|
|
|
25
25
|
## Elements
|
|
26
26
|
|
|
27
|
+
|
|
28
|
+
|
|
27
29
|
### `<om-map>`
|
|
28
30
|
|
|
29
31
|
Root element. Children are layers, widgets, overlays, behaviors, stories, and an optional fallback.
|
|
@@ -39,13 +41,13 @@ Common attributes:
|
|
|
39
41
|
- `validate` to show live validation errors during authoring
|
|
40
42
|
- `map-id="<random UUID>"` — identifies the map artifact for usage telemetry (not a visitor id). Include one on new complete pages, freshly generated — never copied from an example; deleting it is the opt-out.
|
|
41
43
|
- `telemetry="off"` disables usage telemetry for this map (global: `OmMap.configureTelemetry({ disabled: true })`)
|
|
42
|
-
- `license-key="om_live_…"` — lifts the free-plan limits (5 layers, 25k rows per layer) and removes the corner badge; publishable origin-restricted token, safe in page source (or `OmMap.configureLicense(key)` once). Free-plan violations don't break the map: the offending layer isn't rendered and validation names the limit.
|
|
44
|
+
- `license-key="om_live_…"` — lifts the free-plan limits (5 layers, 25k rows per layer — a layer past the layer cap doesn't render, a layer past the row cap renders its first 25k rows plus a dismissible on-map notice) and removes the corner badge; publishable origin-restricted token, safe in page source (or `OmMap.configureLicense(key)` once). Free-plan violations don't break the map: the offending layer isn't rendered and validation names the limit.
|
|
43
45
|
- `terrain="terrarium|<preset>|<{z}/{x}/{y} DEM URL>|off"` — 3D elevation surface. `terrarium` is keyless (AWS); `maptiler-terrain` needs `basemap-key`/`configureBasemap`; raw DEM URLs need `terrain-decoder` (`terrarium`, `mapbox-rgb`, or `{rScaler,gScaler,bScaler,offset}` JSON). `terrain-exaggeration` scales relief (1 = true); `terrain-max-zoom` = the provider's REAL tileset cap; `terrain-texture` drapes a `{z}/{x}/{y}` imagery template. Geographic layers drape automatically; per-layer `terrain="drape|offset|off"` overrides (3D-model layers default to `offset`). Terrain REPLACES an active basemap while on (restored when off) — validation warns. Register presets with `OmMap.registerTerrain(name, {...})`; `set-terrain` action + `terrain` watch token; attribute-backed (undoable).
|
|
44
46
|
- `lighting="daylight|studio|flat|custom"` — scene lighting for 3D content (extruded polygons, models); absent = deck defaults. Preset seeds values; `lighting-ambient`, `lighting-sun` (intensity; 0 removes the sun), `lighting-sun-azimuth` (° CW from north), `lighting-sun-elevation` (° above horizon), `lighting-camera` (model-inspection fill) override individual fields; `lighting-sun-date` (ISO 8601 or epoch ms) computes the sun from solar position at the map center and wins over azimuth/elevation. Attribute-backed: changes are undoable, and the `set-lighting {lighting, sunAzimuth, …}` action makes lighting story-steppable (`lighting="default"` removes the whole attribute set; a bare preset is a clean reset). `<om-widget type="lighting">` is the native UI. Widget scripts can `watch = ["lighting"]`.
|
|
45
47
|
- `widgets-dim="off"` disables collision-dim — by default a widget slot dims (`--om-widget-opacity-dimmed`, 0.35) while an open `<om-overlay>` popup covers it, rather than the popup dodging (attribution/toggle slots never dim).
|
|
46
48
|
- `headless width="800" height="600"` for test harness use
|
|
47
49
|
|
|
48
|
-
Events: `om-map-ready` (boot complete; `await mapEl.ready` is the promise twin), `om-validation-error`,
|
|
50
|
+
Events: `om-map-ready` (boot complete; `await mapEl.ready` is the promise twin), `om-validation-error`, `om-view-changed` — fires once the camera settles after a move (debounced; `detail = {longitude, latitude, zoom, pitch, bearing}`), the hook for persisting the camera — `om-map-point` (`detail = {coordinate: [lng,lat]|null, kind: "click"|"hover"}` — every click/hover map coordinate, including empty-map clicks; the hook for custom capture tools the built-in draw widget doesn't cover), and `om-tileset-load` (`detail = {layerId, tileset}` — a `Tile3DLayer`'s live deck `Tileset3D`, for tools needing the real tileset such as region export). `MapController` mirrors these as `onViewChange` / `onMapPoint` / `onTilesetLoad` options.
|
|
49
51
|
|
|
50
52
|
`await mapEl.snapshot()` (also on `MapController`) returns a canvas-only PNG dataURL of the scene — basemap + layers composited at device pixels (`{as: "blob"}` for files; `type`/`quality` for jpeg/webp). DOM widgets, overlays, and the provider attribution are NOT in the pixels: exports must render credits themselves. Await `ready` first; headless maps reject.
|
|
51
53
|
|
|
@@ -57,6 +59,8 @@ Example:
|
|
|
57
59
|
</om-map>
|
|
58
60
|
```
|
|
59
61
|
|
|
62
|
+
|
|
63
|
+
|
|
60
64
|
### `<om-layer>`
|
|
61
65
|
|
|
62
66
|
Declares a deck.gl layer. Required: `id`, `type`.
|
|
@@ -68,7 +72,7 @@ Core attributes:
|
|
|
68
72
|
- `data="./points.json"` — URL, stream, draw store, or omit for inline JSON.
|
|
69
73
|
- `label="Earthquakes"` and `color="#b30000"` — legend metadata.
|
|
70
74
|
- `pickable` — enable click/hover behaviors.
|
|
71
|
-
- `visible="false"` or `opacity="0"` — initial visibility/opacity.
|
|
75
|
+
- `visible="false"` or `opacity="0"` — initial visibility/opacity.w
|
|
72
76
|
|
|
73
77
|
Accessors:
|
|
74
78
|
|
|
@@ -98,6 +102,8 @@ Filtering:
|
|
|
98
102
|
filter-field="magnitude" filter-range="[4, 10]"
|
|
99
103
|
```
|
|
100
104
|
|
|
105
|
+
|
|
106
|
+
|
|
101
107
|
### Built-In Layer Types
|
|
102
108
|
|
|
103
109
|
Use the `type` value exactly:
|
|
@@ -114,6 +120,8 @@ Common choices:
|
|
|
114
120
|
- 3D models: `ScenegraphLayer`, `SimpleMeshLayer`, `PointCloudLayer`, `Tile3DLayer`.
|
|
115
121
|
- GeoTIFF/COG rasters: `COGLayer`.
|
|
116
122
|
|
|
123
|
+
|
|
124
|
+
|
|
117
125
|
### COGLayer (GeoTIFF / COG rasters)
|
|
118
126
|
|
|
119
127
|
```html
|
|
@@ -133,21 +141,57 @@ External layer classes become manifest types via `OmMap.registerLayer({type, dec
|
|
|
133
141
|
|
|
134
142
|
### Data Sources
|
|
135
143
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
|
139
|
-
|
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
|
|
|
144
|
+
|
|
145
|
+
| Source | Manifest | Notes |
|
|
146
|
+
| -------------------- | ----------------------------------------------------------- | ------------------------------------------------------------- |
|
|
147
|
+
| JSON / GeoJSON URL | `data="./points.json"` | Arrays or FeatureCollections. |
|
|
148
|
+
| Inline JSON | child `<script type="application/json">` | Good for tests/demos. |
|
|
149
|
+
| Columnar JSON | `{"columns": {"lon": [...], "lat": [...]}}` | Fast point path. |
|
|
150
|
+
| CSV / TSV | `data="./quakes.csv"` | Parsed to typed columns. |
|
|
151
|
+
| Arrow / GeoArrow IPC | `data="./big.arrow"` | Points stay columnar; lines/polygons become GeoJSON features. |
|
|
152
|
+
| Shapefile | `data="./countries.shp"` | Loads sidecars and joins `.dbf` attributes. |
|
|
153
|
+
| KML | `data="./tour.kml"` | Placemarks become GeoJSON features. |
|
|
154
|
+
| CityJSON | `data="./tile.city.json"` | 3D city models → extruded footprints, or `?om-surfaces=1` for real per-face roof geometry; see below. |
|
|
155
|
+
| CityJSONSeq | `data="./tile.city.jsonl"` | Same, streamed line by line as it downloads. |
|
|
156
|
+
| WebSocket | `data="wss://feed" key="id" flush="250ms" source="decoder"` | Upsert-by-key stream. |
|
|
157
|
+
| Polling | `data="/api/fleet.json" refresh="5s"` | Snapshot replace. |
|
|
158
|
+
| Draw store | `data="draw:sketch"` | Written by draw widget. |
|
|
159
|
+
|
|
148
160
|
|
|
149
161
|
Data URLs accept any scheme the runtime's `fetch` supports — desktop webviews (Tauri, Electron) pass asset-protocol URLs (`asset://localhost/…`, custom schemes) straight in; format detection reads the path extension either way.
|
|
150
162
|
|
|
163
|
+
CityJSON (3DBAG, PLATEAU, swisstopo) decodes to one of two shapes, chosen by the `data` URL — there is no CityJSON layer type:
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<!-- Default: one GeoJSON footprint per CityObject, extruded to a single
|
|
167
|
+
derived height. Lit, terrain-aware; can't show a pitched roof's shape. -->
|
|
168
|
+
<om-layer id="buildings" type="GeoJsonLayer" data="./tile.city.json"
|
|
169
|
+
extruded get-elevation="$roof_height"
|
|
170
|
+
get-fill-color="$b3_dak_type == 'slanted' ? '#d6604d' : '#4393c3'"
|
|
171
|
+
pickable></om-layer>
|
|
172
|
+
|
|
173
|
+
<!-- ?om-surfaces=1: one row per FACE, each at its own real height — a
|
|
174
|
+
pitched LoD2.2 roof actually looks pitched. Flat-shaded (unlit). NO
|
|
175
|
+
get-fill-color needed: SolidPolygonLayer defaults it to a `fill_color`
|
|
176
|
+
field the decoder populates with a ninja-viewer-style palette. -->
|
|
177
|
+
<om-layer id="roofs" type="SolidPolygonLayer" data="./tile.city.json?om-surfaces=1"
|
|
178
|
+
get-polygon="$polygon" full3d
|
|
179
|
+
pickable></om-layer>
|
|
180
|
+
|
|
181
|
+
<!-- Surfaces mode is unlit, so face edges are invisible without this: a
|
|
182
|
+
companion PathLayer tracing the decoder's `outline` field (the same
|
|
183
|
+
face, flattened to a closed 2D path). ALWAYS pair one with the
|
|
184
|
+
SolidPolygonLayer above in surfaces mode — SolidPolygonLayer's own
|
|
185
|
+
`wireframe` prop is a no-op here (deck only builds wireframe geometry
|
|
186
|
+
when `extruded: true`). Match filter-field/filter-range to the fill
|
|
187
|
+
layer so filtered-out buildings' outlines disappear too. -->
|
|
188
|
+
<om-layer id="roof-outlines" type="PathLayer" data="./tile.city.json?om-surfaces=1"
|
|
189
|
+
get-path="$outline" get-color="[0, 0, 0]" width-min-pixels="1"
|
|
190
|
+
pickable="false"></om-layer>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Derived properties (these win over same-named source attributes, present in both modes): `roof_height` (area-weighted mean roof height above ground — the one to extrude by in the default mode), `eaves_height`, `ridge_height`, `ground_height`, `roof_area` (true 3D m²), `surface_count`, `lod`, `cityobject_id`, `cityobject_type`, `parent_id`. Surfaces-mode rows add `polygon` (this face's rings, `[[lng, lat, elevation], …]` — bind `get-polygon` to it), `outline` (same face's outer ring only, flattened and closed — bind a `PathLayer`'s `get-path` to it for visible edges, per above), `surface_type` (`"RoofSurface"` / `"WallSurface"` / `"GroundSurface"` / undefined), and `fill_color` — a default color per surface_type/cityobject_type (RoofSurface red, WallSurface white, Building blue, WaterBody light blue, …), verified against the actual default palette `cityjson-threejs-loader` (the engine behind the ninja reference viewer) ships. `SolidPolygonLayer`'s `get-fill-color` reads `$fill_color` automatically when left unauthored — zero color attributes needed for a reasonable render — and an authored `get-fill-color` (or a plain `color="…"`) still overrides it, same as any layer. Every source attribute survives, with a parent `Building`'s attributes inherited by its `BuildingPart` rows. National grids (NL 28992/7415, CH 2056, DE 25832/25833/5555/5556, JP 6668/6697/6669–6687, AT 31254/31255/31256, SG 3414) reproject automatically, axis order included; any other EPSG code fails with an error naming it. The highest LoD is used — pin one with `?om-lod=1.2` (combine as `?om-lod=1.2&om-surfaces=1`). `?om-surfaces=1` is flat-shaded — deck's solid-polygon shader only lights `extruded` geometry; it also costs roughly 30× the rows (one per face, not per building), so it reaches the free tier's 25k-row cap at a few hundred buildings where the footprint mode would not — past which the layer renders its first 25k rows (a partial scene, with an on-map notice) rather than going blank. See `docs/3d-assets.md`.
|
|
194
|
+
|
|
151
195
|
Authenticated fetches:
|
|
152
196
|
|
|
153
197
|
```js
|
|
@@ -173,6 +217,8 @@ OmMap.registerSource("fleet", {
|
|
|
173
217
|
});
|
|
174
218
|
```
|
|
175
219
|
|
|
220
|
+
|
|
221
|
+
|
|
176
222
|
### Accessor Blocks
|
|
177
223
|
|
|
178
224
|
Restricted expression block:
|
|
@@ -291,6 +337,8 @@ Example:
|
|
|
291
337
|
<om-behavior on="click" layer="quakes" action="show-overlay" target="detail"></om-behavior>
|
|
292
338
|
```
|
|
293
339
|
|
|
340
|
+
|
|
341
|
+
|
|
294
342
|
### `<om-fallback>`
|
|
295
343
|
|
|
296
344
|
Static content shown only where scripts never run — chat-app/email file previews (iOS QuickLook), file managers, sandboxed webviews. Hidden automatically once the map boots. Good practice on every complete page, especially one that may be shared as a file.
|
|
@@ -311,6 +359,8 @@ Example:
|
|
|
311
359
|
</om-fallback>
|
|
312
360
|
```
|
|
313
361
|
|
|
362
|
+
|
|
363
|
+
|
|
314
364
|
### `<om-behavior>`
|
|
315
365
|
|
|
316
366
|
Declarative event to action binding.
|
|
@@ -341,6 +391,8 @@ Example:
|
|
|
341
391
|
<om-behavior on="click" layer="regions" action="zoom-to-feature" duration="1200ms"></om-behavior>
|
|
342
392
|
```
|
|
343
393
|
|
|
394
|
+
|
|
395
|
+
|
|
344
396
|
### Stories
|
|
345
397
|
|
|
346
398
|
Use `<om-story>` with `<om-step>` children. Stories are siblings of layers/overlays, not containers.
|
|
@@ -425,3 +477,4 @@ For 3D Tiles LOD/refinement experiments, use:
|
|
|
425
477
|
maximum-memory-usage="256"
|
|
426
478
|
view-distance-scale="0.85"></om-layer>
|
|
427
479
|
```
|
|
480
|
+
|
|
@@ -83,6 +83,7 @@ Harness operations:
|
|
|
83
83
|
|
|
84
84
|
- `h.pick({ layer, featureId | index, type })`
|
|
85
85
|
- `h.clearSelection()`
|
|
86
|
+
- `h.mapPoint(coordinate, kind?)` — feed a click/hover map coordinate through the real onMapPoint path (fires `om-map-point`, drives the draw controller); for testing custom capture/AOI tools headlessly
|
|
86
87
|
- `h.emit(action, payload)`
|
|
87
88
|
- `h.setView({ center, zoom, pitch, bearing })`
|
|
88
89
|
- `h.layers()`
|
|
@@ -92,7 +93,7 @@ Harness operations:
|
|
|
92
93
|
|
|
93
94
|
Use `vi.stubGlobal("fetch", ...)` for URL data and `vi.stubGlobal("WebSocket", ...)` for streams.
|
|
94
95
|
|
|
95
|
-
Free-plan license gates (5 layers, 25k rows per layer) apply in headless tests exactly as in production
|
|
96
|
+
Free-plan license gates (5 layers, 25k rows per layer) apply in headless tests exactly as in production, and emit errors on the validation stream either way: a layer past the LAYER cap doesn't render at all, while a layer past the ROW cap renders its first 25,000 rows in source order — so assert on row counts, not on emptiness. (The dismissible on-map notice that accompanies truncation is browser-only chrome; headless never mounts it.) If the page under test legitimately exceeds the limits, call `OmMap.configureLicense("om_live_…")` in test setup; keys verify offline, so CI needs no network. Telemetry never fires from headless maps.
|
|
96
97
|
|
|
97
98
|
## Layout Audit
|
|
98
99
|
|