Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 122 additions & 23 deletions docs/src/components/Chart1Tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import useBaseUrl from '@docusaurus/useBaseUrl';

// Chart1Tests embeds the S-52 PresLib "ECDIS Chart 1" reference sheet LIVE — one
// read-only <chart-plotter> widget — and turns the docs page into a symbol-compliance
// checker: every panel of the sheet is a row in the list; click one and the widget
// frames that panel. The whole sheet is one contiguous synthetic ENC, so navigation
// is just map.fitBounds(panel) — which fits the panel to the actual map size with
// padding that keeps the widget's own chrome (HUD, controls, scalebar) off the data.
// checker: every panel of the sheet is a row in the list; click one (or arrow up/down)
// and the widget frames that panel. The whole sheet is one contiguous synthetic ENC,
// so navigation is just map.fitBounds(panel). The widget runs in `spec` mode (no
// chrome) with the reference mariner settings forced on, so what you see is an
// apples-to-apples diff against the spec's own reference plots.
// Tiles load from the /chart1/ bundle (`make demo-chart1`); the frontend assets are
// shared with the /demo/ bundle.

Expand All @@ -18,8 +19,31 @@ const PX_PITCH_M = 0.00026458;
const zoomForScale = (scale, lat) =>
Math.log2((M_PER_PX_Z0 * Math.cos((lat * Math.PI) / 180)) / (PX_PITCH_M * scale));

// Inset the fit so the sheet clears the widget's overlaid chrome on every edge.
const PAD = {top: 48, bottom: 56, left: 48, right: 48};
// Spec mode hides all widget chrome, so the fit just needs a small, symmetric margin
// to keep edge symbology off the frame edge — like the thin border around each
// PresLib reference plot. (No asymmetric chrome padding any more.)
const PAD = {top: 18, bottom: 18, left: 18, right: 18};

// Mariner display state pinned to match the IHO PresLib reference plots — kept in
// step with the MARINER block in scripts/preslib-chart1.mjs. The widget is read-only
// here (spec mode), so the viewer can't change anything; we force these at ready so
// the render is an apples-to-apples diff against the spec's own figures. ALL symbology
// shown; data-quality overlay on (CATZOC panels); metres (IHO, not NOAA feet); the
// depth-shading demo's 0/5/10/30 contours labelled; date-dependency + meta boundaries
// shown; 25 mm sectors and symbolized boundaries (the S-52 defaults the plots use).
const MARINER = {
displayBase: true, displayStandard: true, displayOther: true,
dataQuality: true,
depthUnit: 'm',
showContourLabels: true,
shallowContour: 5, safetyContour: 10, deepContour: 30,
highlightDateDependent: true,
dateDependent: false,
showMetaBounds: true,
showFullSectorLines: false,
boundaryStyle: 'symbolized',
simplifiedPoints: false,
};

// One row per PresLib reference-plot page (Part I §16, doc pages 238–253). Bounds
// are the cells' data extents [W, S, E, N]; the harbor pages are 1:14 000, the
Expand Down Expand Up @@ -52,7 +76,7 @@ const INITIAL_SCALE = 105000; // generous pre-fit paint; fitBounds refines on re
// map so neither the whole-sheet fit (on a small map) nor a scroll can cross it.
const SCAMIN_MIN_ZOOM = zoomForScale(139000, SHEET.lat);

