From d75a797f466030fb3e2f10320c6b733a27681555 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Thu, 25 Jun 2026 16:36:44 -0400 Subject: [PATCH] feat(web): read-only "widget" mode + embeddable Annapolis demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a static, embeddable, read-only build of — the "widget" mode — and ship it as a live demo on the docs site. Renames the old prebaked "prod" mode to "widget" (attribute / ?widget / :host([widget]) / make serve-widget) and hardens it into a pure viewer: hides the Charts and Share buttons, drops the Advanced settings tab, and keeps only the client-side display settings (General/Text/Units/Depths), all local. No backend, no in-browser baking, no connections/dev tools (already prod-gated). Tooling + hosting: - `make demo` assembles a self-contained static bundle (per-band Annapolis .pmtiles + manifest, emitted S-101 client assets, the frontend) via the new scripts/fetch-demo-cells.sh; `make serve-demo` previews it. - docs CI builds the bundle into docs/static/demo and deploys it at /chartplotter/demo/; the intro page embeds it live via a component. New Widget docs page covers packaging, deploying, and CORS. Fixes: - serve: emitted S-101 assets are now a fallback, not a replacement, so an explicit --assets bundle serves its own index.html / manifest / .pmtiles (also fixes serve-widget). - downloader: resolve manifest archive (and aux) paths against the manifest URL, not the page — so a widget embedded on a page in a different directory finds its tiles instead of 404ing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docs.yml | 44 +++++++++- Makefile | 50 ++++++++++-- README.md | 8 +- cmd/chartplotter/serve.go | 9 ++- docs/.gitignore | 4 + docs/docs/getting-started.md | 2 + docs/docs/{intro.md => intro.mdx} | 9 ++- docs/docs/widget.md | 108 +++++++++++++++++++++++++ docs/docusaurus.config.js | 5 ++ docs/sidebars.js | 1 + docs/src/components/LiveChart.js | 46 +++++++++++ docs/src/css/custom.css | 28 +++++++ internal/engine/server/http.go | 35 ++++++-- scripts/fetch-demo-cells.sh | 29 +++++++ scripts/shot-s64.mjs | 2 +- web/demo.html | 82 +++++++++++++++++++ web/index.html | 2 +- web/src/chartplotter.mjs | 60 +++++++------- web/src/chartplotter.view.mjs | 13 +-- web/src/core/core-settings.mjs | 4 +- web/src/core/settings-store.mjs | 14 ++-- web/src/data/chart-downloader.mjs | 12 ++- web/src/data/vessel-state-store.mjs | 10 +-- web/src/plugins/ais-overlay.mjs | 6 +- web/src/plugins/chart-library.mjs | 18 ++--- web/src/plugins/chart-library.view.mjs | 6 +- 26 files changed, 518 insertions(+), 89 deletions(-) rename docs/docs/{intro.md => intro.mdx} (91%) create mode 100644 docs/docs/widget.md create mode 100644 docs/src/components/LiveChart.js create mode 100755 scripts/fetch-demo-cells.sh create mode 100644 web/demo.html diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 36b46d4..cde97dd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,6 +6,15 @@ on: paths: - "docs/**" - ".github/workflows/docs.yml" + # The live demo is built from the app + bake pipeline and deployed under the + # docs site, so redeploy when any of those change too. + - "web/**" + - "cmd/**" + - "internal/**" + - "scripts/fetch-demo-cells.sh" + - "Makefile" + - "go.mod" + - "go.sum" workflow_dispatch: permissions: @@ -21,20 +30,49 @@ concurrency: jobs: build: runs-on: ubuntu-latest - defaults: - run: - working-directory: docs steps: - uses: actions/checkout@v7 + + - uses: actions/setup-go@v6 + with: + go-version: "1.26" + cache: true + + # Cache the curated NOAA demo-cell downloads (one per band over Annapolis), + # keyed on the cell list / fetch script so a change re-downloads. Lets the + # bake reuse the same cells across runs. + - name: Cache demo ENC cells + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/demo-cells + key: demo-cells-${{ hashFiles('scripts/fetch-demo-cells.sh', 'Makefile') }} + + # Build the read-only widget demo bundle into docs/static/demo so Docusaurus + # copies it into the published site at /chartplotter/demo/. Needs the S-101 + # catalogue (IHO material kept out of the repo): clone it and let `make build` + # (a dep of `make demo`) embed it. The bundle is pure static files — no backend. + - name: Build live demo bundle (docs/static/demo) + run: | + git clone --depth 1 https://github.com/iho-ohi/S-101_Portrayal-Catalogue.git "$RUNNER_TEMP/s101-pc" + git clone --depth 1 https://github.com/iho-ohi/S-101-Documentation-and-FC.git "$RUNNER_TEMP/s101-fc" + make demo \ + S101_PC="$RUNNER_TEMP/s101-pc/PortrayalCatalog" \ + S101_FC="$RUNNER_TEMP/s101-fc/S-101FC/FeatureCatalogue.xml" \ + DEMO_CACHE="$RUNNER_TEMP/demo-cells" \ + DEMO_OUT="docs/static/demo" + - uses: actions/setup-node@v6 with: node-version: "20" cache: npm cache-dependency-path: docs/package-lock.json - name: Install + working-directory: docs run: npm ci - name: Build + working-directory: docs run: npm run build + - uses: actions/upload-pages-artifact@v3 with: path: docs/build diff --git a/Makefile b/Makefile index e41788d..038a65d 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ S101_PC ?= $(HOME)/Projects/s101-portrayal-catalogue/PortrayalCatalog S101_FC ?= $(HOME)/Projects/s101-feature-catalogue/S-101FC/FeatureCatalogue.xml S101_CACHE ?= $(CACHE)/s101 -.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-prod +.PHONY: build xbuild test vet fmt fmt-check tidy clean clear-cache serve docs docs-shots bake-ienc bake-noaa serve-widget demo serve-demo # Prebaked prod test set (US Inland ENC bundle + the NOAA world archive). # NB: keep these as bare values with NO inline `#` comments — Make folds any @@ -146,10 +146,11 @@ noaa-d%.stamp: $(NOAA_CACHE)/%CGD_ENCs.zip | $(BIN) @touch "$@" # Serve the per-district NOAA archives + the baked IENC archive TOGETHER, -# prebaked, in production mode on 0.0.0.0:8080. Every .pmtiles lives at the project -# root; they're symlinked into web/ (the served asset dir) and listed in a combined -# charts-index.json manifest the prod app loads via ?catalog=. Open the printed URL. -serve-prod: build bake-noaa ## Serve per-district per-band NOAA + IENC prebaked pmtiles together, prod mode, on 0.0.0.0:8080 +# prebaked, in read-only widget mode on 0.0.0.0:8080. Every .pmtiles lives at the +# project root; they're symlinked into web/ (the served asset dir) and listed in a +# combined charts-index.json manifest the widget app loads via ?catalog=. Open the +# printed URL. +serve-widget: build bake-noaa ## Serve per-district per-band NOAA + IENC prebaked pmtiles together, read-only widget mode, on 0.0.0.0:8080 @ln -sf "$(abspath $(IENC_PMTILES))" web/ienc.pmtiles @for d in $(DISTRICTS); do for s in $(NOAA_BANDS); do \ f="noaa-d$$d-$$s.pmtiles"; [ -f "$$f" ] && ln -sf "$(abspath .)/$$f" "web/$$f" || true; \ @@ -162,12 +163,45 @@ serve-prod: build bake-noaa ## Serve per-district per-band NOAA + IENC prebaked printf ' { "file": "ienc.pmtiles", "band": "all" }\n ]\n}\n'; \ } > web/charts-index.json @echo - @echo " Prebaked prod test server — open:" - @echo " http://localhost:8080/?prod&catalog=/charts-index.json" - @echo " (binds 0.0.0.0 — reachable from the LAN at http://:8080/?prod&catalog=/charts-index.json)" + @echo " Prebaked widget test server — open:" + @echo " http://localhost:8080/?widget&catalog=/charts-index.json" + @echo " (binds 0.0.0.0 — reachable from the LAN at http://:8080/?widget&catalog=/charts-index.json)" @echo $(BIN) serve --host 0.0.0.0 --port 8080 --assets web +# ---- read-only demo bundle (the `widget` mode, packaged for static hosting) ---- +# A self-contained, no-backend chart viewer over ONE location (Annapolis) with all +# the bands NOAA publishes there, so a visitor can zoom from the whole bay down to +# the docks on a few MB of tiles. `make demo` assembles dist/demo/ (override with +# DEMO_OUT, e.g. DEMO_OUT=docs/static/demo in CI): the per-band .pmtiles + manifest, +# the generated S-101 client assets, and the committed static frontend (demo.html +# as index.html). Serve it from ANY static host / CDN — no server logic required. +DEMO_CELLS ?= US2EC03M US3EC08M US4MD1DC US5MD1MC +DEMO_CACHE ?= $(CACHE)/demo +DEMO_OUT ?= dist/demo +DEMO_MAXZOOM ?= 16 + +demo: build ## Assemble the read-only Annapolis widget demo bundle into $(DEMO_OUT) + DEMO_CACHE="$(DEMO_CACHE)" DEMO_CELLS="$(DEMO_CELLS)" NOAA_URL_BASE="$(NOAA_URL_BASE)" scripts/fetch-demo-cells.sh + @mkdir -p "$(DEMO_OUT)" + $(BIN) bake "$(DEMO_CACHE)" -o "$(DEMO_OUT)/demo.pmtiles" --bands --max-zoom $(DEMO_MAXZOOM) --manifest "$(DEMO_OUT)/charts-index.json" + $(BIN) emit-assets "$(DEMO_OUT)" $(if $(wildcard $(S101_PC)),--s101 "$(S101_PC)") + @echo "assembling static frontend → $(DEMO_OUT)" + @cp web/demo.html "$(DEMO_OUT)/index.html" + @cp web/manifest.webmanifest web/catalog.json web/icon-192.png web/icon-512.png web/apple-touch-icon.png "$(DEMO_OUT)/" + @cp -R web/src web/vendor web/glyphs web/basemap "$(DEMO_OUT)/" + @echo " demo bundle ready: $(DEMO_OUT)/ — host it on any static server / CDN" + +# LOCAL PREVIEW ONLY. The bundle is pure static files — deploy it to ANY +# range-capable static host (GitHub Pages, S3/CloudFront, nginx, `npx serve`); it +# needs no backend. PMTiles are read with HTTP Range, which python's http.server +# does NOT support, so we preview with the chartplotter binary acting purely as a +# range-capable static file server (the widget page makes no /api calls). +serve-demo: demo ## Preview the static demo bundle locally (range-capable static serve; HOST/PORT overridable) + @echo " Read-only widget demo — open: http://$(HOST):$(PORT)/" + $(BIN) serve --host $(HOST) --port $(PORT) --assets "$(DEMO_OUT)" \ + $(if $(wildcard $(S101_PC)),--s101 "$(S101_PC)" --s101-fc "$(S101_FC)" --cache "$(S101_CACHE)") + docs: ## Run the documentation site dev server (Docusaurus; DOCS_HOST/DOCS_PORT overridable) cd docs && { [ -d node_modules ] || npm install; } && npm start -- --host $(DOCS_HOST) --port $(DOCS_PORT) diff --git a/README.md b/README.md index 73689cc..64fe7cc 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,16 @@

