Skip to content

Commit 266f3ef

Browse files
authored
Merge pull request #33 from beetlebugorg/fix/docs-embed-clean-defaults
fix(web): make widget/embed mode hermetic; pin the docs demo defaults
2 parents 1a10897 + 45f741f commit 266f3ef

2 files changed

Lines changed: 68 additions & 13 deletions

File tree

docs/src/components/LiveChart.js

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, {useEffect} from 'react';
1+
import React, {useEffect, useRef} from 'react';
22
import BrowserOnly from '@docusaurus/BrowserOnly';
33
import useBaseUrl from '@docusaurus/useBaseUrl';
44

@@ -7,11 +7,27 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
77
// catalog) load from the prebaked demo bundle the docs build assembles under
88
// /<baseUrl>/demo/ — see `make demo` and .github/workflows/docs.yml. Build it
99
// locally first with: make demo DEMO_OUT=docs/static/demo
10+
11+
// The demo opens on Annapolis harbour at 1:6090 (a detailed harbour view). Widget
12+
// mode is HERMETIC — it ignores localStorage and boots from DEFAULT_MARINER — so the
13+
// non-default display state we want for the demo (Display Other + scale boundaries)
14+
// is forced at ready, and the scale is pinned via the zoom attribute below.
15+
const CENTER = [-76.48167, 38.975]; // Annapolis, MD — 38°58.5′N 076°28.9′W
16+
const SCALE = 6090; // display scale denominator (1:6090)
17+
// scale → MapLibre zoom (512-tile resolution, default 0.2645 mm CSS pixel — the
18+
// widget's DEFAULT_PX_PITCH_MM), so scaleDenomPhysical reads ~1:6090 in the HUD.
19+
const M_PER_PX_Z0 = 78271.516964020485;
20+
const PX_PITCH_M = 0.0002645;
21+
const ZOOM = Math.log2(
22+
(M_PER_PX_Z0 * Math.cos((CENTER[1] * Math.PI) / 180)) / (PX_PITCH_M * SCALE),
23+
);
24+
1025
function Chart() {
1126
// useBaseUrl prefixes the site baseUrl, e.g. "/chartplotter/demo/". The widget
1227
// resolves ALL of its assets (incl. vendor/maplibre-gl.js and charts-index.json)
1328
// relative to this, so the whole demo is self-contained in that one directory.
1429
const base = useBaseUrl('/demo/');
30+
const ref = useRef(null);
1531
useEffect(() => {
1632
const id = 'chartplotter-widget-module';
1733
if (document.getElementById(id)) return; // define <chart-plotter> once
@@ -21,11 +37,36 @@ function Chart() {
2137
s.src = `${base}src/chartplotter.mjs`;
2238
document.head.appendChild(s);
2339
}, [base]);
40+
41+
// Once the map is ready, force the demo's display state (widget mode is hermetic, so
42+
// these aren't persisted): Display category Other on, chart scale boundaries on.
43+
useEffect(() => {
44+
let tries = 0;
45+
const iv = setInterval(() => {
46+
const el = ref.current;
47+
if (el && el.map) {
48+
clearInterval(iv);
49+
if (typeof el.applyMariner === 'function') {
50+
try { el.applyMariner({displayOther: true, showScaleBoundaries: true}); } catch (e) { /* best-effort */ }
51+
}
52+
} else if (++tries > 60) {
53+
clearInterval(iv);
54+
}
55+
}, 200);
56+
return () => clearInterval(iv);
57+
}, []);
58+
2459
return (
2560
<>
2661
<div className="liveChart">
2762
{/* widget = read-only viewer; assets points every fetch at the demo bundle */}
28-
<chart-plotter widget="" assets={base} center="-76.482,38.978" zoom="13" />
63+
<chart-plotter
64+
ref={ref}
65+
widget=""
66+
assets={base}
67+
center={CENTER.join(',')}
68+
zoom={ZOOM.toFixed(3)}
69+
/>
2970
</div>
3071
{/* Plain <a> (not a router Link) → full-page nav to the static bundle. */}
3172
<p className="liveChart__caption">

web/src/chartplotter.mjs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,15 @@ export class ChartPlotter extends HTMLElement {
229229
this._pxPitch = loadJSON(LS_PX_PITCH, undefined); // calibrated CSS-pixel pitch (mm); undefined → util default (CSS reference)
230230
// Feature-inspect + tile-debugger state now live in DevTools (Advanced tab).
231231
this._hasArchive = false; // is a chart archive currently loaded?
232-
this._mariner = { ...DEFAULT_MARINER, ...loadJSON(LS_MARINER, {}) };
232+
// Widget (embed) mode is HERMETIC for display settings: it must not read or write
233+
// the shared localStorage scheme/basemap/mariner. Several embeds share one origin
234+
// (the docs intro demo + the Chart 1 reference page), so persisting would let one
235+
// clobber another — e.g. the reference page forcing dataQuality:true would leak
236+
// onto the intro demo on its next load. Embeds boot from DEFAULT_MARINER and set
237+
// their own state via applyMariner/applyScheme at ready. (boot() also sets
238+
// this._widget the same way, before any apply* runs.)
239+
const embed = this.hasAttribute("widget") || new URLSearchParams(location.search).has("widget");
240+
this._mariner = { ...DEFAULT_MARINER, ...(embed ? {} : loadJSON(LS_MARINER, {})) };
233241
// Migrate the old single-value display category (base|standard|other) to
234242
// the multi-select Base/Standard/Other booleans (now client-side filters).
235243
if (this._mariner.displayCategory) {
@@ -250,8 +258,8 @@ export class ChartPlotter extends HTMLElement {
250258
// S-52 §10.2: Display Base is the minimum safe-navigation set and can never
251259
// be deselected. Force it on regardless of any (stale) persisted value.
252260
this._mariner.displayBase = true;
253-
this._scheme = localStorage.getItem(LS_SCHEME) || "day";
254-
if (!SCHEMES.includes(this._scheme)) this._scheme = "day"; // fall back if the persisted scheme isn't a known one
261+
this._scheme = (embed ? this.getAttribute("scheme") : localStorage.getItem(LS_SCHEME)) || "day";
262+
if (!SCHEMES.includes(this._scheme)) this._scheme = "day"; // fall back if the persisted/attr scheme isn't a known one
255263
// The provision job is a SERVER task: `_task` mirrors GET /api/tasks (polled,
256264
// never invented), `_taskMeta` holds the client-only label hints (which region,
257265
// which verb) the server doesn't know. `_poll` is the polling interval handle.
@@ -393,14 +401,17 @@ export class ChartPlotter extends HTMLElement {
393401
}
394402

395403
const plotter = document.createElement("chart-canvas");
396-
const view = shareView || loadJSON(LS_VIEW, null); // resume the last view → load in-region
404+
// Embeds are hermetic (see constructor): never resume a persisted view — always
405+
// boot from the `center`/`zoom` attributes (the docs intro demo pins Annapolis),
406+
// so the Chart 1 reference page's view can't leak onto the intro demo.
407+
const view = shareView || (this._widget ? null : loadJSON(LS_VIEW, null)); // resume the last view → load in-region
397408
plotter.setAttribute("center", view ? view.center.join(",") : (this.getAttribute("center") || "-76.4875,38.975"));
398409
plotter.setAttribute("zoom", String(view ? view.zoom : (this.getAttribute("zoom") || 11)));
399410
if (this.hasAttribute("cell-url")) plotter.setAttribute("cell-url", this.getAttribute("cell-url"));
400411
plotter.setAttribute("assets", this._assets);
401412
this._osmVecUrl = this._cfg("osm-pmtiles"); // hosted OSM vector basemap archive (enables the "Vector" option)
402413
if (this._osmVecUrl) plotter.setAttribute("osm-pmtiles", this._osmVecUrl);
403-
this._basemap = this._serverBasemap || localStorage.getItem(LS_BASEMAP) || this.getAttribute("basemap") || "coastline";
414+
this._basemap = this._serverBasemap || (this._widget ? null : localStorage.getItem(LS_BASEMAP)) || this.getAttribute("basemap") || "coastline";
404415
if (!["coastline", "osm", "osmvec", "none"].includes(this._basemap)) this._basemap = "coastline";
405416
if (this._basemap === "osmvec" && !this._osmVecUrl) this._basemap = "coastline"; // vector not configured
406417
plotter.setAttribute("basemap", this._basemap);
@@ -769,7 +780,9 @@ export class ChartPlotter extends HTMLElement {
769780
loaded = true;
770781
const frames = [...(regions || [])];
771782
if (this._userBake && this._userBake.bounds && !this._isWorldBounds(this._userBake.bounds)) frames.push({ bounds: this._userBake.bounds });
772-
if (frames.length && !loadJSON(LS_VIEW, null)) this._frameRegionArchives(frames);
783+
// Embeds keep their pinned center/zoom (Annapolis) — don't auto-frame the
784+
// loaded region and don't consult the persisted view (hermetic; see constructor).
785+
if (frames.length && !this._widget && !loadJSON(LS_VIEW, null)) this._frameRegionArchives(frames);
773786
}
774787
}
775788
if (loaded) { this.updateEmptyState(); return; }
@@ -784,7 +797,7 @@ export class ChartPlotter extends HTMLElement {
784797
// answers "is the centre covered at all", which is all we need here.)
785798
_frameInitial() {
786799
// (the cell-picker "charts mode" was removed — this is just the no-saved-view guard)
787-
if (loadJSON(LS_VIEW, null) || !this._districts.length) return;
800+
if (this._widget || loadJSON(LS_VIEW, null) || !this._districts.length) return; // embeds keep their pinned view
788801
const c = this._map.getCenter();
789802
const covered = (d) => d.bounds && c.lng >= d.bounds[0] && c.lng <= d.bounds[2] && c.lat >= d.bounds[1] && c.lat <= d.bounds[3];
790803
if (this._districts.some(covered)) return;
@@ -862,7 +875,7 @@ export class ChartPlotter extends HTMLElement {
862875
try {
863876
const arc = add ? await this._plotter.addArchive(url, "all") : await this._plotter.loadArchiveUrl(url);
864877
const b = (entry && entry.bounds) || (arc && arc.bounds);
865-
if (b && !loadJSON(LS_VIEW, null)) this._map.fitBounds([[b[0], b[1]], [b[2], b[3]]], { padding: 40, duration: 0 });
878+
if (b && !this._widget && !loadJSON(LS_VIEW, null)) this._map.fitBounds([[b[0], b[1]], [b[2], b[3]]], { padding: 40, duration: 0 }); // embeds keep their pinned view
866879
this._markArchive(entry ? { type: "url", file: entry.file } : null);
867880
return true;
868881
} catch (e) { console.warn("[archive] load", url, e); return false; }
@@ -1102,6 +1115,7 @@ export class ChartPlotter extends HTMLElement {
11021115
}
11031116

11041117
saveView() {
1118+
if (this._widget) return; // embeds are hermetic — never persist the view (see constructor)
11051119
// The cell-picker "charts mode" (whose zoomed-out framing we used to skip
11061120
// persisting) was removed; the live view is always the one to save.
11071121
const c = this._map.getCenter();
@@ -1801,7 +1815,7 @@ export class ChartPlotter extends HTMLElement {
18011815
this._scheme = name;
18021816
this._plotter.setScheme(name);
18031817
this.setAttribute("data-scheme", name);
1804-
localStorage.setItem(LS_SCHEME, name);
1818+
if (!this._widget) localStorage.setItem(LS_SCHEME, name); // embeds are hermetic (see constructor)
18051819
this._persistSettings();
18061820
this._syncSchemeUI();
18071821
}
@@ -1811,7 +1825,7 @@ export class ChartPlotter extends HTMLElement {
18111825
applyBasemap(mode) {
18121826
this._basemap = (mode === "osm" || mode === "osmvec" || mode === "none") ? mode : "coastline";
18131827
if (this._plotter) this._plotter.setBasemap(this._basemap);
1814-
localStorage.setItem(LS_BASEMAP, this._basemap);
1828+
if (!this._widget) localStorage.setItem(LS_BASEMAP, this._basemap); // embeds are hermetic (see constructor)
18151829
this._persistSettings();
18161830
}
18171831

@@ -1847,7 +1861,7 @@ export class ChartPlotter extends HTMLElement {
18471861
// re-bake), so just apply the changed key(s) and persist.
18481862
try { this._plotter.setMariner(patch); }
18491863
catch (e) { console.warn(e); }
1850-
localStorage.setItem(LS_MARINER, JSON.stringify(this._mariner));
1864+
if (!this._widget) localStorage.setItem(LS_MARINER, JSON.stringify(this._mariner)); // embeds are hermetic (see constructor)
18511865
this._persistSettings();
18521866
// Switching units relabels + reconverts the depth fields (still in metres
18531867
// under the hood), so redraw the settings panel.

0 commit comments

Comments
 (0)