// Fit the map to a panel's bounds with chrome padding. Returns false if the map
// Fit the map to a panel's bounds with a symmetric margin. Returns false if the map
// isn't up yet (caller falls back to setView).
function fitPanel(el, p, animate) {
const m = el && el.map;
Expand All @@ -70,8 +94,11 @@ function Chart() {
const manifest = useBaseUrl('/chart1/charts-index.json');
const overviewImg = useBaseUrl('/img/chart1/page-238-overview.png');
const ref = useRef(null);
const listRef = useRef(null); // the <ol> of panel buttons (for focus moves)
const activeRef = useRef(SHEET); // current panel, for handlers in stale closures
const [active, setActive] = useState(238);
const [status, setStatus] = useState('checking'); // checking | ready | missing
const [full, setFull] = useState(false); // fullscreen: panel list + widget fill the viewport

// Only boot the live widget if the tile bundle is actually published. Locally
// (no `make demo-chart1`) fall back to the static overview image.
Expand All @@ -95,36 +122,93 @@ function Chart() {
return () => { cancelled = true; };
}, [demo, manifest]);

// Once the widget's map is ready, frame the whole sheet (fitBounds, not a guessed
// scale, so the entire box + labels show with margin for the chrome).
// Once the widget's map is ready: pin the reference mariner settings + Day scheme
// (spec mode is read-only — the viewer can't change them), floor the zoom past the
// SCAMIN cutoff, and frame the whole sheet.
useEffect(() => {
if (status !== 'ready') return undefined;
let tries = 0;
const iv = setInterval(() => {
const m = ref.current && ref.current.map;
if (m) {
try { m.setMinZoom(SCAMIN_MIN_ZOOM); } catch (e) { /* older map */ }
fitPanel(ref.current, SHEET, false);
clearInterval(iv);
} else if (++tries > 60) {
clearInterval(iv);
const el = ref.current;
const m = el && el.map;
if (!m) { if (++tries > 60) clearInterval(iv); return; }
clearInterval(iv);
try { m.setMinZoom(SCAMIN_MIN_ZOOM); } catch (e) { /* older map */ }
// Force the reference display state. applyScheme('day') also resets any scheme
// a previous Day/Dusk click (or the sibling demo) left in localStorage.
if (typeof el.applyMariner === 'function') {
try { el.applyMariner(MARINER); } catch (e) { /* widget best-effort */ }
}
if (typeof el.applyScheme === 'function') {
try { el.applyScheme('day'); } catch (e) { /* widget best-effort */ }
}
fitPanel(el, SHEET, false);
}, 200);
return () => clearInterval(iv);
}, [status]);

// Entering/leaving fullscreen resizes the map frame, so let the map relayout and
// re-frame the active panel. Also lock page scroll + wire Escape / arrow nav.
useEffect(() => {
if (typeof document !== 'undefined') {
document.body.style.overflow = full ? 'hidden' : '';
}
const t = setTimeout(() => {
const m = ref.current && ref.current.map;
if (!m) return;
try { m.resize(); } catch (e) { /* older map */ }
fitPanel(ref.current, activeRef.current, false);
}, 80);
if (!full) return () => clearTimeout(t);
// Fullscreen owns the screen, so Up/Down nav works globally even when focus is on
// the map. But if focus is on a panel button, the list's own onKeyDown already
// handled it (and called preventDefault) — bail so we don't step twice.
const onKey = (e) => {
if (e.key === 'Escape') { setFull(false); return; }
if (e.defaultPrevented) return;
onNavKey(e);
};
window.addEventListener('keydown', onKey);
return () => {
clearTimeout(t);
window.removeEventListener('keydown', onKey);
if (typeof document !== 'undefined') document.body.style.overflow = '';
};
}, [full]); // eslint-disable-line react-hooks/exhaustive-deps

const go = (p) => {
setActive(p.page);
activeRef.current = p;
const el = ref.current;
if (!el) return;
if (p.scheme && typeof el.applyScheme === 'function') {
try { el.applyScheme(p.scheme); } catch (e) { /* widget-mode best-effort */ }
// Day/Dusk colour-test panels carry their own scheme; everything else stays Day.
if (typeof el.applyScheme === 'function') {
try { el.applyScheme(p.scheme || 'day'); } catch (e) { /* widget-mode best-effort */ }
}
if (!fitPanel(el, p, true) && typeof el.setView === 'function') {
el.setView({lng: p.lng, lat: p.lat, scale: p.scale, animate: true, duration: 900});
}
};

// Keyboard nav: Up/Down step through the panel list. Uses activeRef (always
// current) so it works from the fullscreen effect's stale closure too.
const focusBtn = (page) => {
const root = listRef.current;
const btn = root && root.querySelector(`button[data-page="${page}"]`);
if (btn) btn.focus();
};
const step = (dir) => {
const i = PANELS.findIndex((p) => p.page === activeRef.current.page);
const ni = Math.min(PANELS.length - 1, Math.max(0, i + dir));
if (ni === i) return;
go(PANELS[ni]);
focusBtn(PANELS[ni].page);
};
const onNavKey = (e) => {
if (e.key === 'ArrowDown') { e.preventDefault(); step(1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); step(-1); }
};

if (status === 'missing') {
return (
<div className="chart1 chart1--poster">
Expand All @@ -141,16 +225,28 @@ function Chart() {

const zoom = zoomForScale(INITIAL_SCALE, SHEET.lat);
return (
<div className="chart1">
<div className={'chart1' + (full ? ' chart1--full' : '')}>
<div className="chart1__panel">
<div className="chart1__title">
Reference panels <span className="chart1__sub">PresLib §16, pp. 238–253</span>
<div className="chart1__head">
<div className="chart1__title">
Reference panels <span className="chart1__sub">PresLib §16, pp. 238–253</span>
</div>
<button
type="button"
className="chart1__full"
onClick={() => setFull((v) => !v)}
aria-pressed={full}
title={full ? 'Exit fullscreen (Esc)' : 'Fullscreen — compare against the spec plots'}
>
{full ? '✕ Exit' : '⤢ Fullscreen'}
</button>
</div>
<ol className="chart1__list">
<ol className="chart1__list" ref={listRef} onKeyDown={onNavKey}>
{PANELS.map((p) => (
<li key={p.page}>
<button
type="button"
data-page={p.page}
className={'chart1__test' + (active === p.page ? ' chart1__test--active' : '')}
onClick={() => go(p)}
>
Expand All @@ -162,10 +258,13 @@ function Chart() {
</ol>
</div>
<div className="liveChart chart1__map">
{/* widget = read-only viewer; assets = demo frontend; catalog = Chart 1 tiles */}
{/* widget = read-only viewer; assets = demo frontend; catalog = Chart 1 tiles.
spec = chrome-free clean map (no controls/databox/attr/scalebar) so the
render matches the PresLib reference plots for side-by-side diffing. */}
<chart-plotter
ref={ref}
widget=""
spec=""
assets={demo}
catalog={manifest}
basemap="none"
Expand Down
93 changes: 84 additions & 9 deletions docs/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,27 +73,58 @@
flex-direction: column;
max-height: 78vh;
min-height: 520px;
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: 12px;
background: var(--ifm-card-background-color, var(--ifm-background-surface-color));
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.chart1__head {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.55rem 0.6rem 0.5rem 0.7rem;
border-bottom: 1px solid var(--ifm-color-emphasis-200);
background: var(--ifm-color-emphasis-100);
}
.chart1__title {
flex: 1;
min-width: 0;
font-size: 0.78rem;
font-weight: 600;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ifm-color-emphasis-700);
padding: 0 0.3rem 0.4rem;
color: var(--ifm-color-emphasis-800);
}
.chart1__sub {
display: block;
font-weight: 400;
text-transform: none;
letter-spacing: 0;
font-size: 0.72rem;
color: var(--ifm-color-emphasis-500);
color: var(--ifm-color-emphasis-600);
}
.chart1__full {
flex: none;
cursor: pointer;
border: 1px solid var(--ifm-color-primary);
border-radius: 6px;
padding: 0.25rem 0.55rem;
font-size: 0.72rem;
font-weight: 600;
white-space: nowrap;
color: var(--ifm-color-primary);
background: transparent;
transition: background 0.12s, color 0.12s;
}
.chart1__full:hover {
background: var(--ifm-color-primary);
color: #fff;
}
.chart1__list {
list-style: none;
margin: 0;
padding: 0;
padding: 0.4rem;
overflow: auto;
display: flex;
flex-direction: column;
Expand All @@ -102,9 +133,9 @@
.chart1__test {
display: flex;
align-items: baseline;
gap: 0.5rem;
gap: 0.55rem;
width: 100%;
padding: 0.4rem 0.55rem;
padding: 0.45rem 0.55rem;
text-align: left;
cursor: pointer;
border: 0;
Expand All @@ -116,19 +147,27 @@
transition: background 0.12s, border-color 0.12s;
}
.chart1__test:hover {
background: var(--ifm-color-emphasis-100);
background: var(--ifm-color-emphasis-200);
}
.chart1__test--active {
background: var(--ifm-color-primary-lightest);
border-left-color: var(--ifm-color-primary);
}
.chart1__test--active .chart1__label {
font-weight: 600;
color: var(--ifm-color-primary-darkest);
}
.chart1__page {
min-width: 2.7em;
font-size: 0.68rem;
font-size: 0.66rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--ifm-color-emphasis-600);
white-space: nowrap;
}
.chart1__test--active .chart1__page {
color: var(--ifm-color-primary);
}
.chart1__label {
flex: 1;
font-size: 0.85rem;
Expand All @@ -141,6 +180,28 @@
height: 78vh;
min-height: 520px;
}

/* Fullscreen: lift the whole checker out of the docs column so the reference
list and the live widget fill the viewport together — easy spec diffing. */
.chart1--full {
position: fixed;
inset: 0;
z-index: 9999;
margin: 0;
padding: 0.75rem;
gap: 0.75rem;
background: var(--ifm-background-color);
grid-template-columns: 280px minmax(0, 1fr);
}
.chart1--full .chart1__panel {
max-height: none;
min-height: 0;
height: 100%;
}
.chart1--full .chart1__map {
height: 100%;
min-height: 0;
}
.chart1__poster {
width: 100%;
border-radius: 12px;
Expand Down Expand Up @@ -173,4 +234,18 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
/* Fullscreen on a narrow screen: keep it stacked but fill the viewport —
map on top takes the room, panel list below scrolls. */
.chart1--full {
grid-template-columns: 1fr;
grid-template-rows: minmax(0, 1fr) auto;
}
.chart1--full .chart1__map {
grid-row: 1;
height: auto;
}
.chart1--full .chart1__panel {
grid-row: 2;
max-height: 38vh;
}
}