@nika-js/onlymap 0.4.6 → 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 CHANGED
@@ -46,7 +46,7 @@ deck.gl is the best WebGL data-visualization engine there is — and OnlyMapJS i
46
46
  |---|---|---|
47
47
  | Setup | `new Deck({...})`, canvas + basemap sync wiring | one `<om-map>` element (or `<OmMap>` in React) |
48
48
  | State | your reducers/stores drive `setProps` | the manifest **is** the state — edit an attribute, the map reconciles; undo/redo built in |
49
- | Data loading | fetch + parse + reload yourself | `data="…"` — GeoJSON, CSV, Arrow/GeoArrow, Shapefile, KML, WebSocket streams, polled REST; GeoTIFF/COG rasters via `type="COGLayer"` |
49
+ | Data loading | fetch + parse + reload yourself | `data="…"` — GeoJSON, CSV, Arrow/GeoArrow, Shapefile, KML, CityJSON, WebSocket streams, polled REST; GeoTIFF/COG rasters via `type="COGLayer"` |
50
50
  | Accessors | JS functions + `updateTriggers` bookkeeping | `get-*` expressions; update triggers derived automatically |
51
51
  | UI | build legends/popups/filters from scratch | built-in widgets, overlays, behaviors — declarative |
52
52
  | Testing | mock WebGL or ship untested | `OmMap.validate`, IR snapshots, headless behavioral harness |
@@ -69,7 +69,7 @@ Or with no build step at all, straight from a CDN:
69
69
 
70
70
  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
71
 
72
- Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with 540 unit/behavioral tests and 53 Playwright GPU tests.
72
+ Then `npx @nika-js/onlymap init` wires up VS Code IntelliSense and `!`-prefixed manifest snippets for your project. The library ships with 590 unit/behavioral tests and 58 Playwright GPU tests.
73
73
 
74
74
  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
75
 
@@ -79,7 +79,7 @@ A handful of elements, one rule: **attributes are kebab-case versions of deck.gl
79
79
 
80
80
  | Element | Role |
81
81
  |---|---|
82
- | `<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. |
82
+ | `<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
83
  | `<om-layer>` | Any of **34 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 and the native `COGLayer` for GeoTIFF rasters. `id` required; `label`/`color` feed the legend. |