+ ▶ Try the live demo +  ·  📚 Read the docs →

- chartplotter rendering the North Carolina coast off Cape Lookout -
The North Carolina coast off Cape Lookout — NOAA ENC data, S-101 portrayal, ~1:700,000. + + chartplotter rendering NOAA charts of Annapolis — click to open the live demo + +
Open the live, interactive demo — official NOAA charts of Annapolis, rendered in your browser. No install, no server.

--- diff --git a/cmd/chartplotter/serve.go b/cmd/chartplotter/serve.go index 1d521bb..860957d 100644 --- a/cmd/chartplotter/serve.go +++ b/cmd/chartplotter/serve.go @@ -35,6 +35,7 @@ func (c serveCmd) Run() error { // on its own (baker.applyPortrayer); here we emit the matching client assets // (colortables/sprite/patterns/linestyles) into a temp dir and serve them. var catalogFS fs.FS + var s101AssetDir string // freshly-emitted S-101 client assets (temp dir), or "" switch { case c.S101 != "": if c.S101FC == "" { @@ -63,7 +64,12 @@ func (c serveCmd) Run() error { if _, err := assets.EmitS101FS(catalogFS, "daySvgStyle.css", assetDir); err != nil { return fmt.Errorf("emit S-101 assets: %w", err) } - c.Assets = assetDir // override colortables/linestyles/sprite; rest falls back to embedded + // The emitted S-101 client assets (colortables/linestyles/sprite/patterns) + // are a FALLBACK, not a replacement: an explicit --assets dir stays primary + // (so a prebaked widget bundle serves its own index.html / charts-index.json / + // .pmtiles), this temp dir fills in the generated S-101 files it lacks, and the + // embedded bundle backs the rest. Registered on the Server below. + s101AssetDir = assetDir } cacheDir := c.Cache @@ -87,6 +93,7 @@ func (c serveCmd) Run() error { // other bind means the operator opted into network exposure. allowRemote := !(c.Host == "127.0.0.1" || c.Host == "localhost" || c.Host == "::1") srv := server.New(c.Assets, cacheDir, dataDir, allowRemote) + srv.SetAssetFallback(s101AssetDir) // emitted S-101 assets, searched after --assets, before embedded srv.Version = version srv.ReportStaleCache() // loud warning if any served pack predates this binary diff --git a/docs/.gitignore b/docs/.gitignore index 4bbd4fe..7ecad62 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -8,6 +8,10 @@ .docusaurus .cache-loader +# Live demo bundle — generated by `make demo DEMO_OUT=docs/static/demo` in CI +# (the read-only widget app + baked Annapolis .pmtiles), never committed. +/static/demo/ + # Misc .DS_Store .env.local diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index 6a9c0dc..404500d 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -64,6 +64,8 @@ restrictions, light characteristics, depths, source dates, and any attached text ## Where to go next +- Put a chart on your own page — see [Widget mode](./widget.md) for the read-only, + no-server build and how to package and deploy it. - Learn how a cell becomes tiles in [Architecture](./architecture.md). - See the exact tile layers and fields in the [Tile Schema](./tile-schema.md). - Look up the commands in the [CLI Reference](./cli.md) if you want to bake diff --git a/docs/docs/intro.md b/docs/docs/intro.mdx similarity index 91% rename from docs/docs/intro.md rename to docs/docs/intro.mdx index 6ee5fb5..cdae1db 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.mdx @@ -5,6 +5,8 @@ slug: / sidebar_position: 1 --- +import LiveChart from '@site/src/components/LiveChart'; + # chartplotter :::warning Not for navigation @@ -19,7 +21,12 @@ real-world navigation.** **chartplotter** is a marine chart engine written in Go. It turns official NOAA nautical charts into fast, offline map tiles that you can view in a web browser. -![A chartplotter chart of Chesapeake Bay](/img/ui/chart-day.png) +The chart below is **live** — official NOAA charts of Annapolis, Maryland, running +right here in your browser. Pan, zoom from the whole Chesapeake to the docks, and +switch Day / Dusk / Night. It is the read-only [**widget**](./widget.md) build: +static, embeddable, no server. + + It reads **S-57** electronic navigational chart (ENC) cells, draws them with the **S-101 Portrayal Catalogue** — the modern IHO standard for how charts look — and diff --git a/docs/docs/widget.md b/docs/docs/widget.md new file mode 100644 index 0000000..2b32840 --- /dev/null +++ b/docs/docs/widget.md @@ -0,0 +1,108 @@ +--- +id: widget +title: Widget +sidebar_position: 4 +--- + +# Widget mode + +**Widget mode** is a static, embeddable, read-only build of `` — the +same viewer you see [on the home page](./intro.mdx). It has **no backend and does no +in-browser baking**: the chart is a handful of prebaked +[PMTiles](./architecture.md) archives streamed straight from a static host over HTTP +range requests. It is how you put a chartplotter chart on a web page or ship a +self-contained demo. + +Widget mode strips the management chrome down to a pure viewer: + +- **No chart library, import, or downloads** — the charts are fixed. +- **No share link, no connections (NMEA/AIS), no Advanced / developer tools.** +- **Only the client-side display settings** — General, Text, Units, and Depths — + applied live and stored locally in the browser. Nothing is sent anywhere. + +:::warning Not for navigation +A widget renders a fixed set of cached cells. **Do not use it for real-world +navigation.** See [Known limitations](./limitations.md). +::: + +## Enable it + +Widget mode is turned on by the `widget` attribute (or the `?widget` query param). +Point the viewer at a prebaked archive — either a per-band set discovered from a +`charts-index.json` next to the page, or a single archive via `pmtiles="…"`: + +```html + + +``` + +The `assets` attribute is the key: the widget resolves **everything** relative to it — +`vendor/maplibre-gl.js`, the S-101 client assets, `glyphs/`, `basemap/`, +`catalog.json`, and `charts-index.json`. Put the whole bundle in one directory and +point `assets` at it. (That is exactly how the [home page](./intro.mdx) embeds the +live chart, via the `` React component in `docs/src/components/`.) + +## Package it + +One make target assembles a complete bundle — it downloads the curated cells, bakes +the per-band archives, emits the S-101 client assets, and copies in the static +frontend: + +```sh +make demo # → dist/demo/ +make demo DEMO_OUT=path/to/out # → a directory of your choosing +make serve-demo # build it, then preview at http://127.0.0.1:8080 +``` + +The result is a self-contained static site. `dist/demo/` contains: + +| What | Files | +| --- | --- | +| Per-band tiles + manifest | `demo-.pmtiles`, `charts-index.json` | +| External text/picture files | `demo-aux.zip` | +| S-101 client assets | `colortables.json`, `linestyles.json`, `sprite.{json,png}`, `patterns.{json,png}` | +| Frontend | `index.html`, `src/`, `vendor/`, `glyphs/`, `basemap/`, `catalog.json` | + +To bake a different location, override the cells (and re-centre the page): + +```sh +make demo DEMO_CELLS="US2EC03M US3EC08M US4MD1DC US5MD1MC" +``` + +You can also assemble a bundle by hand from the CLI: + +```sh +chartplotter bake CELLS… -o out/demo.pmtiles --bands --manifest out/charts-index.json +chartplotter emit-assets out/ # the S-101 client assets +# then copy web/{index.html,src,vendor,glyphs,basemap,catalog.json} into out/ +``` + +## Deploy it + +The bundle is **pure static files** — copy the directory to any static host. The only +hard requirement is **HTTP range request support**, because the PMTiles archives are +read with byte ranges. Every real static host has it: + +- **GitHub Pages** — what this project uses; CI builds the bundle into + `docs/static/demo/` so it deploys at `//demo/`. +- **Amazon S3 + CloudFront** — serve `.pmtiles` as `application/octet-stream` and + allow long-lived immutable caching (see `web/customHttp.yml` for the header set). +- **nginx / Caddy / Netlify / Cloudflare Pages** — work out of the box. + +What does **not** work: `python3 -m http.server` — it ignores `Range` and returns the +whole archive for every tile fetch. For a quick local preview without deploying, +`make serve-demo` runs the `chartplotter` binary purely as a range-capable static file +server (the widget page itself makes no API calls). + +A few small things hosts occasionally need: + +- Serve `.pmtiles` with `Content-Type: application/octet-stream` and + `Accept-Ranges: bytes`. +- Keep relative paths intact — the bundle uses only relative URLs, so it works under + any sub-path (e.g. `/chartplotter/demo/`) without configuration. +- **Cross-origin embedding:** if the page and the bundle are on *different* origins + (e.g. your blog embeds a chart hosted on GitHub Pages), the bundle's host must send + `Access-Control-Allow-Origin` — the assets are fetched with `fetch()` and the sprite + image with `crossOrigin="anonymous"`. GitHub Pages, S3/CloudFront, and the + `chartplotter` server already send `*`. A **same-origin** embed — like this docs + site, where the page and the bundle share a host — needs nothing. diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 56ed4d0..6f42480 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -50,6 +50,11 @@ const config = { navbar: { title: 'chartplotter', items: [ + { + href: 'https://beetlebugorg.github.io/chartplotter/demo/', + label: 'Live demo', + position: 'right', + }, { href: 'https://github.com/beetlebugorg/chartplotter', label: 'GitHub', diff --git a/docs/sidebars.js b/docs/sidebars.js index ec8ea03..cc80ea9 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -6,6 +6,7 @@ const sidebars = { 'intro', 'installation', 'getting-started', + 'widget', 'cli', 'architecture', 'tile-schema', diff --git a/docs/src/components/LiveChart.js b/docs/src/components/LiveChart.js new file mode 100644 index 0000000..c7793db --- /dev/null +++ b/docs/src/components/LiveChart.js @@ -0,0 +1,46 @@ +import React, {useEffect} from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +// LiveChart embeds the read-only "widget" build of live in the +// page. The chart, MapLibre, and every asset (tiles, sprite, glyphs, basemap, +// catalog) load from the prebaked demo bundle the docs build assembles under +// //demo/ — see `make demo` and .github/workflows/docs.yml. Build it +// locally first with: make demo DEMO_OUT=docs/static/demo +function Chart() { + // useBaseUrl prefixes the site baseUrl, e.g. "/chartplotter/demo/". The widget + // resolves ALL of its assets (incl. vendor/maplibre-gl.js and charts-index.json) + // relative to this, so the whole demo is self-contained in that one directory. + const base = useBaseUrl('/demo/'); + useEffect(() => { + const id = 'chartplotter-widget-module'; + if (document.getElementById(id)) return; // define once + const s = document.createElement('script'); + s.type = 'module'; + s.id = id; + s.src = `${base}src/chartplotter.mjs`; + document.head.appendChild(s); + }, [base]); + return ( + <> +
+ {/* widget = read-only viewer; assets points every fetch at the demo bundle */} + +
+ {/* Plain (not a router Link) → full-page nav to the static bundle. */} +

+ Open the chart full-screen → +

+ + ); +} + +export default function LiveChart() { + return ( + Loading live chart…} + > + {() => } + + ); +} diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 7f8a45e..c5bfc35 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -24,3 +24,31 @@ --ifm-color-primary-lightest: #b3e0f5; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } + +/* The embed (the read-only widget chart, mounted on the intro page). + A fixed-height, rounded frame; the widget fills it and floats its own chrome. */ +.liveChart { + height: 68vh; + min-height: 440px; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 20px rgba(0, 0, 0, 0.18); + margin: 1.5rem 0 0.5rem; + background: #0b1b2b; +} +.liveChart chart-plotter { + display: block; + width: 100%; + height: 100%; +} +.liveChart--loading { + display: flex; + align-items: center; + justify-content: center; + color: var(--ifm-color-emphasis-600); +} +.liveChart__caption { + margin: 0 0 1.5rem; + font-size: 0.85rem; + text-align: right; +} diff --git a/internal/engine/server/http.go b/internal/engine/server/http.go index 58a241d..641c2e8 100644 --- a/internal/engine/server/http.go +++ b/internal/engine/server/http.go @@ -30,13 +30,14 @@ import ( // embedded copy. Downloaded raw cells are cached under cacheDir/ENC_ROOT. The // zero value is not usable; use New. type Server struct { - assetsDir string // optional on-disk asset override (dev); "" → embedded only - cacheDir string // XDG cache root: REGENERABLE baked tile sets (NOAA//*.pmtiles) - dataDir string // XDG data root: SOURCE ENC (district zips, raw cells) — safe, not auto-deleted - allowRemote bool - share shareStore // latest "share my view" snapshot (camera + cell list) - settings settingsStore // persisted client display settings (/client-settings.json) - Version string // build version + assetsDir string // optional on-disk asset override (dev); "" → embedded only + assetsFallback string // secondary on-disk asset root (emitted S-101 assets), searched after assetsDir, before embedded + cacheDir string // XDG cache root: REGENERABLE baked tile sets (NOAA//*.pmtiles) + dataDir string // XDG data root: SOURCE ENC (district zips, raw cells) — safe, not auto-deleted + allowRemote bool + share shareStore // latest "share my view" snapshot (camera + cell list) + settings settingsStore // persisted client display settings (/client-settings.json) + Version string // build version sets *tileSets // registry of ENABLED tile sets served at /tiles/{set}/… imports *importJobs // background server-side bake jobs (POST /api/import) @@ -361,8 +362,15 @@ func isCellName(s string) bool { return true } +// SetAssetFallback registers a secondary on-disk asset root, searched AFTER the +// primary --assets dir and BEFORE the embedded bundle. Used to serve the freshly +// emitted S-101 client assets (sprite/colortables/…) from a temp dir without +// shadowing an explicit --assets directory (e.g. a prebaked widget bundle that +// already carries its own copies). Pass "" to disable. +func (s *Server) SetAssetFallback(dir string) { s.assetsFallback = dir } + // serveAsset serves a static web asset: an on-disk --assets override (if set and -// present) or the embedded bundle. +// present), then the emitted S-101 asset fallback, then the embedded bundle. func (s *Server) serveAsset(w http.ResponseWriter, r *http.Request) { rel := r.URL.Path if rel == "" || rel == "/" { @@ -384,6 +392,17 @@ func (s *Server) serveAsset(w http.ResponseWriter, r *http.Request) { return } } + // Secondary on-disk root: the freshly-emitted S-101 client assets (sprite/ + // colortables/…). Searched only after the primary --assets dir so an explicit + // bundle's own files win, and before the embedded copy so a `make`-less serve + // still gets the generated assets. + if s.assetsFallback != "" { + full := filepath.Join(s.assetsFallback, filepath.FromSlash(name)) + if fi, err := os.Stat(full); err == nil && !fi.IsDir() { + s.serveFile(w, r, full, rel) + return + } + } s.serveEmbedded(w, r, name, rel) } diff --git a/scripts/fetch-demo-cells.sh b/scripts/fetch-demo-cells.sh new file mode 100755 index 0000000..66d5359 --- /dev/null +++ b/scripts/fetch-demo-cells.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Download the curated NOAA ENC cells for the read-only Annapolis demo, one per +# usage band. NOAA publishes no overview (band 1) or berthing (band 6) cell over +# this spot, so the set is bands 2-5 (general -> harbor); the general band renders +# the zoomed-out view, so the full zoom range still works on little disk. +# +# Idempotent: a cell already present in the cache dir is skipped, so CI can cache +# the directory across runs. Driven by `make demo`; override via env: +# DEMO_CELLS space-separated NOAA cell IDs (default below) +# DEMO_CACHE output dir (default ./.demo-cache) +# NOAA_URL_BASE download host (default https://www.charts.noaa.gov/ENCs) +set -euo pipefail + +BASE="${NOAA_URL_BASE:-https://www.charts.noaa.gov/ENCs}" +OUT="${DEMO_CACHE:-.demo-cache}" +CELLS="${DEMO_CELLS:-US2EC03M US3EC08M US4MD1DC US5MD1MC}" + +mkdir -p "$OUT" +for c in $CELLS; do + if [ -s "$OUT/$c.zip" ]; then + echo "cached $c" + continue + fi + echo "fetch $c.zip" + curl -fSL --retry 3 -o "$OUT/$c.zip" "$BASE/$c.zip" +done + +echo "demo cells ready in $OUT:" +ls -1 "$OUT" diff --git a/scripts/shot-s64.mjs b/scripts/shot-s64.mjs index 471fa85..ab37c29 100644 --- a/scripts/shot-s64.mjs +++ b/scripts/shot-s64.mjs @@ -1,7 +1,7 @@ // Render an S-64 test cell headlessly with explicit mariner settings, for // comparing our portrayal against the S-64 reference plots. Presets localStorage // (mariner settings, day scheme, NOAA agreement) BEFORE the app boots, points the -// app at a single-cell catalog (?prod&catalog=…) which auto-frames the cell, then +// app at a single-cell catalog (?widget&catalog=…) which auto-frames the cell, then // screenshots the clean map after the wasm baker settles. // // Usage: node scripts/shot-s64.mjs [w] [h] [settle-ms] diff --git a/web/demo.html b/web/demo.html new file mode 100644 index 0000000..d920441 --- /dev/null +++ b/web/demo.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + chartplotter — live demo (Annapolis) + + + + + +
Loading charts…
+ + + + + + diff --git a/web/index.html b/web/index.html index 13e7946..ed10151 100644 --- a/web/index.html +++ b/web/index.html @@ -62,7 +62,7 @@ `.000`; the cells are uploaded and the server bakes them (POST /api/import) into a tile set the renderer reads from /tiles/{set}. Every S-52 mariner setting is then applied client-side. A hosted deployment can instead serve - prebaked per-region `.pmtiles` (the `prod` mode / `pmtiles=` attribute). + prebaked per-region `.pmtiles` (the `widget` mode / `pmtiles=` attribute). Prereqs in this dir: colortables/sprite/linestyles/patterns + glyphs (`chartplotter --emit-assets web`), vendor/maplibre-gl.*, basemap/coastline.geojson (`scripts/fetch-basemap.sh`), diff --git a/web/src/chartplotter.mjs b/web/src/chartplotter.mjs index 995ec45..e7fb5a4 100644 --- a/web/src/chartplotter.mjs +++ b/web/src/chartplotter.mjs @@ -278,17 +278,18 @@ export class ChartPlotter extends HTMLElement { } async boot() { - // Prod (prebaked) mode: enabled by the `prod` attribute OR `?prod` (so a dev - // build can be tested without rebuilding). Reflect it as the attribute so the - // :host([prod]) styles apply either way. - this._prod = this.hasAttribute("prod") || new URLSearchParams(location.search).has("prod"); - if (this._prod) this.setAttribute("prod", ""); + // Widget (read-only, prebaked) mode: a static, embeddable chart viewer with no + // backend and no in-browser baking. Enabled by the `widget` attribute OR + // `?widget` (so a dev build can be tested without rebuilding). Reflect it as the + // attribute so the :host([widget]) styles apply either way. + this._widget = this.hasAttribute("widget") || new URLSearchParams(location.search).has("widget"); + if (this._widget) this.setAttribute("widget", ""); // Display settings (scheme · basemap · mariner toggles · cell-boundary toggle · // bands-off) are persisted SERVER-side so every screen pointed at this boat's // server shares them and they survive a restart. Adopt them BEFORE the renderer // is configured below, overriding the localStorage values the constructor seeded - // (localStorage stays as the offline cache). Prod (offline pmtiles) has no server. - if (!this._prod) await this._loadServerSettings(); + // (localStorage stays as the offline cache). The widget viewer (offline pmtiles) has no server. + if (!this._widget) await this._loadServerSettings(); this.renderChrome(); // What's already stored? We do NOT eagerly load it (that's the slow part on @@ -331,7 +332,7 @@ export class ChartPlotter extends HTMLElement { // .pmtiles archive path (the plotter is shell-owned). this._chartLib = this.shadowRoot.getElementById("chart-lib"); if (this._chartLib) { - this._chartLib.configure({ dl: this._dl, api: this._api, notify: this._notify, store: this._store, assets: this._assets, prod: this._prod }); + this._chartLib.configure({ dl: this._dl, api: this._api, notify: this._notify, store: this._store, assets: this._assets, widget: this._widget }); this._chartLib.setHiddenCells([...this._hiddenCells]); // seed checkbox state from persisted prefs this._chartLib.addEventListener("charts-changed", () => { this._renderInstalledSets().catch(() => {}); }); // Per-cell show/hide: apply the client filter to the live map + persist. @@ -352,7 +353,12 @@ export class ChartPlotter extends HTMLElement { // themselves as the first NON-core contribution (a DevTools instance built in // onReady, once the map exists — see below); plugins will register here too. this._settingsRegistry = new SettingsRegistry(); - for (const c of coreSettingsContributions(this)) this._settingsRegistry.register(c); + for (const c of coreSettingsContributions(this)) { + // The widget viewer is read-only: drop the Advanced tab (its only entry is the + // cell-boundary toggle; the dev tools never register in widget mode anyway). + if (this._widget && c.id === "core-advanced") continue; + this._settingsRegistry.register(c); + } this._settingsDlg = this.shadowRoot.getElementById("settings-dlg"); if (this._settingsDlg) this._settingsDlg.configure({ registry: this._settingsRegistry }); @@ -385,12 +391,12 @@ export class ChartPlotter extends HTMLElement { if (!["coastline", "osm", "osmvec", "none"].includes(this._basemap)) this._basemap = "coastline"; if (this._basemap === "osmvec" && !this._osmVecUrl) this._basemap = "coastline"; // vector not configured plotter.setAttribute("basemap", this._basemap); - // Render source. Prod (hosted): prebaked per-region .pmtiles, loaded by + // Render source. Widget (hosted): prebaked per-region .pmtiles, loaded by // restoreArchive through the renderer's pmtiles path — no tile server needed. // Local serve: server-baked MVT from /tiles/{set} (server mode); imported / // downloaded cells are baked on the server (POST /api/import) and the renderer // is pointed at the resulting set (see _refreshCharts). - if (!this._prod) plotter.setAttribute("tiles", "server"); + if (!this._widget) plotter.setAttribute("tiles", "server"); this._plotter = plotter; this.shadowRoot.getElementById("map").appendChild(plotter); @@ -483,7 +489,7 @@ export class ChartPlotter extends HTMLElement { // external files); load it so the pick report can show that content inline. this._aux = new AuxStore(); const dl = this._dl.loadCatalog().then(async (r) => { - // Prod/hosted: a companion aux.zip named in the manifest (fetched whole once). + // Widget/hosted: a companion aux.zip named in the manifest (fetched whole once). // Server: per-file on demand via GET api/aux — the raw zip is never exposed. if (this._dl.auxUrl) await this._aux.load(this._dl.auxUrl); else await this._aux.loadApi(this._assets); @@ -532,9 +538,9 @@ export class ChartPlotter extends HTMLElement { // sticks and you can magnify past the floor (the 1:900 over-zoom). map.on("moveend", () => this._applyScaleFloor()); await this.restoreArchive(); - // Local serve: render every baked pack the server holds (survives reload). Prod + // Local serve: render every baked pack the server holds (survives reload). The widget viewer // already loaded its prebaked archives in restoreArchive() above. - if (!this._prod) { + if (!this._widget) { try { await this._renderInstalledSets(); } catch (e) { console.warn("[charts] initial render", e); } } this._applyBandsOff(); // re-apply any persisted band on/off now that chart layers exist @@ -587,7 +593,7 @@ export class ChartPlotter extends HTMLElement { // settings registry. A plain class (like the map controllers), built now that // the map exists; it registers itself as the Advanced-tab contribution and owns // the rebake + feature-inspector tools (and their map listeners). Dev-only. - if (!this._prod) { + if (!this._widget) { this._devTools = new DevTools({ registry: this._settingsRegistry, map, @@ -614,8 +620,8 @@ export class ChartPlotter extends HTMLElement { // connection manager). VesselStateStore streams /api/vessel for the render // plugins (own-ship/AIS/HUD); ConnectionsController contributes the // Connections settings tab for managing data sources. - if (!this._prod) { - this._vessel = new VesselStateStore({ assets: this._assets, prod: this._prod }); + if (!this._widget) { + this._vessel = new VesselStateStore({ assets: this._assets, widget: this._widget }); this._vessel.start(); this._connections = new ConnectionsController({ registry: this._settingsRegistry, @@ -632,7 +638,7 @@ export class ChartPlotter extends HTMLElement { // re-centre chip mounted in the shell chrome) and streams fixes to the camera. this._ownShip = new OwnShip({ map, plotter: this._plotter, vessel: this._vessel, host: this.shadowRoot, onSelect: showInfo, units: () => this._mariner }); // AIS targets (other vessels) from the live feed. - this._ais = new AISOverlay({ map, assets: this._assets, prod: this._prod, onSelect: showInfo, units: () => this._mariner }); + this._ais = new AISOverlay({ map, assets: this._assets, widget: this._widget, onSelect: showInfo, units: () => this._mariner }); } // Persist the view so a refresh resumes where you were; refresh the coverage @@ -849,7 +855,7 @@ export class ChartPlotter extends HTMLElement { // popup" when entering via the welcome "Browse chart regions" button). r.getElementById("drawer").classList.toggle("wide", true); r.getElementById("drawer").classList.toggle("set-wide", false); - r.getElementById("dtitle").textContent = this._prod ? "Add charts" : "Chart library"; + r.getElementById("dtitle").textContent = this._widget ? "Add charts" : "Chart library"; r.getElementById("empty").hidden = true; if (this._chartLib) this._chartLib.show(provider); // no default provider — nothing selected until the user picks this.setDrawerOpen(true); @@ -1490,7 +1496,7 @@ export class ChartPlotter extends HTMLElement { // Re-bake the local OPFS store on the server (the User-Charts import path). The // component owns this; the shell's debug tools delegate to it. - // Returns a resolved promise when there's no component yet (boot/prod guards). + // Returns a resolved promise when there's no component yet (boot/widget guards). _refreshCharts() { return this._chartLib ? this._chartLib._refreshCharts() : Promise.resolve(); } @@ -1536,12 +1542,12 @@ export class ChartPlotter extends HTMLElement { _refreshInstalledBounds() { if (!this._coverage) return; // coverage overlay not set up yet const feats = []; - // Per-CELL footprints are the prod (pmtiles) path only. In SERVER mode we draw + // Per-CELL footprints are the widget (pmtiles) path only. In SERVER mode we draw // one box per ENABLED pack (below) instead — a full NOAA install has thousands // of cells, and a box per cell (re-projected to its min on-screen size on every // zoom frame) would freeze the map. Per-cell boxes also ignore the enabled flag, // so they'd keep showing a disabled district's coverage. Per-pack boxes fix both. - if (this._prod) { + if (this._widget) { for (const name of this._installed) { const bb = this._cellLocation(name); // catalog footprint if (!bb) continue; @@ -1697,7 +1703,7 @@ export class ChartPlotter extends HTMLElement { // Fetch the server-persisted display settings at boot and adopt them over the // localStorage values the constructor seeded. Best-effort: an older server, an - // offline/prod load, or a malformed blob just keeps the local values. + // offline/widget load, or a malformed blob just keeps the local values. async _loadServerSettings() { let s = null; try { @@ -1717,10 +1723,10 @@ export class ChartPlotter extends HTMLElement { } // Persist the display settings server-side (shared across screens). Debounced so - // a flurry of toggles coalesces into one POST. Server mode only — prod has no + // a flurry of toggles coalesces into one POST. Server mode only — the widget viewer has no // server, and localStorage already holds the per-screen copy. _persistSettings() { - if (this._prod) return; + if (this._widget) return; clearTimeout(this._settingsSaveT); this._settingsSaveT = setTimeout(() => { fetch(`${this._assets}api/settings`, { @@ -1862,7 +1868,7 @@ export class ChartPlotter extends HTMLElement { r.querySelectorAll(".panel").forEach((p) => p.classList.toggle("sel", p.dataset.panel === name)); r.getElementById("drawer").classList.toggle("wide", name === "charts"); // two-pane list+map r.getElementById("drawer").classList.toggle("set-wide", name === "settings"); // rail + content - r.getElementById("dtitle").textContent = name === "settings" ? "Settings" : (this._prod ? "Add charts" : "Chart library"); + r.getElementById("dtitle").textContent = name === "settings" ? "Settings" : (this._widget ? "Add charts" : "Chart library"); // Charts = the panel; open it on its current provider (none // selected by default — the user picks a source). if (name === "charts" && this._chartLib) this._chartLib.show(this._chartLib._selProvider); @@ -1943,7 +1949,7 @@ export class ChartPlotter extends HTMLElement { _hasInstalledPacks() { return !!(this._installedSets && this._installedSets.size); } // Installed packs exist but none render (all disabled) → nothing on the map. - _noChartsEnabled() { return !this._prod && this._hasInstalledPacks() && !this._hasArchive; } + _noChartsEnabled() { return !this._widget && this._hasInstalledPacks() && !this._hasArchive; } // The archive-list rendering + file-import wiring (renderArchiveList / // _wireImport) moved into , which owns the User-Charts import UI. diff --git a/web/src/chartplotter.view.mjs b/web/src/chartplotter.view.mjs index 7f30692..74ea6d6 100644 --- a/web/src/chartplotter.view.mjs +++ b/web/src/chartplotter.view.mjs @@ -77,11 +77,14 @@ export const STYLE = ` .rbtn:active { transform:scale(.94); } .rbtn.on { background:var(--ui-accent); color:var(--ui-accent-text); border-color:var(--ui-accent); } .rbtn svg { width:21px; height:21px; display:block; } - /* Prod / prebaked deployment: charts load from a configured hosted archive - (pmtiles="…" / catalog="…"); there's no NOAA download and no Dev tools. - The Charts button stays but its panel becomes import-only (drop your own - ENC, baked server-side) — see renderCharts. */ - :host([prod]) #empty-add, :host([prod]) #empty .welcome-sub { display:none; } + /* Widget mode: a read-only, embeddable chart viewer (a "CDN"/widget deploy). + Charts load from a configured hosted archive (pmtiles="…" / catalog="…"); + there's no backend, no NOAA download, no in-browser baking, no Dev tools. + It strips the management chrome down to a pure viewer: hide the Charts + (library/import) and Share buttons, and the welcome card's add/import + affordances. Settings drop the Advanced tab (gated in JS). */ + :host([widget]) #empty-add, :host([widget]) #empty .welcome-sub { display:none; } + :host([widget]) #charts-btn, :host([widget]) #share-btn { display:none; } .box-sel { position:absolute; z-index:5; border:2px solid var(--ui-accent); background:rgba(21,101,192,.12); pointer-events:none; } /* charts panel: action header + "your charts" cards */ .charts-actions { display:flex; gap:8px; margin-bottom:10px; } diff --git a/web/src/core/core-settings.mjs b/web/src/core/core-settings.mjs index c6eecfa..8a75009 100644 --- a/web/src/core/core-settings.mjs +++ b/web/src/core/core-settings.mjs @@ -144,8 +144,8 @@ export function coreSettingsContributions(app) { // inspector / coverage / bands / diagnostics) are rendered by the SHELL in its // own shadow (#dev-region) because _renderDevPanel / _renderInspect reach into // the shell's shadow by id; keeping them there avoids a risky refactor (option - // B). The shell reveals #dev-region only when this tab is active. In prod there - // are no dev tools, so the tab is just the toggle. + // B). The shell reveals #dev-region only when this tab is active. In the widget + // viewer there are no dev tools, so the tab is just the toggle. const advanced = { id: "core-advanced", tab: { id: "advanced", label: "Advanced" }, diff --git a/web/src/core/settings-store.mjs b/web/src/core/settings-store.mjs index ae12032..d877566 100644 --- a/web/src/core/settings-store.mjs +++ b/web/src/core/settings-store.mjs @@ -6,7 +6,7 @@ // app's own display settings and each plugin's settings live side by side in one // store without colliding: // -// const settings = new SettingsStore({ assets, prod }); +// const settings = new SettingsStore({ assets, widget }); // settings.load(); // overlay the server copy at boot // const core = settings.ns("core"); // the app's display settings // const ais = settings.ns("ais"); // a plugin's settings @@ -22,19 +22,19 @@ const LS_KEY = "chartplotter:settings"; export class SettingsStore { - constructor({ assets = "", prod = false } = {}) { + constructor({ assets = "", widget = false } = {}) { this._assets = assets; - this._prod = prod; + this._widget = widget; this._saveT = 0; // Seed instantly from the localStorage mirror so boot has values before the - // server responds (and so prod, which has no server, works at all). + // server responds (and so widget, which has no server, works at all). this._blob = readLocal(); } // Overlay the server-persisted copy (server mode). Best-effort: offline / older - // server / prod just keeps the local values. Returns the merged blob. + // server / widget just keeps the local values. Returns the merged blob. async load() { - if (this._prod) return this._blob; + if (this._widget) return this._blob; try { const r = await fetch(`${this._assets}api/settings`, { cache: "no-store" }); if (r.ok) { @@ -67,7 +67,7 @@ export class SettingsStore { // Mirror locally now; debounce the server POST so a flurry of toggles coalesces. _dirty() { writeLocal(this._blob); - if (this._prod) return; // no server in prod; localStorage is the store + if (this._widget) return; // no server in widget; localStorage is the store clearTimeout(this._saveT); this._saveT = setTimeout(() => { fetch(`${this._assets}api/settings`, { diff --git a/web/src/data/chart-downloader.mjs b/web/src/data/chart-downloader.mjs index 1ee44dd..e3dc302 100644 --- a/web/src/data/chart-downloader.mjs +++ b/web/src/data/chart-downloader.mjs @@ -49,9 +49,15 @@ export class ChartDownloader { const man = fetch(manUrl) .then((r) => (r.ok ? r.json() : null)) .then((j) => { - this.districts = (j && j.districts) || []; - // The aux zip sits beside the manifest; resolve its basename against manUrl. - this.auxUrl = j && j.aux ? manUrl.replace(/[^/]*$/, j.aux) : ""; + // Manifest paths (the district archives and the aux zip) are relative to the + // MANIFEST, not to the page. Resolve them against manUrl so a widget embedded + // on a page in a DIFFERENT directory (assets ≠ the page's dir, e.g. a chart + // dropped onto a docs page) still finds the .pmtiles — otherwise the renderer + // resolves the bare filenames against the page URL and 404s. + const manAbs = new URL(manUrl, location.href); + this.districts = ((j && j.districts) || []).map((d) => + d && d.file ? { ...d, file: new URL(d.file, manAbs).href } : d); + this.auxUrl = j && j.aux ? new URL(j.aux, manAbs).href : ""; }) .catch(() => { this.districts = []; this.auxUrl = ""; }); return Promise.all([cat, man]); diff --git a/web/src/data/vessel-state-store.mjs b/web/src/data/vessel-state-store.mjs index 42ae777..5840e92 100644 --- a/web/src/data/vessel-state-store.mjs +++ b/web/src/data/vessel-state-store.mjs @@ -5,13 +5,13 @@ // no DOM and renders nothing — mirroring the server-side "a source, not a // renderer" split. // -// The feed requires the Go server, so in prod (server-less, prebaked) mode it +// The feed requires the Go server, so in widget (server-less, prebaked) mode it // stays idle. Where EventSource is unavailable it falls back to polling. export class VesselStateStore { - constructor({ assets = "/", prod = false } = {}) { + constructor({ assets = "/", widget = false } = {}) { this._assets = assets; - this._prod = prod; + this._widget = widget; this._state = {}; this._listeners = new Set(); this._es = null; @@ -39,9 +39,9 @@ export class VesselStateStore { } } - /** Begin streaming. No-op in prod (no server feed) or if already started. */ + /** Begin streaming. No-op in widget (no server feed) or if already started. */ start() { - if (this._prod || this._es || this._polling) return; + if (this._widget || this._es || this._polling) return; if (!window.EventSource) { this._poll(); return; diff --git a/web/src/plugins/ais-overlay.mjs b/web/src/plugins/ais-overlay.mjs index 45818ce..a7871da 100644 --- a/web/src/plugins/ais-overlay.mjs +++ b/web/src/plugins/ais-overlay.mjs @@ -27,10 +27,10 @@ const GLYPH_STYLE = "filter:drop-shadow(0 0 1px var(--ais-halo,#fff)) drop-shadow(0 0 1px var(--ais-halo,#fff));"; export class AISOverlay { - constructor({ map, assets = "/", prod = false, onSelect, units } = {}) { + constructor({ map, assets = "/", widget = false, onSelect, units } = {}) { this._map = map; this._assets = assets; - this._prod = prod; + this._widget = widget; this._units = units; // () => mariner prefs, for SOG/CPA/draught units (live) this._onSelect = onSelect; // tap → info picker this._markers = new Map(); // mmsi -> {marker, el, hasDir} @@ -40,7 +40,7 @@ export class AISOverlay { } start() { - if (this._prod || this._es || this._polling) return; // AIS feed needs the server + if (this._widget || this._es || this._polling) return; // AIS feed needs the server if (!window.EventSource) { this._poll(); return; diff --git a/web/src/plugins/chart-library.mjs b/web/src/plugins/chart-library.mjs index 553cddb..08997da 100644 --- a/web/src/plugins/chart-library.mjs +++ b/web/src/plugins/chart-library.mjs @@ -30,7 +30,7 @@ import { esc, fmtIssue } from "../lib/util.mjs"; import { readCentralDirectory, cellEntries, extractEntry } from "../data/zip-import.mjs"; import { seaColor, landColor, coastColor } from "../chart-canvas/s52-style.mjs"; // our own basemap palette (consistent with the chart) import { - STYLE, prodBody, libraryBody, packSearch, providersCol, packsHeader, + STYLE, widgetBody, libraryBody, packSearch, providersCol, packsHeader, packBadge, userPackRow, packRow, packsCol, emptyRow, downloadBtn, detailEmpty, detailUnknownSet, detailPack, installedActions, previewMapHost, importDetail, dataFreshness, agreementModal, archiveList, millerBack, packCellList, @@ -130,7 +130,7 @@ export class ChartLibrary extends HTMLElement { this._previewKey = null; // pack key the detail preview currently targets this._snapEl = null; this._snapMap = null; // in-flight off-screen snapshot map this._lightbox = null; this._lightboxKey = null; // tap-to-enlarge overlay - this._prod = false; // prod (prebaked) build: import-only Library + this._widget = false; // widget (prebaked) build: import-only Library this._active = false; // is the charts UI currently shown? } @@ -151,15 +151,15 @@ export class ChartLibrary extends HTMLElement { if (this._previewMap) { try { this._previewMap.remove(); } catch (e) { /* ignore */ } this._previewMap = null; } } - // Inject dependencies (call once after creation). `prod` flips the Library to - // import-only (no NOAA download/region picker), matching the shell's prod mode. - configure({ dl, api, notify, store, assets, prod } = {}) { + // Inject dependencies (call once after creation). `widget` flips the Library to + // import-only (no NOAA download/region picker), matching the shell's widget mode. + configure({ dl, api, notify, store, assets, widget } = {}) { this._dl = dl || null; this._api = api || null; this._notify = notify || null; this._store = store || null; if (assets) this._assets = assets; - this._prod = !!prod; + this._widget = !!widget; return this; } @@ -273,13 +273,13 @@ export class ChartLibrary extends HTMLElement { } // -- rendering ------------------------------------------------------------ - // The whole charts panel. Prod (prebaked) builds get the import-only Library; + // The whole charts panel. Widget (prebaked) builds get the import-only Library; // otherwise the 3-pane drill-down + search + preview. render() { const el = this.shadowRoot.getElementById("body"); if (!el) return; - if (this._prod) { - el.innerHTML = prodBody(); + if (this._widget) { + el.innerHTML = widgetBody(); this._wireImport(); return; } diff --git a/web/src/plugins/chart-library.view.mjs b/web/src/plugins/chart-library.view.mjs index b343ab0..b08a14c 100644 --- a/web/src/plugins/chart-library.view.mjs +++ b/web/src/plugins/chart-library.view.mjs @@ -163,9 +163,9 @@ export const STYLE = ` } `; -// The prod (prebaked) Library body: import-only — no NOAA download / region +// The widget (prebaked) Library body: import-only — no NOAA download / region // picker. Wired by _wireImport via #file/#drop/#pick. -export function prodBody() { +export function widgetBody() { return `

Add your own charts — drop a NOAA .zip / .000, or a baked .pmtiles. They're baked right here in your browser and kept offline alongside the prebaked charts.

Drop a .zip, .000 or .pmtiles here, or
@@ -174,7 +174,7 @@ export function prodBody() {
`; } -// The full (non-prod) Library body: search box + 3-pane miller + freshness +// The full (non-widget) Library body: search box + 3-pane miller + freshness // footer. The three columns are passed in pre-rendered (the logic computes the // data they need). export function libraryBody({ searchHtml, providersCol, packsCol, detailCol, freshnessHtml, level, backLabel }) {