@@ -698,12 +698,15 @@ export class ChartPlotterApp extends HTMLElement {
698698 const n = this . _installed . size ;
699699 label = `${ n } chart${ n !== 1 ? "s" : "" } ` + ( failed ? ` · ${ failed } failed` : "" ) ;
700700 } else {
701- el . hidden = true ; el . classList . remove ( "busy" ) ; return ;
701+ el . hidden = true ; el . classList . remove ( "busy" ) ;
702+ const sb = root . getElementById ( "statusbar" ) ; if ( sb ) sb . hidden = true ;
703+ return ;
702704 }
703705 txt . textContent = label ;
704706 el . classList . toggle ( "busy" , busy ) ;
705707 el . classList . toggle ( "has-fail" , failed > 0 ) ;
706708 el . hidden = false ;
709+ const sb = root . getElementById ( "statusbar" ) ; if ( sb ) sb . hidden = false ;
707710 }
708711
709712 // Subtle "loading more while data is shown" cue: a thin indeterminate bar at the
@@ -954,7 +957,8 @@ export class ChartPlotterApp extends HTMLElement {
954957 `<span class="hud-main"><span class="hud-dot" style="background:${ BAND_COLOR [ band ] } "></span>` +
955958 `<span class="hud-band">${ BAND_LABEL [ band ] } </span><span class="hud-sep">·</span>` +
956959 `<span class="hud-scale">1:${ fmtScale ( scaleDenom ( z , c . lat ) ) } </span><span class="hud-sep">·</span>` +
957- `<span class="hud-z">z${ z . toFixed ( 1 ) } </span></span>` ;
960+ `<span class="hud-z">z${ z . toFixed ( 1 ) } </span><span class="hud-sep">·</span>` +
961+ `<span class="hud-coord">${ fmtLatLon ( c . lat , c . lng ) } </span></span>` ;
958962 }
959963
960964
@@ -1512,6 +1516,16 @@ export class ChartPlotterApp extends HTMLElement {
15121516 this . _clearTaskSoon ( 1200 ) ;
15131517 }
15141518
1519+ // Public: open the viewport on a position at a target paper scale or zoom — a
1520+ // shell-level entry to the <chart-plotter> primitive (see its setView for the
1521+ // option shape). The map's move/moveend then update the readout and persist the
1522+ // view as if the user had navigated there. Examples:
1523+ // app.setView({ lat: 37.81, lng: -122.45, scale: 20000 }) // 1:20,000
1524+ // app.setView({ lat: 37.81, lng: -122.45, zoom: 14, animate: true })
1525+ setView ( opts ) {
1526+ return this . _plotter ? this . _plotter . setView ( opts ) : null ;
1527+ }
1528+
15151529 saveView ( ) {
15161530 // Don't persist the Charts-mode selection framing (the zoomed-out world view):
15171531 // a refresh should resume where the user was actually looking at charts, not
@@ -3323,14 +3337,26 @@ export class ChartPlotterApp extends HTMLElement {
33233337 .csp-stat { flex:none; font-weight:600; font-size:11px; }
33243338 .csp-queued { color:#9aa7b4; } .csp-loading { color:#d9892b; } .csp-ready { color:#2e9b57; } .csp-failed { color:#cf3b3b; }
33253339 .csp-empty { color:var(--ui-text-dim); font:500 12px/1.2 system-ui,sans-serif; padding:8px 0; }
3326- /* The chip hugs its content; left edge is anchored, so the leading dot+band
3327- stay put as the scale digits change (tabular figures). */
3328- .sb-readout { flex:none; }
3340+ /* Scale/zoom/position readout — its own fixed-size pill, pinned to the
3341+ bottom-MIDDLE. Every field below has a fixed width + tabular figures so
3342+ the pill's size never changes and nothing shifts as the scale, zoom, or
3343+ centre coordinates update (panning changes only the position field). */
3344+ .sb-readout { position:absolute; left:50%; bottom:calc(var(--botbar-h) + 8px);
3345+ transform:translateX(-50%); z-index:6; box-sizing:border-box;
3346+ display:inline-flex; align-items:center; padding:5px 12px; max-width:calc(100vw - 20px);
3347+ background:color-mix(in srgb, var(--ui-surface) 82%, transparent); border:1px solid rgba(0,0,0,.06);
3348+ border-radius:11px; backdrop-filter:blur(6px);
3349+ box-shadow:inset 0 1px 2px rgba(0,0,0,.18), inset 0 -1px 0 rgba(255,255,255,.4), 0 1px 0 rgba(255,255,255,.4);
3350+ font:11px system-ui,sans-serif; color:var(--ui-text); }
3351+ .sb-readout:empty { display:none; }
33293352 .sb-readout .hud-main { display:flex; align-items:center; gap:6px;
33303353 font-weight:600; font-size:12px; white-space:nowrap; font-variant-numeric:tabular-nums; }
33313354 .sb-readout .hud-dot { width:8px; height:8px; border-radius:50%; flex:none; box-shadow:0 0 0 2px rgba(255,255,255,.6); margin-right:1px; }
3332- .sb-readout .hud-scale { color:var(--ui-accent); }
3333- .sb-readout .hud-z { color:var(--ui-text-dim); }
3355+ /* Fixed-width fields (left-aligned) so the pill is a constant size. */
3356+ .sb-readout .hud-band { display:inline-block; width:56px; }
3357+ .sb-readout .hud-scale { display:inline-block; width:74px; color:var(--ui-accent); }
3358+ .sb-readout .hud-z { display:inline-block; width:40px; color:var(--ui-text-dim); }
3359+ .sb-readout .hud-coord { display:inline-block; width:150px; color:var(--ui-text-dim); }
33343360 .sb-readout .hud-sep { color:var(--ui-text-faint); }
33353361 /* In-view band pills, right-aligned; each opens a cell-list popup. */
33363362 .sb-bands { display:flex; align-items:center; gap:8px; min-width:0; margin-left:auto; }
@@ -3571,13 +3597,13 @@ export class ChartPlotterApp extends HTMLElement {
35713597 }
35723598 </style>
35733599 <div id="map"></div>
3574- <div id="statusbar">
3600+ <div id="statusbar" hidden >
35753601 <button id="bake-status" class="sb-bake" type="button" hidden title="Chart tile generation — click for per-cell status">
35763602 <span class="sb-bake-spin"></span><span class="sb-bake-txt"></span>
35773603 </button>
3578- <div id="cov-readout" class="sb-readout"></div>
35793604 <div id="cell-status-pop" hidden></div>
35803605 </div>
3606+ <div id="cov-readout" class="sb-readout"></div>
35813607 <div id="load-bar" class="load-bar" aria-hidden="true"></div>
35823608 <div id="rail">
35833609 <div class="rail-tabs">
@@ -4035,6 +4061,22 @@ function fmtScale(d) {
40354061 return ( Math . round ( d / mag ) * mag ) . toLocaleString ( ) ;
40364062}
40374063
4064+ // Map-centre position as fixed-width degrees-decimal-minutes, e.g.
4065+ // "39°27.6′N 104°39.6′W". Degrees are zero-padded (lat 2, lon 3) and minutes to
4066+ // "MM.M", so the character count never changes — with tabular figures, the
4067+ // centred status pill never reflows as you pan. Longitude is normalised to ±180.
4068+ function fmtLatLon ( lat , lng ) {
4069+ const dm = ( v , degDigits ) => {
4070+ let a = Math . abs ( v ) ;
4071+ let d = Math . floor ( a ) ;
4072+ let m = ( a - d ) * 60 ;
4073+ if ( m >= 59.95 ) { m = 0 ; d += 1 ; } // round-up carry: 59.95′ rolls to next degree
4074+ return String ( d ) . padStart ( degDigits , "0" ) + "°" + m . toFixed ( 1 ) . padStart ( 4 , "0" ) + "′" ;
4075+ } ;
4076+ const x = ( ( ( ( lng + 180 ) % 360 ) + 360 ) % 360 ) - 180 ; // wrap to [-180, 180)
4077+ return dm ( lat , 2 ) + ( lat >= 0 ? "N" : "S" ) + " " + dm ( x , 3 ) + ( x >= 0 ? "E" : "W" ) ;
4078+ }
4079+
40384080// True when the page was opened as a shared-view link (<origin>/#share or
40394081// ?share) — boot() then reconstructs the publisher's scene from /api/share.
40404082function isShareUrl ( ) {
0 commit comments