84
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`, `attribution`, `filter`, `draw`, `vega-lite` (live charts). 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
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`. `{{field}}` interpolates the picked feature, HTML-escaped by default. |
@@ -111,6 +111,7 @@ Built-ins: `scale()` (types: `sequential`, `diverging`, `threshold`, `sqrt`, `lo
111
111
  | **CSV / TSV** | `data="./quakes.csv"` | parsed to typed columns (lazy ~48 KB chunk); numbers auto-typed, quoted fields intact |
112
112
  | **Shapefile** | `data="./countries.shp"` | geometry + `.dbf` attributes joined into GeoJSON features (sidecars fetched with your auth config) |
113
113
  | **KML** | `data="./tour.kml"` | placemarks → GeoJSON features; other formats plug in via `OmMap.registerFormat` |
114
+ | **CityJSON / CityJSONSeq** | `data="./tile.city.json"` + `extruded get-elevation="$roof_height"` | semantic 3D city models (3DBAG, PLATEAU) → extruded footprints with derived `roof_height`/`eaves_height`/`ridge_height`/`roof_area`; national grids reprojected automatically (NL/CH/DE/JP/AT/SG); `.city.jsonl` streams in as it downloads; lazy chunk — see [docs/3d-assets.md](docs/3d-assets.md#semantic-city-models-cityjson) |
114
115
  | **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 |
115
116
  | **WebSocket stream** | `data="wss://feed" key="id" flush="250ms" source="myFormat"` | upsert-by-key, burst coalescing, auto-reconnect; decode any format via `OmMap.registerSource` |
116
117
  | **Polled REST snapshot** | `data="/api/fleet.json" refresh="5s"` | full-snapshot replace per poll; outages keep the last good data |
@@ -263,7 +264,7 @@ Plus `OmMap.snapshotIR(html)` to lock down what a manifest *means* in a snapshot
263
264
 
264
265
  ## Free tier & licensing
265
266
 
266
- **Non-commercial use is free with attribution** (LICENSE.md §3); commercial use requires a license. Without a license key, maps run on the **free plan**: up to **5 layers** and **25,000 rows per layer** (20 MB per data fetch), with a small "OnlyMap by NIKA. Free for non-commercial use." badge in the corner. Exceeding a limit never breaks the map the offending layer simply doesn't render, and the validation stream/error panel tells you exactly which limit, why, and how to lift it. Limits apply identically everywhere (including localhost — no dev/prod surprises).
267
+ **Non-commercial use is free with attribution** (LICENSE.md §3); commercial use requires a license. Without a license key, maps run on the **free plan**: up to **5 layers** and **25,000 rows per layer** (20 MB per data fetch), with a small "OnlyMap by NIKA. Free for non-commercial use." badge in the corner. Exceeding a limit never breaks the map. Past the **layer** cap the extra layer simply doesn't render; past the **row** cap the layer renders its first 25,000 rows (an arbitrary subset, in source order) rather than nothing, with a dismissible on-map notice saying the data is partial — so a capped map is still useful to look at without ever pretending it's complete. Either way the validation stream/error panel tells you exactly which limit, why, and how to lift it. Limits apply identically everywhere (including localhost — no dev/prod surprises).
267
268
 
268
269
  A license key lifts all limits and removes the badge:
269
270
 
@@ -168,7 +168,7 @@ hand.
168
168
  | pbf | 3.3.0 | BSD-3-Clause |
169
169
  | potpack | 2.1.0 | ISC |
170
170
  | process-nextick-args | 2.0.1 | MIT |
171
- | proj4 | 2.6.2 | MIT |
171
+ | proj4 | 2.20.9 | MIT |
172
172
  | protocol-buffers-schema | 3.6.1 | MIT |
173
173
  | quickselect | 3.0.0 | ISC |
174
174
  | readable-stream | 2.3.8 | MIT |
@@ -7210,7 +7210,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7210
7210
  SOFTWARE.**
7211
7211
  ```
7212
7212
 
7213
- ## proj4@2.6.2 (MIT)
7213
+ ## proj4@2.20.9 (MIT)
7214
7214
 
7215
7215
  ```
7216
7216
  ## Proj4js -- Javascript reprojection library.
@@ -19,7 +19,7 @@ async function v(N = {}) {
19
19
  if (n) {
20
20
  const { createRequire: I } = await import(
21
21
  /*webpackIgnore:true*/
22
- "./lerc-CaQOmDCP.js"
22
+ "./lerc-BhtFlCQ3.js"
23
23
  ).then((g) => g._);
24
24
  var O = I(import.meta.url);
25
25
  }
@@ -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-DSkgI4Oc.js";
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-DiYFebYs.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;
@@ -0,0 +1,407 @@
1
+ function P(t, e) {
2
+ if (!Array.isArray(t)) return [];
3
+ const n = e?.scale?.[0] ?? 1, o = e?.scale?.[1] ?? 1, s = e?.scale?.[2] ?? 1, a = e?.translate?.[0] ?? 0, i = e?.translate?.[1] ?? 0, c = e?.translate?.[2] ?? 0, r = new Array(t.length);
4
+ for (let f = 0; f < t.length; f++) {
5
+ const l = t[f];
6
+ r[f] = [l[0] * n + a, l[1] * o + i, (l[2] ?? 0) * s + c];
7
+ }
8
+ return r;
9
+ }
10
+ const T = {
11
+ MultiPoint: 1,
12
+ MultiLineString: 2,
13
+ MultiSurface: 3,
14
+ CompositeSurface: 3,
15
+ Solid: 4,
16
+ MultiSolid: 5,
17
+ CompositeSolid: 5
18
+ };
19
+ function J(t, e) {
20
+ let n = t;
21
+ for (let o = 0; o < e; o++) n = n.flat();
22
+ return n;
23
+ }
24
+ function A(t) {
25
+ const e = T[t.type ?? ""];
26
+ return !e || e < 3 || !Array.isArray(t.boundaries) ? [] : J(t.boundaries, e - 3);
27
+ }
28
+ function X(t, e) {
29
+ const n = new Array(e).fill(void 0), o = T[t.type ?? ""], s = t.semantics?.surfaces, a = t.semantics?.values;
30
+ if (!o || !Array.isArray(s) || !Array.isArray(a)) return n;
31
+ const i = J(a, o - 3);
32
+ for (let c = 0; c < e; c++) {
33
+ const r = i[c];
34
+ typeof r == "number" && (n[c] = s[r]?.type);
35
+ }
36
+ return n;
37
+ }
38
+ function L(t) {
39
+ const e = typeof t == "number" ? t : Number.parseFloat(String(t ?? ""));
40
+ return Number.isFinite(e) ? e : -1;
41
+ }
42
+ function q(t, e) {
43
+ if (!Array.isArray(t)) return;
44
+ const n = t.filter((o) => A(o).length > 0);
45
+ if (n.length !== 0) {
46
+ if (e !== void 0) {
47
+ const o = n.find((s) => L(s.lod) === e);
48
+ if (o) return o;
49
+ }
50
+ return n.reduce((o, s) => L(s.lod) > L(o.lod) ? s : o);
51
+ }
52
+ }
53
+ const K = {
54
+ 7415: 28992,
55
+ // Amersfoort / RD New + NAP height — the 3DBAG default
56
+ 6697: 6668,
57
+ // JGD2011 geographic 3D + height — the PLATEAU default
58
+ 5555: 25832,
59
+ // ETRS89 / UTM 32N + DHHN92 height
60
+ 5556: 25833
61
+ // ETRS89 / UTM 33N + DHHN92 height
62
+ }, G = [
63
+ [33, 129.5],
64
+ [33, 131],
65
+ [36, 132.166666666667],
66
+ [33, 133.5],
67
+ [36, 134.333333333333],
68
+ [36, 136],
69
+ [36, 137.166666666667],
70
+ [36, 138.5],
71
+ [36, 139.833333333333],
72
+ [40, 140.833333333333],
73
+ [44, 140.25],
74
+ [44, 142.25],
75
+ [44, 144.25],
76
+ [26, 142],
77
+ [26, 127.5],
78
+ [26, 124],
79
+ [26, 131],
80
+ [20, 136],
81
+ [26, 154]
82
+ ], j = {
83
+ // Netherlands — Amersfoort / RD New (3DBAG).
84
+ 28992: {
85
+ def: "+proj=sterea +lat_0=52.1561605555556 +lon_0=5.38763888888889 +k=0.9999079 +x_0=155000 +y_0=463000 +ellps=bessel +towgs84=565.4171,50.3319,465.5524,-0.398957,0.343988,-1.8774,4.0725 +units=m +no_defs"
86
+ },
87
+ // Switzerland — CH1903+ / LV95.
88
+ 2056: {
89
+ def: "+proj=somerc +lat_0=46.9524055555556 +lon_0=7.43958333333333 +k_0=1 +x_0=2600000 +y_0=1200000 +ellps=bessel +towgs84=674.374,15.056,405.346,0,0,0,0 +units=m +no_defs"
90
+ },
91
+ // Germany — ETRS89 / UTM zones 32N and 33N.
92
+ 25832: { def: "+proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs" },
93
+ 25833: { def: "+proj=utm +zone=33 +ellps=GRS80 +units=m +no_defs" },
94
+ // Plain WGS84 UTM zones 32N/33N (EPSG:326xx) — the generic, non-European-specific
95
+ // code many CityJSON files declare instead of the ETRS89 pair above (e.g.
96
+ // Ingolstadt's own open-data LoD3 export). Numerically near-identical to
97
+ // 25832/25833 at map-rendering precision (WGS84 vs. ETRS89 differ by ~cm in
98
+ // Europe), so no +towgs84 shift is needed — same bare projection string.
99
+ 32632: { def: "+proj=utm +zone=32 +datum=WGS84 +units=m +no_defs" },
100
+ 32633: { def: "+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs" },
101
+ // Austria — MGI / Austria GK West/Central/East (the national cadastral
102
+ // system's three meridian-strip zones; Vienna sits in GK East). Same
103
+ // Bessel ellipsoid + towgs84 shift and y_0=-5,000,000 false-northing
104
+ // convention across all three — proj4 strings verified against epsg.io,
105
+ // not hand-derived, and cross-checked by reprojecting a real Vienna
106
+ // open-data file's own extent and confirming it lands in Vienna.
107
+ 31254: { def: "+proj=tmerc +lat_0=0 +lon_0=10.3333333333333 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs" },
108
+ 31255: { def: "+proj=tmerc +lat_0=0 +lon_0=13.3333333333333 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs" },
109
+ 31256: { def: "+proj=tmerc +lat_0=0 +lon_0=16.3333333333333 +k=1 +x_0=0 +y_0=-5000000 +ellps=bessel +towgs84=577.326,90.129,463.919,5.137,1.474,5.297,2.4232 +units=m +no_defs" },
110
+ // Singapore — SVY21 / Singapore TM (the national grid; NUS's own Singapore
111
+ // HDB buildings dataset declares this via metadata.referenceSystem
112
+ // directly, unlike the Austria/3DBAG files, which had none). proj4 string
113
+ // verified against epsg.io, cross-checked by reprojecting a real dataset's
114
+ // own extent and confirming it lands in Singapore (103.68-103.99°E,
115
+ // 1.27-1.46°N — matches).
116
+ 3414: { def: "+proj=tmerc +lat_0=1.36666666666667 +lon_0=103.833333333333 +k=1 +x_0=28001.642 +y_0=38744.572 +ellps=WGS84 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs" },
117
+ // Japan — JGD2011 geographic 2D. EPSG axis order is (lat, lon).
118
+ 6668: { def: "+proj=longlat +ellps=GRS80 +no_defs", geographic: !0, swapXY: !0 },
119
+ // WGS 84. Files in the wild store lon/lat despite EPSG's formal (lat, lon).
120
+ 4326: { def: "+proj=longlat +datum=WGS84 +no_defs", geographic: !0 },
121
+ 3857: { def: "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +no_defs" }
122
+ };
123
+ for (let t = 0; t < G.length; t++) {
124
+ const [e, n] = G[t];
125
+ j[6669 + t] = {
126
+ def: `+proj=tmerc +lat_0=${e} +lon_0=${n} +k=0.9999 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs`,
127
+ swapXY: !0
128
+ };
129
+ }
130
+ const Q = /EPSG.*?(\d{4,6})\D*$/i;
131
+ function ee(t) {
132
+ if (!t) return;
133
+ const e = Number(Q.exec(t)?.[1]);
134
+ return Number.isFinite(e) ? e : void 0;
135
+ }
136
+ let x = !1;
137
+ async function k(t, e) {
138
+ const n = { toLngLat: (r, f) => [r, f], projected: !1 }, o = ee(t);
139
+ if (o === void 0)
140
+ return x || (x = !0, console.warn(
141
+ `[onlymapjs] data="${e}": CityJSON has no metadata.referenceSystem — assuming coordinates are already WGS84 lon/lat. Add a referenceSystem to the file if they are not.`
142
+ )), n;
143
+ const s = K[o] ?? o, a = j[s];
144
+ if (!a)
145
+ throw new Error(
146
+ `CityJSON declares EPSG:${o}${s === o ? "" : ` (horizontal component EPSG:${s})`}, which is not one of the bundled coordinate systems (${Object.keys(j).join(", ")}). Reproject the file to EPSG:4326 server-side (cjio --reproject 4326), or claim the URL with OmMap.registerFormat() and supply your own projection.`
147
+ );
148
+ if (a.geographic)
149
+ return a.swapXY ? { toLngLat: (r, f) => [f, r], projected: !1 } : n;
150
+ const i = (await import("./index-CW1n5LdO.js")).default, c = i(a.def, "+proj=longlat +datum=WGS84 +no_defs");
151
+ return {
152
+ toLngLat: (r, f) => {
153
+ const [l, p] = c.forward(a.swapXY ? [f, r] : [r, f]);
154
+ return [l, p];
155
+ },
156
+ projected: !0
157
+ };
158
+ }
159
+ function F(t) {
160
+ let e = 0, n = 0, o = 0;
161
+ for (let s = 0; s < t.length; s++) {
162
+ const a = t[s], i = t[(s + 1) % t.length];
163
+ e += (a[1] - i[1]) * (a[2] + i[2]), n += (a[2] - i[2]) * (a[0] + i[0]), o += (a[0] - i[0]) * (a[1] + i[1]);
164
+ }
165
+ return Math.hypot(e, n, o) / 2;
166
+ }
167
+ function te(t) {
168
+ const e = t.slice().sort((o, s) => o - s), n = e.length >> 1;
169
+ return e.length % 2 ? e[n] : (e[n - 1] + e[n]) / 2;
170
+ }
171
+ function U(t, e, n, o, s) {
172
+ const a = q(e.geometry, s);
173
+ if (!a) return;
174
+ const i = A(a);
175
+ if (i.length === 0) return;
176
+ const c = X(a, i.length), r = i.map(
177
+ (d) => d.map((m) => m.map((u) => n[u]).filter((u) => u !== void 0))
178
+ );
179
+ let f = 1 / 0, l = -1 / 0;
180
+ for (const d of r)
181
+ for (const m of d)
182
+ for (const [, , u] of m)
183
+ u < f && (f = u), u > l && (l = u);
184
+ if (!Number.isFinite(f)) return;
185
+ const p = f, g = c.some((d) => d === "RoofSurface"), y = [];
186
+ let _ = 1 / 0, h = -1 / 0;
187
+ for (let d = 0; d < r.length; d++)
188
+ if (!(g && c[d] !== "RoofSurface"))
189
+ for (const m of r[d])
190
+ for (const [, , u] of m)
191
+ (g || u > p + 0.5) && (y.push(u), u < _ && (_ = u), u > h && (h = u));
192
+ y.length === 0 && (y.push(l), _ = l, h = l);
193
+ let S = c.indexOf("GroundSurface");
194
+ if (S < 0) {
195
+ let d = -1;
196
+ for (let m = 0; m < r.length; m++) {
197
+ const u = r[m][0];
198
+ if (!u || u.length < 3) continue;
199
+ const b = u.every(([, , v]) => v <= p + 0.5), w = F(u) * (b ? 1e3 : 1);
200
+ w > d && (d = w, S = m);
201
+ }
202
+ }
203
+ if (S < 0) return;
204
+ const O = r[S];
205
+ if (!O[0] || O[0].length < 3) return;
206
+ const [, B] = o.toLngLat(O[0][0][0], O[0][0][1]), Z = 111320 * Math.cos(B * Math.PI / 180), W = 110540, Y = (d) => o.projected ? d : d.map(([m, u, b]) => {
207
+ const [w, v] = o.toLngLat(m, u);
208
+ return [w * Z, v * W, b];
209
+ });
210
+ let R = 0, E = 0;
211
+ for (let d = 0; d < r.length; d++) {
212
+ if (g && c[d] !== "RoofSurface") continue;
213
+ const m = r[d][0];
214
+ if (!m || m.length < 3) continue;
215
+ const u = F(Y(m));
216
+ R += u, E += u * (m.reduce((b, [, , w]) => b + w, 0) / m.length);
217
+ }
218
+ const V = R > 0 ? E / R : te(y), H = {
219
+ cityobject_id: t,
220
+ cityobject_type: e.type ?? "Unknown",
221
+ ...e.parents?.[0] !== void 0 ? { parent_id: e.parents[0] } : {},
222
+ ...a.lod !== void 0 ? { lod: String(a.lod) } : {},
223
+ ground_height: C(p),
224
+ roof_height: C(V - p),
225
+ eaves_height: C(_ - p),
226
+ ridge_height: C(h - p),
227
+ roof_area: C(R),
228
+ surface_count: i.length
229
+ };
230
+ return { faceRings: r, semantics: c, footprintIndex: S, derived: H };
231
+ }
232
+ function ne(t, e, n, o, s, a) {
233
+ const i = U(t, e, n, o, a);
234
+ if (!i) return;
235
+ const r = i.faceRings[i.footprintIndex].filter((f) => f.length >= 3).map((f) => {
236
+ const l = f.map(([p, g]) => o.toLngLat(p, g));
237
+ return l.push(l[0]), l;
238
+ });
239
+ return {
240
+ type: "Feature",
241
+ id: t,
242
+ geometry: { type: "Polygon", coordinates: r },
243
+ // Source attributes first: the derived names are the ones manifests bind
244
+ // to, so they must be deterministic even against a same-named source field.
245
+ properties: { ...s, ...e.attributes, ...i.derived }
246
+ };
247
+ }
248
+ const I = {
249
+ GroundSurface: "#999999",
250
+ WallSurface: "#ffffff",
251
+ RoofSurface: "#ff0000",
252
+ TrafficArea: "#6e6e6e",
253
+ AuxiliaryTrafficArea: "#2c8200",
254
+ Window: "#0059ff",
255
+ Door: "#640000"
256
+ }, M = {
257
+ Building: "#7497df",
258
+ BuildingPart: "#7497df",
259
+ BuildingInstallation: "#7497df",
260
+ Bridge: "#999999",
261
+ BridgePart: "#999999",
262
+ BridgeInstallation: "#999999",
263
+ BridgeConstructiveElement: "#999999",
264
+ CityObjectGroup: "#ffffb3",
265
+ LandUse: "#ffffb3",
266
+ CityFurniture: "#cc0000",
267
+ GenericCityObject: "#cc0000",
268
+ PlantCover: "#39ac39",
269
+ SolitaryVegetationObject: "#39ac39",
270
+ Railway: "#000000",
271
+ Road: "#999999",
272
+ TransportSquare: "#999999",
273
+ TINRelief: "#ffdb99",
274
+ Tunnel: "#999999",
275
+ TunnelPart: "#999999",
276
+ TunnelInstallation: "#999999",
277
+ WaterBody: "#4da6ff"
278
+ }, oe = "#b0b0b0";
279
+ function re(t, e) {
280
+ return t && I[t] ? I[t] : e && M[e] ? M[e] : oe;
281
+ }
282
+ function ie(t, e, n, o, s, a) {
283
+ const i = U(t, e, n, o, a);
284
+ if (!i) return [];
285
+ const c = [];
286
+ for (let r = 0; r < i.faceRings.length; r++) {
287
+ const f = i.faceRings[r], l = f[0];
288
+ if (!l || l.length < 3) continue;
289
+ const p = f.filter((y) => y.length >= 3).map((y) => y.map(([_, h, S]) => [...o.toLngLat(_, h), S])), g = p[0];
290
+ c.push({
291
+ ...s,
292
+ ...e.attributes,
293
+ ...i.derived,
294
+ surface_type: i.semantics[r],
295
+ fill_color: re(i.semantics[r], e.type),
296
+ polygon: p,
297
+ outline: [...g, g[0]]
298
+ });
299
+ }
300
+ return c;
301
+ }
302
+ function C(t) {
303
+ return Number.isFinite(t) ? Math.round(t * 1e3) / 1e3 : 0;
304
+ }
305
+ function D(t, e, n, o, s = "footprint") {
306
+ if (!t) return [];
307
+ const a = (c) => Array.isArray(c.children) && c.children.some((r) => {
308
+ const f = t[r];
309
+ return f?.type === `${c.type}Part` && (f.geometry?.some((l) => A(l).length > 0) ?? !1);
310
+ }), i = [];
311
+ for (const [c, r] of Object.entries(t)) {
312
+ if (!r || typeof r != "object" || a(r)) continue;
313
+ const l = (r.parents?.[0] ? t[r.parents[0]] : void 0)?.attributes ?? {};
314
+ if (s === "surfaces")
315
+ i.push(...ie(c, r, e, n, l, o));
316
+ else {
317
+ const p = ne(c, r, e, n, l, o);
318
+ p && i.push(p);
319
+ }
320
+ }
321
+ return i;
322
+ }
323
+ function N(t, e, n) {
324
+ if (t?.type !== n)
325
+ throw new Error(
326
+ `expected a ${n} object but found type="${t?.type ?? "undefined"}" — this does not look like CityJSON. If it is a different newline-delimited format, claim the URL with OmMap.registerFormat().`
327
+ );
328
+ }
329
+ function $(t) {
330
+ const e = /[?&]om-lod=([^&#]+)/.exec(t)?.[1];
331
+ if (e === void 0) return;
332
+ const n = Number.parseFloat(decodeURIComponent(e));
333
+ if (!Number.isFinite(n)) {
334
+ console.warn(`[onlymapjs] om-lod="${e}" is not a number — ignoring and using the highest available LoD.`);
335
+ return;
336
+ }
337
+ return n;
338
+ }
339
+ function z(t) {
340
+ const e = /[?&]om-surfaces=([^&#]+)/.exec(t)?.[1];
341
+ if (e === void 0) return !1;
342
+ const n = decodeURIComponent(e).toLowerCase();
343
+ return n === "1" || n === "true";
344
+ }
345
+ async function ce(t, e) {
346
+ const n = await t.json();
347
+ N(n, e, "CityJSON");
348
+ const o = await k(n.metadata?.referenceSystem, e), s = P(n.vertices, n.transform), a = z(e) ? "surfaces" : "footprint";
349
+ return D(n.CityObjects, s, o, $(e), a);
350
+ }
351
+ const se = 250, ae = 512;
352
+ async function* fe(t) {
353
+ if (!t.body) {
354
+ for (const s of (await t.text()).split(`
355
+ `)) yield s;
356
+ return;
357
+ }
358
+ const e = t.body.getReader(), n = new TextDecoder();
359
+ let o = "";
360
+ for (; ; ) {
361
+ const { done: s, value: a } = await e.read();
362
+ if (s) break;
363
+ o += n.decode(a, { stream: !0 });
364
+ let i;
365
+ for (; (i = o.indexOf(`
366
+ `)) >= 0; )
367
+ yield o.slice(0, i), o = o.slice(i + 1);
368
+ }
369
+ o += n.decode(), o.length > 0 && (yield o);
370
+ }
371
+ async function le(t, e, n) {
372
+ const o = $(e), s = z(e) ? "surfaces" : "footprint";
373
+ let a, i, c = !1;
374
+ const r = [];
375
+ let f = 0, l = 0;
376
+ for await (const p of fe(t)) {
377
+ const g = p.trim();
378
+ if (g.length === 0) continue;
379
+ const y = JSON.parse(g);
380
+ if (c)
381
+ N(y, e, "CityJSONFeature");
382
+ else if (N(y, e, "CityJSON"), i = y.transform, a = await k(y.metadata?.referenceSystem, e), c = !0, y.CityObjects && Object.keys(y.CityObjects).length === 0) continue;
383
+ const _ = P(y.vertices, i);
384
+ r.push(...D(y.CityObjects, _, a, o, s)), f++;
385
+ const h = performance.now();
386
+ (f >= ae || h - l >= se) && (f = 0, l = h, n(r.slice()));
387
+ }
388
+ if (!c)
389
+ throw new Error(
390
+ 'CityJSONSeq file is empty or has no header line — the first line must be a CityJSON object carrying "transform" and "metadata", followed by one CityJSONFeature per line.'
391
+ );
392
+ return r;
393
+ }
394
+ export {
395
+ q as chooseGeometry,
396
+ D as decodeCityObjects,
397
+ P as decompressVertices,
398
+ re as defaultFillColor,
399
+ X as faceSemantics,
400
+ A as flattenToFaces,
401
+ ce as parseCityJsonDocument,
402
+ le as parseCityJsonSeq,
403
+ ee as parseEpsgCode,
404
+ $ as parseLodPin,
405
+ z as parseSurfacesFlag,
406
+ k as resolveCrs
407
+ };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * CityJSON / CityJSONSeq ingestion (spec: "CityJSON ingestion"; issue #25).
3
+ *
4
+ * A LAZY chunk — reached only through `CITYJSON_FORMAT`'s dynamic import in
5
+ * data-layer.ts, so neither this decoder nor proj4 weighs on the eager
6
+ * bundle (the src/raster.ts precedent). proj4 itself is imported lazily
7
+ * AGAIN inside `resolveCrs`, so a file already in lon/lat never pays for it.
8
+ *
9
+ * Two output shapes share one geometry computation (`computeCityObjectGeometry`):
10
+ *
11
+ * - DEFAULT — one GeoJSON `Feature` per CityObject: a 2D footprint polygon
12
+ * plus DERIVED height properties, rendered through GeoJsonLayer's
13
+ * `extruded` + `get-elevation`. Lit, pickable, GPU-filterable,
14
+ * terrain-aware — and, because extrusion is a flat-topped prism, it
15
+ * cannot draw the actual shape of a pitched/hipped LoD2.2 roof no matter
16
+ * how accurate the derived height is.
17
+ * - `?om-surfaces=1` — one flat row PER FACE (wall, roof, ground), each
18
+ * carrying its real 3D ring coordinates (real per-vertex height, not a
19
+ * uniform extrusion) for `SolidPolygonLayer` with `extruded="false"` and
20
+ * `full3d`. This draws the true reconstructed shape. The tradeoff: deck's
21
+ * solid-polygon vertex shader computes lighting only inside its
22
+ * `if (solidPolygon.extruded)` branch (verified against
23
+ * solid-polygon-layer-vertex-main.glsl.js), so this path is unavoidably
24
+ * unlit — flat per-face fill color, no shading from surface orientation.
25
+ * `_full3d` is what makes near-vertical wall polygons triangulate at all:
26
+ * without it earcut works in the flat xy plane, where a wall's projected
27
+ * area is ~zero and the face silently vanishes.
28
+ *
29
+ * Semantics are not discarded in either mode: they drive which vertices
30
+ * count as roof (so the derived heights are real roof heights, not
31
+ * bounding-box heights) and survive as a `surface_type` per-face property in
32
+ * surfaces mode.
33
+ */
34
+ /** [x, y, z] in the source CRS — decompressed, not yet reprojected. */
35
+ type Vec3 = [number, number, number];
36
+ /** A face is a list of rings; ring[0] is the outer boundary, the rest are holes. */
37
+ type Face = number[][];
38
+ interface CityJsonTransform {
39
+ scale?: number[];
40
+ translate?: number[];
41
+ }
42
+ interface CityJsonSemantics {
43
+ surfaces?: {
44
+ type?: string;
45
+ }[];
46
+ values?: unknown;
47
+ }
48
+ interface CityJsonGeometry {
49
+ type?: string;
50
+ lod?: string | number;
51
+ boundaries?: unknown;
52
+ semantics?: CityJsonSemantics;
53
+ }
54
+ interface CityObject {
55
+ type?: string;
56
+ attributes?: Record<string, unknown>;
57
+ geometry?: CityJsonGeometry[];
58
+ parents?: string[];
59
+ children?: string[];
60
+ }
61
+ export declare function decompressVertices(vertices: number[][] | undefined, transform: CityJsonTransform | undefined): Vec3[];
62
+ /**
63
+ * Geometry → flat face list. Surface-less geometry types (points, lines) and
64
+ * anything unrecognized yield no faces, so the CityObject is skipped rather
65
+ * than emitted with a broken footprint.
66
+ */
67
+ export declare function flattenToFaces(geometry: CityJsonGeometry): Face[];
68
+ /** One semantic surface type per face (`"RoofSurface"`, …), aligned by index. */
69
+ export declare function faceSemantics(geometry: CityJsonGeometry, faceCount: number): (string | undefined)[];
70
+ /**
71
+ * Highest-LoD geometry that actually has surfaces. Higher LoD wins even though
72
+ * the output is an extrusion: an LoD2 solid carries real roof vertices and
73
+ * semantics, so its derived heights beat LoD1's single stated height.
74
+ * `?om-lod=` pins a specific one (see `parseCityJsonDocument`).
75
+ */
76
+ export declare function chooseGeometry(geometries: CityJsonGeometry[] | undefined, lodPin?: number): CityJsonGeometry | undefined;
77
+ /** `"https://www.opengis.net/def/crs/EPSG/0/7415"` / `"urn:ogc:def:crs:EPSG::7415"` / `"EPSG:7415"` → 7415. */
78
+ export declare function parseEpsgCode(referenceSystem: string | undefined): number | undefined;
79
+ /** Source [x, y] → WGS84 [lon, lat]. */
80
+ export interface CrsAdapter {
81
+ toLngLat(x: number, y: number): [number, number];
82
+ /**
83
+ * True when source coordinates are already metres (a projected grid), so
84
+ * horizontal areas can be measured on them directly. Geographic sources are
85
+ * in degrees and need a local metric frame first — see `buildFeature`.
86
+ */
87
+ projected: boolean;
88
+ }
89
+ /**
90
+ * Build the reprojection adapter for a document. An unsupported EPSG code
91
+ * throws rather than silently misplacing a whole city — the failure surfaces
92
+ * through the Data Layer's own `Failed to fetch data="…"` path, the same as
93
+ * any other format error.
94
+ */
95
+ export declare function resolveCrs(referenceSystem: string | undefined, url: string): Promise<CrsAdapter>;
96
+ /** Derived property names. Documented as reserved — they win over same-named source attributes. */
97
+ export interface DerivedProperties {
98
+ cityobject_id: string;
99
+ cityobject_type: string;
100
+ parent_id?: string;
101
+ lod?: string;
102
+ ground_height: number;
103
+ roof_height: number;
104
+ eaves_height: number;
105
+ ridge_height: number;
106
+ roof_area: number;
107
+ surface_count: number;
108
+ }
109
+ export declare function defaultFillColor(surfaceType: string | undefined, cityObjectType: string | undefined): string;
110
+ /**
111
+ * CityObjects → features (default) or per-face surface rows (`mode:
112
+ * "surfaces"`, see `parseSurfacesFlag`). Geometry usually hangs off child
113
+ * objects (`BuildingPart`, `BridgePart`), so a parent's attributes are
114
+ * inherited by its children — the child's own values win, and `parent_id`
115
+ * keeps the hierarchy walkable for selection.
116
+ */
117
+ export declare function decodeCityObjects(cityObjects: Record<string, CityObject> | undefined, vertices: Vec3[], crs: CrsAdapter, lodPin: number | undefined, mode?: "footprint" | "surfaces"): unknown[];
118
+ /** `?om-lod=1.2` → 1.2. The DataFormat contract sees only the URL, so this is the option channel. */
119
+ export declare function parseLodPin(url: string): number | undefined;
120
+ /**
121
+ * `?om-surfaces=1` → per-face `SolidPolygonLayer` rows instead of the default
122
+ * footprint-extrusion `GeoJsonLayer` Features. Same option-channel idiom as
123
+ * `om-lod` — and the same URL-keyed cache benefit: a plain URL and its
124
+ * `?om-surfaces=1` counterpart parse and cache independently, so one manifest
125
+ * can point a `GeoJsonLayer` at one and a `SolidPolygonLayer` at the other.
126
+ */
127
+ export declare function parseSurfacesFlag(url: string): boolean;
128
+ /** A whole `.city.json` document. One JSON parse — a single JSON value has no incremental path. */
129
+ export declare function parseCityJsonDocument(res: Response, url: string): Promise<unknown[]>;
130
+ /**
131
+ * Streaming `.city.jsonl`. `push` hands the Data Layer a fresh array reference
132
+ * every batch so deck.gl's shallow `data` diff fires and the city fills in as
133
+ * it downloads; the returned array is the complete set.
134
+ */
135
+ export declare function parseCityJsonSeq(res: Response, url: string, push: (features: unknown[]) => void): Promise<unknown[]>;
136
+ /** Test hook — resets the once-per-page missing-CRS warning. */
137
+ export declare function resetCityJsonWarningsForTests(): void;
138
+ export {};
@@ -70,6 +70,16 @@ export interface DataFormat {
70
70
  match(url: string, contentType?: string): boolean;
71
71
  /** Parse the claimed response. The parser module should be lazy-imported here (HU1 — the apache-arrow precedent). `url` is the requested URL (res.url is empty on constructed Responses — test fetches, service workers). */
72
72
  parse(res: Response, url: string): Promise<LayerData>;
73
+ /**
74
+ * Optional progressive variant for record-per-line formats (CityJSONSeq):
75
+ * `push` publishes a partial snapshot mid-download, taking the same
76
+ * cache-swap + notify path a stream flush takes, so the map fills in as
77
+ * bytes arrive instead of staying empty until EOF. Pass a FRESH array
78
+ * reference each time — deck.gl diffs `data` by identity. When present this
79
+ * is used instead of `parse`; the returned value is the complete dataset.
80
+ * `push` is a no-op during a `refresh` re-fetch — see getData for why.
81
+ */
82
+ parseIncremental?(res: Response, url: string, push: (partial: LayerData) => void): Promise<LayerData>;
73
83
  }
74
84
  /**
75
85
  * Row objects → ColumnarData (the memory-efficient shape: one array per