@nika-js/onlymap 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +3 -1
- package/bin/onlymapjs.mjs +236 -0
- package/dist/{LercDecode.es-DEucX1sB.js → LercDecode.es-DYwn8YTS.js} +1 -1
- package/dist/{basemap-CmEvmtwF.js → basemap-CtEc5BCi.js} +1 -1
- package/dist/elements/om-map.d.ts +8 -0
- package/dist/{geoparquet-DL58Htx5.js → geoparquet-RBC0ZJNm.js} +1 -1
- package/dist/{index-ec-8juRl.js → index-Bwu1DFHP.js} +1 -1
- package/dist/{index-CNili_G3.js → index-CJ0pib8I.js} +2 -2
- package/dist/{index-NkRgxbAW.js → index-CiGUw6GD.js} +1 -1
- package/dist/{index-BRdaiH3a.js → index-DkimKgOm.js} +3321 -3295
- package/dist/{index-DrTKLcxB.js → index-HrEJtZoQ.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/{lerc-BPTGJlrt.js → lerc-BIQMg0FV.js} +2 -2
- package/dist/onlymap.standalone.js +3786 -3760
- package/dist/onlymapjs.css +1 -1
- package/dist/onlymapjs.js +89 -87
- package/dist/{raster-lkF1eUo1.js → raster-Dsugvobw.js} +3 -3
- package/dist/{raster-pipeline-B4N48UQX.js → raster-pipeline-rqJJRCEo.js} +1 -1
- package/dist/react/context.d.ts +2 -0
- package/dist/react.js +244 -236
- package/dist/version.d.ts +1 -1
- package/dist/widget-layout.d.ts +21 -0
- package/dist/{zarr-B-l3yHWb.js → zarr-CRysSFhD.js} +2 -2
- package/llms.txt +5 -3
- package/package.json +1 -1
- package/skills/onlymapjs/SKILL.md +3 -2
- package/skills/onlymapjs/references/syntax.md +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,19 @@ Note: npm collapsed a few closely-spaced releases — the GPX/FlatGeobuf (0.5.4)
|
|
|
8
8
|
and GeoParquet (0.5.5) work shipped to npm together as **0.5.6**, so npm's
|
|
9
9
|
version list jumps 0.5.3 → 0.5.6. Each logical version is listed here regardless.
|
|
10
10
|
|
|
11
|
+
## 0.7.5 — 2026-09-11
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **Share any map as one file: `npx @nika-js/onlymap export map.html`.** Writes a portable copy that opens on any computer — relative data is fetched and embedded with format detection preserved (GeoJSON, CSV, Arrow, FlatGeobuf, GeoParquet and friends), library references become pinned CDN tags, and a no-JS fallback is added if missing. Warns when embedded data is heavy, and says exactly which sources must stay network-hosted (tile streams, COGs, shapefiles) and why.
|
|
15
|
+
|
|
16
|
+
## 0.7.4 — 2026-09-04
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- **A lone side widget no longer folds into the drawer so early.** Widgets fold based on what actually shares their row now: the 640px fold width applies when a row (top, middle, or bottom) has widgets on more than one side, and drops to 416px when every occupied row is one-sided — so a single legend or control panel survives narrower maps before tucking away. Adding an opposing widget re-applies the wider threshold immediately.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **The "requires JavaScript" banner no longer flashes during a slow load.** On modern browsers the stylesheet now distinguishes "JavaScript is off" from "still loading": with scripting available the fallback never appears during a normal boot (and only surfaces, with load-failure wording, if the library genuinely cannot load), while genuinely script-free contexts — file previews, email attachments — see the fallback instantly instead of after a delay.
|
|
23
|
+
|
|
11
24
|
## 0.7.3 — 2026-09-03
|
|
12
25
|
|
|
13
26
|
### Added
|
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ A handful of elements, one rule: **attributes are kebab-case versions of deck.gl
|
|
|
84
84
|
|---|---|
|
|
85
85
|
| `<om-map>` | The map. `center`, `zoom`, `pitch`, `bearing`; `basemap` takes a free preset (`positron`, `liberty`, `dark-matter`, `osm`, …), a style URL, or `"none"` (standalone canvas) — and switches **live**; `validate` for a live on-page error panel. Give it a height: a custom element is `display:inline` by default, so the library injects a `display:block` default (fills a sized parent, else a 400px floor) to keep a bare map visible, but set an explicit height (`om-map { height: 100vh }`) for real layout — any height you set wins outright, including one below the floor. A map that still collapses warns in the console; `hidden` and `display:none` maps stay hidden and stay quiet. |
|
|
86
86
|
| `<om-layer>` | Any of **39 layer types** by name — all of deck.gl's core, geo, aggregation, and mesh layers (Scatterplot, GeoJson, Arc, Path, Heatmap, Hexagon, Trips, Tile, Tile3D, Scenegraph, …) plus the built-in `PopupLayer` for WebGL badges/labels at scale, the native `COGLayer` for GeoTIFF rasters, `ZarrLayer` for chunked Zarr/GeoZarr rasters, `ImageOverlay` for georeferenced drone JPEGs, `BIMLayer` for BIM source files (`.ifc` today) loaded straight in the browser with no pre-conversion step, and `Route`/`Tracking` for styled routes and live position tracking. `id` required; `label`/`color` feed the legend. `visible-zoom-range="[8, 14]"` gives any layer scale-dependent visibility (hidden outside min ≤ zoom < max, the standard minzoom/maxzoom convention) — the everyday GIS pattern for keeping a dense layer from swamping a zoomed-out view. |
|
|
87
|
-
| `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `clip-box`, `zoom-controls`, `undo-redo`, `scale-bar` (metric/imperial/nautical `units`), `attribution`, `filter`, `draw` (point/line/polygon sketch capture, GeoJSON save/autosave; lines/polygons complete via double-click, touch double-tap — detected by the library itself, since iOS WebViews never synthesize `dblclick` from taps — Enter, or the toolbar's Finish button; `export-3d` adds an "Export 3D" button that clips loaded `Tile3DLayer`/`BIMLayer` content to the drawn footprint and downloads it as a portable GLB — re-framed to a local coordinate frame at the footprint's own centroid, each triangle carrying its own source color as vertex colors, no textures — or, with `export-3d="b3dm"`, the same mesh wrapped for Cesium/3D-Tiles pipelines; only currently-visible 3D Tiles/BIM layers are included — a layer hidden via `visible="false"` or the `toggle-layer` action is skipped, with a distinct console warning for "nothing loaded" vs. "everything hidden"), `measure` (geodesic distance + area + cut/fill volume — live labels, `units` toggle, an `om-measure` readout event; `modes="distance area volume"` adds a footprint-then-extrude tool: outline a polygon, double-click (double-tap on touch) to close it — it turns solid teal, ready — then drag the double-headed arrow gizmo that appears at its centroid up to fill or down to cut, reading out Cut/Fill/Net (signed, fill−cut)/Total (unsigned, cut+fill) volume plus Area/Perimeter, each labelled with its own sign convention. The math is a REAL per-cell grid integration: closing a footprint bulk-loads its covering DEM tiles, lays a metric grid in a local tangent frame at the ring centroid (cell size = the DEM's ground-sample distance; scanline point-in-polygon; bilinear, tile-seam-correct sampling; worker-offloaded), and integrates terrain-vs-base per cell — mixed cut AND fill within one footprint on undulating ground, with a published ±error (per-cell cellArea × 1.5 × GSD, summed per side), the cell size used, and a no-data warning all carried on the `om-measure` readout (`cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3`/`nodataFraction`). A `base-surface` attribute picks the reference surface: `custom` (default — the gizmo's draggable target plane), or boundary-derived stockpile strategies with no gizmo (`triangulated` boundary TIN, `plane` least-squares fit, `lowest`/`highest`/`average`). On `custom`, the extruded prism's own boundary hugs each corner's real ground elevation by default (so it doesn't visibly float above or sink below sloped terrain) — a "Flat target plane" toggle button appears above the readout once a footprint closes, switching that rendering to a single level plane instead (a rendering choice only; the underlying cut/fill numbers were already computed against one flat target elevation either way). Requires `terrain` on `<om-map>` — validation warns if `volume` is in `modes` with none (without terrain a flat-plane fallback runs, with no error figures — nothing honest to quote). A `profile` attribute alongside `modes` — not a mode of its own — samples elevation around a volume footprint's own perimeter as it's drawn and closed, live, and dispatches it on `om-measure`'s `profileSeries` field for a `dynamic-chart` widget to plot. Each sample is `{x: distance-from-start-m, y: elevation-m}`, and samples that ARE one of the drawn footprint's own corners additionally carry `vertexIndex` (0-based, in draw order) — so a chart can mark the real corners instead of every interpolated sample, and the widget's built-in profile chart marks each corner, labelling the first `1 · Start`. The map badges the first two vertices in draw order (`1 · Start`, `2`), which states the ring's direction outright — a start marker alone leaves clockwise vs counter-clockwise ambiguous, and the two wind to mirror-image profiles. Two badges is the minimum that fixes a direction and a constant cost regardless of how many corners the footprint has. Also volume-only: `deadband` (m³) zeroes out a Cut/Fill figure below the threshold, filtering drag noise near zero height (Cut/Fill/Net/Total are always RAW geometric volumes — deliberately unaffected by `swell`/`shrink`, so they keep answering "does this reach target elevation" regardless of what material's configured); `density` (t/m³ metric, lb/yd³ imperial) and `swell`/`shrink` (multipliers, default 1×) instead populate a separate Material section, standard Bank/Loose/Compacted earthworks convention — Adjusted Cut = raw × swell (loose/haul volume, bigger — excavating adds air voids), Adjusted Fill = raw ÷ shrink (loose/borrow volume needed, also bigger — the raw fill is already the compacted target void), plus Cut/Fill tonnage computed from the raw (not adjusted) volume, since swell/shrink change volume, not mass; the section only appears once at least one of `density`/`swell`/`shrink` is actually configured, no separate toggle. A `stale` field on the `om-measure` readout flags the brief window between a footprint committing and its elevation sample resolving, so a consumer doesn't read numbers left over from a prior footprint as current), `vega-lite` (live charts bound to a layer's data), `dynamic-chart` (the same Vega-Lite rendering, but data-driven by a live DOM event instead of a layer — `on="<event-name>"` + `series-field="<name>"` reads `event.detail[seriesField]` as the chart's `values` on every matching event, redrawing at a fixed `width`; a feature "freezes" it for free by simply omitting that field the next time it fires, no separate pause API needed), and the BIM set `ifc-browser` / `feature-inspector` / `ifc-loader` / `ifc-clash`. Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all authored chrome without destroying it — provider attribution and the license badge never hide. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). At map widths
|
|
87
|
+
| `<om-widget>` | UI panels. Built-ins: `legend` (symbology-aware: color scales render as gradient ramps or class ranges, categorical ternaries as discrete palettes), `layer-switcher`, `basemap-switcher`, `lighting`, `clip-box`, `zoom-controls`, `undo-redo`, `scale-bar` (metric/imperial/nautical `units`), `attribution`, `filter`, `draw` (point/line/polygon sketch capture, GeoJSON save/autosave; lines/polygons complete via double-click, touch double-tap — detected by the library itself, since iOS WebViews never synthesize `dblclick` from taps — Enter, or the toolbar's Finish button; `export-3d` adds an "Export 3D" button that clips loaded `Tile3DLayer`/`BIMLayer` content to the drawn footprint and downloads it as a portable GLB — re-framed to a local coordinate frame at the footprint's own centroid, each triangle carrying its own source color as vertex colors, no textures — or, with `export-3d="b3dm"`, the same mesh wrapped for Cesium/3D-Tiles pipelines; only currently-visible 3D Tiles/BIM layers are included — a layer hidden via `visible="false"` or the `toggle-layer` action is skipped, with a distinct console warning for "nothing loaded" vs. "everything hidden"), `measure` (geodesic distance + area + cut/fill volume — live labels, `units` toggle, an `om-measure` readout event; `modes="distance area volume"` adds a footprint-then-extrude tool: outline a polygon, double-click (double-tap on touch) to close it — it turns solid teal, ready — then drag the double-headed arrow gizmo that appears at its centroid up to fill or down to cut, reading out Cut/Fill/Net (signed, fill−cut)/Total (unsigned, cut+fill) volume plus Area/Perimeter, each labelled with its own sign convention. The math is a REAL per-cell grid integration: closing a footprint bulk-loads its covering DEM tiles, lays a metric grid in a local tangent frame at the ring centroid (cell size = the DEM's ground-sample distance; scanline point-in-polygon; bilinear, tile-seam-correct sampling; worker-offloaded), and integrates terrain-vs-base per cell — mixed cut AND fill within one footprint on undulating ground, with a published ±error (per-cell cellArea × 1.5 × GSD, summed per side), the cell size used, and a no-data warning all carried on the `om-measure` readout (`cellSizeM`/`gsdM`/`cutErrorM3`/`fillErrorM3`/`nodataFraction`). A `base-surface` attribute picks the reference surface: `custom` (default — the gizmo's draggable target plane), or boundary-derived stockpile strategies with no gizmo (`triangulated` boundary TIN, `plane` least-squares fit, `lowest`/`highest`/`average`). On `custom`, the extruded prism's own boundary hugs each corner's real ground elevation by default (so it doesn't visibly float above or sink below sloped terrain) — a "Flat target plane" toggle button appears above the readout once a footprint closes, switching that rendering to a single level plane instead (a rendering choice only; the underlying cut/fill numbers were already computed against one flat target elevation either way). Requires `terrain` on `<om-map>` — validation warns if `volume` is in `modes` with none (without terrain a flat-plane fallback runs, with no error figures — nothing honest to quote). A `profile` attribute alongside `modes` — not a mode of its own — samples elevation around a volume footprint's own perimeter as it's drawn and closed, live, and dispatches it on `om-measure`'s `profileSeries` field for a `dynamic-chart` widget to plot. Each sample is `{x: distance-from-start-m, y: elevation-m}`, and samples that ARE one of the drawn footprint's own corners additionally carry `vertexIndex` (0-based, in draw order) — so a chart can mark the real corners instead of every interpolated sample, and the widget's built-in profile chart marks each corner, labelling the first `1 · Start`. The map badges the first two vertices in draw order (`1 · Start`, `2`), which states the ring's direction outright — a start marker alone leaves clockwise vs counter-clockwise ambiguous, and the two wind to mirror-image profiles. Two badges is the minimum that fixes a direction and a constant cost regardless of how many corners the footprint has. Also volume-only: `deadband` (m³) zeroes out a Cut/Fill figure below the threshold, filtering drag noise near zero height (Cut/Fill/Net/Total are always RAW geometric volumes — deliberately unaffected by `swell`/`shrink`, so they keep answering "does this reach target elevation" regardless of what material's configured); `density` (t/m³ metric, lb/yd³ imperial) and `swell`/`shrink` (multipliers, default 1×) instead populate a separate Material section, standard Bank/Loose/Compacted earthworks convention — Adjusted Cut = raw × swell (loose/haul volume, bigger — excavating adds air voids), Adjusted Fill = raw ÷ shrink (loose/borrow volume needed, also bigger — the raw fill is already the compacted target void), plus Cut/Fill tonnage computed from the raw (not adjusted) volume, since swell/shrink change volume, not mass; the section only appears once at least one of `density`/`swell`/`shrink` is actually configured, no separate toggle. A `stale` field on the `om-measure` readout flags the brief window between a footprint committing and its elevation sample resolving, so a consumer doesn't read numbers left over from a prior footprint as current), `vega-lite` (live charts bound to a layer's data), `dynamic-chart` (the same Vega-Lite rendering, but data-driven by a live DOM event instead of a layer — `on="<event-name>"` + `series-field="<name>"` reads `event.detail[seriesField]` as the chart's `values` on every matching event, redrawing at a fixed `width`; a feature "freezes" it for free by simply omitting that field the next time it fires, no separate pause API needed), and the BIM set `ifc-browser` / `feature-inspector` / `ifc-loader` / `ifc-clash`. Or write your own inline with HTML + a `<script type="om/widget">`. Adjacent compact button widgets (`zoom-controls`, `undo-redo`, `widgets-toggle`) **auto-cluster** into one control group (opt out per widget with `cluster="false"`), and `<om-map widgets-hidden>` / the `set-widgets-visible` action / `<om-widget type="widgets-toggle">` hide all authored chrome without destroying it — provider attribution and the license badge never hide. **Placement is managed**: `position` takes one of 8 logical, RTL-aware slots (`top-start`, `top-center`, `top-end`, `center-start`, `center-end`, `bottom-start`, `bottom-center`, `bottom-end`; legacy corner names alias) — same-slot widgets stack with flush edges and a shared gap, `order` sets in-slot ordering, and `position="manual"` opts out entirely (a plain block you style yourself, even outside the map). At narrow map widths, managed widgets automatically move into accessible top/end/bottom/start drawers — at ≤640px when a row has widgets on more than one of its slots, at ≤416px when every occupied row is one-sided; `fold="never"` keeps an essential control out, `widgets-fold="off"` disables folding, and `--om-widget-fold-breakpoint` changes the map-width threshold. Provider attribution is an in-flow member of `bottom-end` and the license badge of `bottom-start`, so neither covers a widget. A slot dims automatically while an open popup covers it (`widgets-dim="off"` to disable), except slots containing required chrome. Themeable from plain page CSS via custom properties: `om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }` (also `-muted`, `-border`, `-hover-bg`, `-accent`), plus layout tokens (`--om-widget-inset-x/-y`, `--om-widget-gap-x/-y`, `--om-widget-opacity`, `--om-widget-radius`) or the no-CSS sugar `<om-map widget-style="gap:10 opacity:0.9">`. |
|
|
88
88
|
| `<om-overlay>` | Rich HTML anchored to a map location — a static `anchor="[lng, lat]"`, the current selection, or a feature's own geometry via `anchor-layer`/`anchor-feature-id`. Selection-anchored overlays scope with `layer` (one layer's picks) and `selection-type="click"`/`"hover"` (one pick type — give a click-opened popup `selection-type="click"` so hovering elsewhere doesn't drag it along). `{{field}}` interpolates the picked feature, HTML-escaped by default; `{{z}}` is a real depth-picked elevation when some layer under the cursor set `pickable="3d"` (terrain does this itself, so hovering the ground always has one) — empty otherwise, never a misleading 0. `clip-to-map` (opt-in) hides the overlay when its own box would spill past the map viewport, not just when its anchor leaves — for small transient tips that track the cursor, where an overhanging box would otherwise inflate the page's scroll overflow and cause visible view jitter. |
|
|
89
89
|
| `<om-behavior>` | Declarative interactions: `on="click|hover|drag|load|data-loaded"` → a named action. |
|
|
90
90
|
| `<om-story>` | A storyboard: `<om-step>` children fire actions on a timeline. Controlled by the `player` widget, behaviors, or `storyEl.play()/pause()/seek()`. |
|
|
@@ -192,6 +192,8 @@ npx @nika-js/onlymap init
|
|
|
192
192
|
|
|
193
193
|
That opt-in command updates `.vscode/settings.json` and copies the `!`-prefixed manifest snippets into `.vscode/onlymap.code-snippets`. It never runs automatically during install.
|
|
194
194
|
|
|
195
|
+
**Share a map as one file:** `npx @nika-js/onlymap export map.html` writes a portable copy that opens on any computer — every relative data URL is fetched and embedded (format detection preserved: GeoJSON, CSV, Arrow, FlatGeobuf, GeoParquet and friends keep working), relative library references become pinned CDN tags, and a no-JS `<om-fallback>` is added if missing. Heavy embedded data gets a size warning; basemaps, terrain, tile streams, COGs and shapefiles stay network-backed (the tool says which and why).
|
|
196
|
+
|
|
195
197
|
```jsonc
|
|
196
198
|
// .vscode/settings.json
|
|
197
199
|
{ "html.customData": ["./node_modules/@nika-js/onlymap/onlymapjs.html-data.json"] }
|
package/bin/onlymapjs.mjs
CHANGED
|
@@ -13,10 +13,18 @@ function usage() {
|
|
|
13
13
|
onlymapjs init [--force]
|
|
14
14
|
onlymapjs check-layout <manifest.html>
|
|
15
15
|
onlymapjs record <manifest.html> [options]
|
|
16
|
+
onlymapjs export <manifest.html> [options]
|
|
16
17
|
onlymapjs --help
|
|
17
18
|
|
|
18
19
|
Commands:
|
|
19
20
|
init Configure the current project for OnlyMapJS authoring in VS Code.
|
|
21
|
+
export Write a portable copy of a map page that opens on any computer:
|
|
22
|
+
every relative data/src/scenegraph URL is fetched and embedded as
|
|
23
|
+
a data: URL (format detection preserved — CSV, GeoJSON, Arrow,
|
|
24
|
+
FlatGeobuf, GeoParquet all keep working), relative library
|
|
25
|
+
references become pinned CDN tags, and an <om-fallback> is added
|
|
26
|
+
if missing. Remote http(s) URLs, basemaps, terrain, and tile
|
|
27
|
+
streams are left as-is — those still need network access.
|
|
20
28
|
check-layout
|
|
21
29
|
Load a browser-runnable manifest in isolated headless Chromium,
|
|
22
30
|
audit real widget geometry at 360/640/768/1024px, and exit 0/1.
|
|
@@ -30,6 +38,12 @@ Commands:
|
|
|
30
38
|
Options (init):
|
|
31
39
|
--force Overwrite .vscode/${SNIPPET_FILE} if it already exists.
|
|
32
40
|
|
|
41
|
+
Options (export):
|
|
42
|
+
--out <file> Output path (default: <manifest>.export.html).
|
|
43
|
+
--root <dir> Directory that root-absolute URLs ("/data/x.json") resolve
|
|
44
|
+
against (default: the manifest's own directory).
|
|
45
|
+
--force Overwrite an existing output file.
|
|
46
|
+
|
|
33
47
|
Options (record):
|
|
34
48
|
--story <id> Which <om-story> to record (default: the first one).
|
|
35
49
|
--out <file> Output path; .mp4 (H.264) or .webm (VP9). Default <manifest>.mp4.
|
|
@@ -537,6 +551,226 @@ function writeJson(path, value) {
|
|
|
537
551
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
538
552
|
}
|
|
539
553
|
|
|
554
|
+
// --- export: portable single-file map pages --------------------------------
|
|
555
|
+
|
|
556
|
+
/** Attributes that may carry a fetchable single-file URL worth embedding. */
|
|
557
|
+
const EXPORT_URL_ATTRS = ["data", "src", "scenegraph"];
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Extension → data-URL MIME. The library's format detection reads the URL
|
|
561
|
+
* extension (which survives as a `#name.ext` suffix — base64 contains no
|
|
562
|
+
* dots, so the suffix is the first extension-shaped match) and, for some
|
|
563
|
+
* formats, the Content-Type; simple MIMEs only, since a dotted MIME like
|
|
564
|
+
* application/vnd.google-earth.kml could shadow the suffix.
|
|
565
|
+
*/
|
|
566
|
+
const EXPORT_MIME = {
|
|
567
|
+
json: "application/json",
|
|
568
|
+
geojson: "application/json",
|
|
569
|
+
jsonl: "application/json",
|
|
570
|
+
csv: "text/csv",
|
|
571
|
+
tsv: "text/tab-separated-values",
|
|
572
|
+
kml: "application/xml",
|
|
573
|
+
gpx: "application/xml",
|
|
574
|
+
fgb: "application/octet-stream",
|
|
575
|
+
parquet: "application/octet-stream",
|
|
576
|
+
geoparquet: "application/octet-stream",
|
|
577
|
+
arrow: "application/octet-stream",
|
|
578
|
+
feather: "application/octet-stream",
|
|
579
|
+
glb: "model/gltf-binary",
|
|
580
|
+
ifc: "application/octet-stream",
|
|
581
|
+
png: "image/png",
|
|
582
|
+
jpg: "image/jpeg",
|
|
583
|
+
jpeg: "image/jpeg",
|
|
584
|
+
webp: "image/webp",
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/** Single files that cannot be embedded: the reader needs more than these bytes. */
|
|
588
|
+
const EXPORT_SKIP_EXTENSIONS = {
|
|
589
|
+
shp: "shapefiles fetch .dbf/.prj sidecars next to the .shp URL",
|
|
590
|
+
tif: "COGs stream by HTTP Range request",
|
|
591
|
+
tiff: "COGs stream by HTTP Range request",
|
|
592
|
+
zarr: "Zarr stores are directories of chunk files",
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const EXPORT_WARN_FILE_BYTES = 1 * 1024 * 1024; // per-file "this is getting heavy"
|
|
596
|
+
const EXPORT_WARN_TOTAL_BYTES = 5 * 1024 * 1024; // whole-page ceiling worth flagging
|
|
597
|
+
|
|
598
|
+
const formatBytes = (n) => (n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)} MB` : `${Math.ceil(n / 1024)} KB`);
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Attribute values legally contain ">" (accessor expressions like
|
|
602
|
+
* `$value > 100 ? …`), so tags are scanned with quote awareness instead of
|
|
603
|
+
* a `[^>]*` regex.
|
|
604
|
+
*/
|
|
605
|
+
function* scanTags(html, tagName) {
|
|
606
|
+
const open = new RegExp(`<${tagName}(?=[\\s/>])`, "gi");
|
|
607
|
+
let m;
|
|
608
|
+
while ((m = open.exec(html))) {
|
|
609
|
+
let i = m.index + m[0].length;
|
|
610
|
+
let quote = null;
|
|
611
|
+
while (i < html.length) {
|
|
612
|
+
const c = html[i];
|
|
613
|
+
if (quote) {
|
|
614
|
+
if (c === quote) quote = null;
|
|
615
|
+
} else if (c === '"' || c === "'") quote = c;
|
|
616
|
+
else if (c === ">") break;
|
|
617
|
+
i += 1;
|
|
618
|
+
}
|
|
619
|
+
yield { start: m.index, end: i + 1, text: html.slice(m.index, i + 1) };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function readTagAttr(tagText, attr) {
|
|
624
|
+
const m = new RegExp(`\\b${attr}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i").exec(tagText);
|
|
625
|
+
return m ? { value: m[2] ?? m[3], start: m.index, length: m[0].length, raw: m[0] } : null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function urlExtensionOf(url) {
|
|
629
|
+
return /\.(\w+)(?:\?|#|$)/.exec(url)?.[1]?.toLowerCase();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function exportStandalone(rest) {
|
|
633
|
+
const valueFlags = new Set(["--out", "--root"]);
|
|
634
|
+
const positional = [];
|
|
635
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
636
|
+
if (valueFlags.has(rest[i])) i += 1; // skip the flag's value
|
|
637
|
+
else if (!rest[i].startsWith("--")) positional.push(rest[i]);
|
|
638
|
+
}
|
|
639
|
+
if (positional.length !== 1) throw new Error("Usage: onlymapjs export <manifest.html> [--out file] [--root dir] [--force]");
|
|
640
|
+
const flagValue = (flag) => {
|
|
641
|
+
const i = rest.indexOf(flag);
|
|
642
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
643
|
+
};
|
|
644
|
+
const inputPath = resolve(positional[0]);
|
|
645
|
+
if (!existsSync(inputPath)) throw new Error(`No such file: ${inputPath}`);
|
|
646
|
+
const inputDir = dirname(inputPath);
|
|
647
|
+
const rootDir = resolve(flagValue("--root") ?? inputDir);
|
|
648
|
+
const outPath = resolve(flagValue("--out") ?? inputPath.replace(/\.html?$/i, "") + ".export.html");
|
|
649
|
+
if (resolve(outPath) === inputPath) throw new Error("--out must differ from the input file.");
|
|
650
|
+
if (existsSync(outPath) && !rest.includes("--force")) throw new Error(`${outPath} already exists; pass --force to overwrite it.`);
|
|
651
|
+
|
|
652
|
+
let html = readFileSync(inputPath, "utf8");
|
|
653
|
+
const warnings = [];
|
|
654
|
+
const notes = [];
|
|
655
|
+
const inlined = [];
|
|
656
|
+
let totalBytes = 0;
|
|
657
|
+
|
|
658
|
+
// 1. Embed relative data/src/scenegraph URLs on <om-layer> tags.
|
|
659
|
+
const edits = []; // { start, end, replacement } over the ORIGINAL html, applied back-to-front
|
|
660
|
+
for (const tag of scanTags(html, "om-layer")) {
|
|
661
|
+
for (const attr of EXPORT_URL_ATTRS) {
|
|
662
|
+
const found = readTagAttr(tag.text, attr);
|
|
663
|
+
if (!found || !found.value) continue;
|
|
664
|
+
const url = found.value;
|
|
665
|
+
if (/^(https?:|wss?:|data:|blob:|draw:)/i.test(url)) {
|
|
666
|
+
if (/^wss?:/i.test(url)) notes.push(`${attr}="${url}" is a live stream — it will need network access wherever the file opens.`);
|
|
667
|
+
continue; // remote/self-describing URLs travel as-is
|
|
668
|
+
}
|
|
669
|
+
if (/\{[zxy]\}/.test(url)) {
|
|
670
|
+
notes.push(`${attr}="${url}" is a tile template — tile streams cannot be embedded and stay network-backed.`);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const ext = urlExtensionOf(url);
|
|
674
|
+
if (ext && EXPORT_SKIP_EXTENSIONS[ext]) {
|
|
675
|
+
warnings.push(`${attr}="${url}" was NOT embedded (${EXPORT_SKIP_EXTENSIONS[ext]}). Host it at an absolute URL for the exported file to work elsewhere.`);
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
const cleanPath = decodeURIComponent(url.split(/[?#]/)[0]);
|
|
679
|
+
const filePath = cleanPath.startsWith("/") ? join(rootDir, cleanPath) : resolve(inputDir, cleanPath);
|
|
680
|
+
if (!existsSync(filePath)) {
|
|
681
|
+
warnings.push(`${attr}="${url}" resolves to ${filePath}, which does not exist — left untouched.`);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const bytes = readFileSync(filePath);
|
|
685
|
+
const mime = (ext && EXPORT_MIME[ext]) || "application/json";
|
|
686
|
+
const name = basename(cleanPath);
|
|
687
|
+
const dataUrl = `data:${mime};base64,${bytes.toString("base64")}#${encodeURIComponent(name)}`;
|
|
688
|
+
edits.push({
|
|
689
|
+
start: tag.start + found.start,
|
|
690
|
+
end: tag.start + found.start + found.length,
|
|
691
|
+
replacement: `${attr}="${dataUrl}"`,
|
|
692
|
+
});
|
|
693
|
+
totalBytes += bytes.length;
|
|
694
|
+
inlined.push({ url, bytes: bytes.length });
|
|
695
|
+
if (bytes.length > EXPORT_WARN_FILE_BYTES) {
|
|
696
|
+
warnings.push(
|
|
697
|
+
`${name} is ${formatBytes(bytes.length)} — heavy for an embedded page (base64 adds ~33%). ` +
|
|
698
|
+
`Consider hosting it at an absolute URL instead of embedding.`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) {
|
|
704
|
+
html = html.slice(0, edit.start) + edit.replacement + html.slice(edit.end);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// 2. Pin relative library references to the CDN at this package's version.
|
|
708
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
709
|
+
const version = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")).version;
|
|
710
|
+
const cdnScript = `https://unpkg.com/@nika-js/onlymap@${version}`;
|
|
711
|
+
const cdnCss = `https://unpkg.com/@nika-js/onlymap@${version}/dist/onlymapjs.css`;
|
|
712
|
+
let sawLibScript = false;
|
|
713
|
+
let sawLibCss = false;
|
|
714
|
+
const libEdits = [];
|
|
715
|
+
for (const tag of scanTags(html, "script")) {
|
|
716
|
+
const src = readTagAttr(tag.text, "src");
|
|
717
|
+
if (!src?.value) continue;
|
|
718
|
+
const isLib = /@nika-js\/onlymap|onlymapjs?\.(m?js|ts)|onlymap\.standalone\.js|\/src\/index\.ts/.test(src.value);
|
|
719
|
+
if (!isLib) continue;
|
|
720
|
+
sawLibScript = true;
|
|
721
|
+
if (/^https?:/i.test(src.value)) continue; // already CDN/hosted
|
|
722
|
+
libEdits.push({ start: tag.start + src.start, end: tag.start + src.start + src.length, replacement: `src="${cdnScript}"` });
|
|
723
|
+
}
|
|
724
|
+
for (const tag of scanTags(html, "link")) {
|
|
725
|
+
const href = readTagAttr(tag.text, "href");
|
|
726
|
+
if (!href?.value || !/onlymapjs\.css/.test(href.value)) continue;
|
|
727
|
+
sawLibCss = true;
|
|
728
|
+
if (/^https?:/i.test(href.value)) continue;
|
|
729
|
+
libEdits.push({ start: tag.start + href.start, end: tag.start + href.start + href.length, replacement: `href="${cdnCss}"` });
|
|
730
|
+
}
|
|
731
|
+
for (const edit of libEdits.sort((a, b) => b.start - a.start)) {
|
|
732
|
+
html = html.slice(0, edit.start) + edit.replacement + html.slice(edit.end);
|
|
733
|
+
}
|
|
734
|
+
if (!sawLibScript || !sawLibCss) {
|
|
735
|
+
// A bare-manifest page (no library tags at all): inject the pinned pair
|
|
736
|
+
// so the file boots anywhere. Before </head> when one exists, else at top.
|
|
737
|
+
const inject =
|
|
738
|
+
(sawLibCss ? "" : `<link rel="stylesheet" href="${cdnCss}" />\n`) +
|
|
739
|
+
(sawLibScript ? "" : `<script type="module" src="${cdnScript}"></script>\n`);
|
|
740
|
+
const headClose = /<\/head>/i.exec(html);
|
|
741
|
+
html = headClose ? html.slice(0, headClose.index) + inject + html.slice(headClose.index) : inject + html;
|
|
742
|
+
notes.push("The page had no OnlyMapJS reference — pinned CDN tags were injected.");
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// 3. A no-JS fallback for file-preview contexts (the export's whole audience).
|
|
746
|
+
if (!/<om-fallback[\s>]/i.test(html)) {
|
|
747
|
+
const mapOpen = [...scanTags(html, "om-map")][0];
|
|
748
|
+
if (mapOpen) {
|
|
749
|
+
const fallback =
|
|
750
|
+
`\n <om-fallback><p><strong>This interactive map requires JavaScript.</strong><br/>` +
|
|
751
|
+
`If you are seeing this in a file preview, open the file in a web browser such as Chrome, Safari, or Firefox.</p></om-fallback>`;
|
|
752
|
+
html = html.slice(0, mapOpen.end) + fallback + html.slice(mapOpen.end);
|
|
753
|
+
notes.push("Added a generic <om-fallback> (none was authored).");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
writeFileSync(outPath, html);
|
|
758
|
+
|
|
759
|
+
for (const { url, bytes } of inlined) console.log(` embedded ${url} (${formatBytes(bytes)})`);
|
|
760
|
+
if (totalBytes > EXPORT_WARN_TOTAL_BYTES) {
|
|
761
|
+
warnings.push(
|
|
762
|
+
`Embedded data totals ${formatBytes(totalBytes)} (~${formatBytes(Math.ceil(totalBytes * 1.34))} as base64) — ` +
|
|
763
|
+
`the exported page will be slow to open and share. Host the heavy layers at absolute URLs instead.`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
for (const note of notes) console.log(` note: ${note}`);
|
|
767
|
+
for (const warning of warnings) console.warn(` WARNING: ${warning}`);
|
|
768
|
+
const outSize = statSync(outPath).size;
|
|
769
|
+
console.log(`\nWrote ${outPath} (${formatBytes(outSize)}). Library pinned to @nika-js/onlymap@${version} via CDN.`);
|
|
770
|
+
console.log("Basemaps, terrain, and any remote/tiled sources still need network access when the file is opened.");
|
|
771
|
+
return 0;
|
|
772
|
+
}
|
|
773
|
+
|
|
540
774
|
function init({ force = false } = {}) {
|
|
541
775
|
const cwd = process.cwd();
|
|
542
776
|
const vscodeDir = join(cwd, ".vscode");
|
|
@@ -583,6 +817,8 @@ try {
|
|
|
583
817
|
process.exitCode = await checkLayout(rest[0]);
|
|
584
818
|
} else if (command === "record") {
|
|
585
819
|
process.exitCode = await recordStory(rest);
|
|
820
|
+
} else if (command === "export") {
|
|
821
|
+
process.exitCode = await exportStandalone(rest);
|
|
586
822
|
} else {
|
|
587
823
|
console.error(`Unknown command: ${command}\n`);
|
|
588
824
|
usage();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as ny, L as sy, M as oy, m as ay, c as ka, a as Sd, z as oc, G as sm, V as ly, W as cy, b as uy, g as hy, d as Vu, f as Ed, e as dy, l as py, u as fy, D as Qf, h as my } from "./index-
|
|
1
|
+
import { C as ny, L as sy, M as oy, m as ay, c as ka, a as Sd, z as oc, 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-DkimKgOm.js";
|
|
2
2
|
const Cd = Math.PI / 180, gy = 180 / Math.PI;
|
|
3
3
|
function em(Pe, J = 0) {
|
|
4
4
|
const me = Math.min(180, Pe) * Cd;
|
|
@@ -116,6 +116,14 @@ export declare class OmMapElement extends HTMLElementBase {
|
|
|
116
116
|
/** Cached breakpoint resolution — re-probes only when the token string actually changes, not on every ResizeObserver tick. */
|
|
117
117
|
private foldBreakpointPx;
|
|
118
118
|
private measureWidgetFold;
|
|
119
|
+
/**
|
|
120
|
+
* Authored slot occupancy for the fold criterion — POSITION ATTRIBUTES,
|
|
121
|
+
* never live container membership: folding reparents widgets into drawer
|
|
122
|
+
* panels, and a criterion that read the containers would see them empty
|
|
123
|
+
* and immediately argue for unfolding. The attribute is authored state
|
|
124
|
+
* that survives every reparent.
|
|
125
|
+
*/
|
|
126
|
+
private occupiedWidgetSlots;
|
|
119
127
|
private updateWidgetFoldForWidth;
|
|
120
128
|
/**
|
|
121
129
|
* Lift the bottom-end row clear of the bottom-start row and the folded
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as E, j as S, k as R, o as N, p as h, q as w, s as O, t as W, v as x, w as I, x as B, y as _, A as P, B as k, E as $, F as z, H as v, I as F, J as C, K as U, N as p, O as M } from "./index-
|
|
2
|
-
import { P as $e, R as ze, _ as ve, Q as Ce, S as Ue, T as Me, U as je, X as De, Y as Je, Z as qe, $ as He, a0 as Ve, a1 as Ge, a2 as Ke, a3 as Qe, a4 as Xe, a5 as Ye, a6 as Ze, a7 as et, a8 as tt, a9 as nt, aa as rt, ab as at, ac as st, ad as ot } from "./index-
|
|
1
|
+
import { i as E, j as S, k as R, o as N, p as h, q as w, s as O, t as W, v as x, w as I, x as B, y as _, A as P, B as k, E as $, F as z, H as v, I as F, J as C, K as U, N as p, O as M } from "./index-DkimKgOm.js";
|
|
2
|
+
import { P as $e, R as ze, _ as ve, Q as Ce, S as Ue, T as Me, U as je, X as De, Y as Je, Z as qe, $ as He, a0 as Ve, a1 as Ge, a2 as Ke, a3 as Qe, a4 as Xe, a5 as Ye, a6 as Ze, a7 as et, a8 as tt, a9 as nt, aa as rt, ab as at, ac as st, ad as ot } from "./index-DkimKgOm.js";
|
|
3
3
|
import { g as j, i as D } from "./table-accessors-CYWTzpQI.js";
|
|
4
4
|
async function J(t, e, n = {}, r = {}) {
|
|
5
5
|
const a = E(t), s = S.getWorkerFarm(n), { source: o } = n, c = { name: a, source: o };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ag as Be, ah as le, ai as ce, aj as De, ak as Oe, al as ke } from "./index-
|
|
1
|
+
import { ag as Be, ah as le, ai as ce, aj as De, ak as Oe, al as ke } from "./index-DkimKgOm.js";
|
|
2
2
|
import { y as ve, a as he, z as G, R as J, A as ue, C as Fe, F as je, n as xe, S as Ie, E as Ne } from "./recordbatch-Bpc0uxFn.js";
|
|
3
3
|
import { g as V, a as fe, b as Le, c as Ue, d as Me, e as qe, m as Ve } from "./table-accessors-CYWTzpQI.js";
|
|
4
4
|
import { c as $e } from "./convert-arrow-schema-DrAihRf9.js";
|