@@ -4,10 +4,11 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
44
55// Chart1Tests embeds the S-52 PresLib "ECDIS Chart 1" reference sheet LIVE — one
66// read-only <chart-plotter> widget — and turns the docs page into a symbol-compliance
7- // checker: every panel of the sheet is a row in the list; click one and the widget
8- // frames that panel. The whole sheet is one contiguous synthetic ENC, so navigation
9- // is just map.fitBounds(panel) — which fits the panel to the actual map size with
10- // padding that keeps the widget's own chrome (HUD, controls, scalebar) off the data.
7+ // checker: every panel of the sheet is a row in the list; click one (or arrow up/down)
8+ // and the widget frames that panel. The whole sheet is one contiguous synthetic ENC,
9+ // so navigation is just map.fitBounds(panel). The widget runs in `spec` mode (no
10+ // chrome) with the reference mariner settings forced on, so what you see is an
11+ // apples-to-apples diff against the spec's own reference plots.
1112// Tiles load from the /chart1/ bundle (`make demo-chart1`); the frontend assets are
1213// shared with the /demo/ bundle.
1314
@@ -18,8 +19,31 @@ const PX_PITCH_M = 0.00026458;
1819const zoomForScale = ( scale , lat ) =>
1920 Math . log2 ( ( M_PER_PX_Z0 * Math . cos ( ( lat * Math . PI ) / 180 ) ) / ( PX_PITCH_M * scale ) ) ;
2021
21- // Inset the fit so the sheet clears the widget's overlaid chrome on every edge.
22- const PAD = { top : 48 , bottom : 56 , left : 48 , right : 48 } ;
22+ // Spec mode hides all widget chrome, so the fit just needs a small, symmetric margin
23+ // to keep edge symbology off the frame edge — like the thin border around each
24+ // PresLib reference plot. (No asymmetric chrome padding any more.)
25+ const PAD = { top : 18 , bottom : 18 , left : 18 , right : 18 } ;
26+
27+ // Mariner display state pinned to match the IHO PresLib reference plots — kept in
28+ // step with the MARINER block in scripts/preslib-chart1.mjs. The widget is read-only
29+ // here (spec mode), so the viewer can't change anything; we force these at ready so
30+ // the render is an apples-to-apples diff against the spec's own figures. ALL symbology
31+ // shown; data-quality overlay on (CATZOC panels); metres (IHO, not NOAA feet); the
32+ // depth-shading demo's 0/5/10/30 contours labelled; date-dependency + meta boundaries
33+ // shown; 25 mm sectors and symbolized boundaries (the S-52 defaults the plots use).
34+ const MARINER = {
35+ displayBase : true , displayStandard : true , displayOther : true ,
36+ dataQuality : true ,
37+ depthUnit : 'm' ,
38+ showContourLabels : true ,
39+ shallowContour : 5 , safetyContour : 10 , deepContour : 30 ,
40+ highlightDateDependent : true ,
41+ dateDependent : false ,
42+ showMetaBounds : true ,
43+ showFullSectorLines : false ,
44+ boundaryStyle : 'symbolized' ,
45+ simplifiedPoints : false ,
46+ } ;
2347
2448// One row per PresLib reference-plot page (Part I §16, doc pages 238–253). Bounds
2549// are the cells' data extents [W, S, E, N]; the harbor pages are 1:14 000, the
@@ -52,7 +76,7 @@ const INITIAL_SCALE = 105000; // generous pre-fit paint; fitBounds refines on re
5276// map so neither the whole-sheet fit (on a small map) nor a scroll can cross it.
5377const SCAMIN_MIN_ZOOM = zoomForScale ( 139000 , SHEET . lat ) ;
5478
55- // Fit the map to a panel's bounds with chrome padding . Returns false if the map
79+ // Fit the map to a panel's bounds with a symmetric margin . Returns false if the map
5680// isn't up yet (caller falls back to setView).
5781function fitPanel ( el , p , animate ) {
5882 const m = el && el . map ;
@@ -70,8 +94,11 @@ function Chart() {
7094 const manifest = useBaseUrl ( '/chart1/charts-index.json' ) ;
7195 const overviewImg = useBaseUrl ( '/img/chart1/page-238-overview.png' ) ;
7296 const ref = useRef ( null ) ;
97+ const listRef = useRef ( null ) ; // the <ol> of panel buttons (for focus moves)
98+ const activeRef = useRef ( SHEET ) ; // current panel, for handlers in stale closures
7399 const [ active , setActive ] = useState ( 238 ) ;
74100 const [ status , setStatus ] = useState ( 'checking' ) ; // checking | ready | missing
101+ const [ full , setFull ] = useState ( false ) ; // fullscreen: panel list + widget fill the viewport
75102
76103 // Only boot the live widget if the tile bundle is actually published. Locally
77104 // (no `make demo-chart1`) fall back to the static overview image.
@@ -95,36 +122,93 @@ function Chart() {
95122 return ( ) => { cancelled = true ; } ;
96123 } , [ demo , manifest ] ) ;
97124
98- // Once the widget's map is ready, frame the whole sheet (fitBounds, not a guessed
99- // scale, so the entire box + labels show with margin for the chrome).
125+ // Once the widget's map is ready: pin the reference mariner settings + Day scheme
126+ // (spec mode is read-only — the viewer can't change them), floor the zoom past the
127+ // SCAMIN cutoff, and frame the whole sheet.
100128 useEffect ( ( ) => {
101129 if ( status !== 'ready' ) return undefined ;
102130 let tries = 0 ;
103131 const iv = setInterval ( ( ) => {
104- const m = ref . current && ref . current . map ;
105- if ( m ) {
106- try { m . setMinZoom ( SCAMIN_MIN_ZOOM ) ; } catch ( e ) { /* older map */ }
107- fitPanel ( ref . current , SHEET , false ) ;
108- clearInterval ( iv ) ;
109- } else if ( ++ tries > 60 ) {
110- clearInterval ( iv ) ;
132+ const el = ref . current ;
133+ const m = el && el . map ;
134+ if ( ! m ) { if ( ++ tries > 60 ) clearInterval ( iv ) ; return ; }
135+ clearInterval ( iv ) ;
136+ try { m . setMinZoom ( SCAMIN_MIN_ZOOM ) ; } catch ( e ) { /* older map */ }
137+ // Force the reference display state. applyScheme('day') also resets any scheme
138+ // a previous Day/Dusk click (or the sibling demo) left in localStorage.
139+ if ( typeof el . applyMariner === 'function' ) {
140+ try { el . applyMariner ( MARINER ) ; } catch ( e ) { /* widget best-effort */ }
141+ }
142+ if ( typeof el . applyScheme === 'function' ) {
143+ try { el . applyScheme ( 'day' ) ; } catch ( e ) { /* widget best-effort */ }
111144 }
145+ fitPanel ( el , SHEET , false ) ;
112146 } , 200 ) ;
113147 return ( ) => clearInterval ( iv ) ;
114148 } , [ status ] ) ;
115149
150+ // Entering/leaving fullscreen resizes the map frame, so let the map relayout and
151+ // re-frame the active panel. Also lock page scroll + wire Escape / arrow nav.
152+ useEffect ( ( ) => {
153+ if ( typeof document !== 'undefined' ) {
154+ document . body . style . overflow = full ? 'hidden' : '' ;
155+ }
156+ const t = setTimeout ( ( ) => {
157+ const m = ref . current && ref . current . map ;
158+ if ( ! m ) return ;
159+ try { m . resize ( ) ; } catch ( e ) { /* older map */ }
160+ fitPanel ( ref . current , activeRef . current , false ) ;
161+ } , 80 ) ;
162+ if ( ! full ) return ( ) => clearTimeout ( t ) ;
163+ // Fullscreen owns the screen, so Up/Down nav works globally even when focus is on
164+ // the map. But if focus is on a panel button, the list's own onKeyDown already
165+ // handled it (and called preventDefault) — bail so we don't step twice.
166+ const onKey = ( e ) => {
167+ if ( e . key === 'Escape' ) { setFull ( false ) ; return ; }
168+ if ( e . defaultPrevented ) return ;
169+ onNavKey ( e ) ;
170+ } ;
171+ window . addEventListener ( 'keydown' , onKey ) ;
172+ return ( ) => {
173+ clearTimeout ( t ) ;
174+ window . removeEventListener ( 'keydown' , onKey ) ;
175+ if ( typeof document !== 'undefined' ) document . body . style . overflow = '' ;
176+ } ;
177+ } , [ full ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
178+
116179 const go = ( p ) => {
117180 setActive ( p . page ) ;
181+ activeRef . current = p ;
118182 const el = ref . current ;
119183 if ( ! el ) return ;
120- if ( p . scheme && typeof el . applyScheme === 'function' ) {
121- try { el . applyScheme ( p . scheme ) ; } catch ( e ) { /* widget-mode best-effort */ }
184+ // Day/Dusk colour-test panels carry their own scheme; everything else stays Day.
185+ if ( typeof el . applyScheme === 'function' ) {
186+ try { el . applyScheme ( p . scheme || 'day' ) ; } catch ( e ) { /* widget-mode best-effort */ }
122187 }
123188 if ( ! fitPanel ( el , p , true ) && typeof el . setView === 'function' ) {
124189 el . setView ( { lng : p . lng , lat : p . lat , scale : p . scale , animate : true , duration : 900 } ) ;
125190 }
126191 } ;
127192
193+ // Keyboard nav: Up/Down step through the panel list. Uses activeRef (always
194+ // current) so it works from the fullscreen effect's stale closure too.
195+ const focusBtn = ( page ) => {
196+ const root = listRef . current ;
197+ const btn = root && root . querySelector ( `button[data-page="${ page } "]` ) ;
198+ if ( btn ) btn . focus ( ) ;
199+ } ;
200+ const step = ( dir ) => {
201+ const i = PANELS . findIndex ( ( p ) => p . page === activeRef . current . page ) ;
202+ const ni = Math . min ( PANELS . length - 1 , Math . max ( 0 , i + dir ) ) ;
203+ if ( ni === i ) return ;
204+ go ( PANELS [ ni ] ) ;
205+ focusBtn ( PANELS [ ni ] . page ) ;
206+ } ;
207+ const onNavKey = ( e ) => {
208+ if ( e . key === 'ArrowDown' ) { e . preventDefault ( ) ; step ( 1 ) ; }
209+ else if ( e . key === 'ArrowUp' ) { e . preventDefault ( ) ; step ( - 1 ) ; }
210+ } ;
211+
128212 if ( status === 'missing' ) {
129213 return (
130214 < div className = "chart1 chart1--poster" >
@@ -141,16 +225,28 @@ function Chart() {
141225
142226 const zoom = zoomForScale ( INITIAL_SCALE , SHEET . lat ) ;
143227 return (
144- < div className = " chart1" >
228+ < div className = { ' chart1' + ( full ? ' chart1--full' : '' ) } >
145229 < div className = "chart1__panel" >
146- < div className = "chart1__title" >
147- Reference panels < span className = "chart1__sub" > PresLib §16, pp. 238–253</ span >
230+ < div className = "chart1__head" >
231+ < div className = "chart1__title" >
232+ Reference panels < span className = "chart1__sub" > PresLib §16, pp. 238–253</ span >
233+ </ div >
234+ < button
235+ type = "button"
236+ className = "chart1__full"
237+ onClick = { ( ) => setFull ( ( v ) => ! v ) }
238+ aria-pressed = { full }
239+ title = { full ? 'Exit fullscreen (Esc)' : 'Fullscreen — compare against the spec plots' }
240+ >
241+ { full ? '✕ Exit' : '⤢ Fullscreen' }
242+ </ button >
148243 </ div >
149- < ol className = "chart1__list" >
244+ < ol className = "chart1__list" ref = { listRef } onKeyDown = { onNavKey } >
150245 { PANELS . map ( ( p ) => (
151246 < li key = { p . page } >
152247 < button
153248 type = "button"
249+ data-page = { p . page }
154250 className = { 'chart1__test' + ( active === p . page ? ' chart1__test--active' : '' ) }
155251 onClick = { ( ) => go ( p ) }
156252 >
@@ -162,10 +258,13 @@ function Chart() {
162258 </ ol >
163259 </ div >
164260 < div className = "liveChart chart1__map" >
165- { /* widget = read-only viewer; assets = demo frontend; catalog = Chart 1 tiles */ }
261+ { /* widget = read-only viewer; assets = demo frontend; catalog = Chart 1 tiles.
262+ spec = chrome-free clean map (no controls/databox/attr/scalebar) so the
263+ render matches the PresLib reference plots for side-by-side diffing. */ }
166264 < chart-plotter
167265 ref = { ref }
168266 widget = ""
267+ spec = ""
169268 assets = { demo }
170269 catalog = { manifest }
171270 basemap = "none"
0 commit comments