@@ -334,19 +334,17 @@ function _pushScaminProbes(out, server) {
334334// above the band's native max (where the chart is grossly enlarged, ≥ ~×2 its
335335// compilation scale). Inserted right after the band's base fill so a finer band's
336336// opaque fill covers it — the hatch survives only on coarse-only overscale patches.
337- // No-op for the finest band / merged "all" set (nothing coarser to enlarge). S-52
338- // §10.1.10.2; display priority 3, viewing group 21030.
339- function _pushOverscale ( out , source , band , layerVis , showOverscale , bandsHidden ) {
340- // DISABLED: the old "hatch wherever a band overzooms past its native max"
341- // heuristic over-triggers — it paints AP(OVERSC01) on plain zoom-in of the
342- // best-available chart, which S-52 §10.1.10.1 says must show ONLY the "×N"
343- // indication, never the pattern. Real ECDIS show the area pattern only at a
344- // genuine scale boundary (a coarser cell enlarged ≥×2 in a finer cell's hole,
345- // §10.1.10.2) — that wants a baked overscale_areas layer (task #3). Until then,
346- // no auto-hatch (the HUD still shows the ×N overscale indication).
347- return ; // eslint-disable-line no-unreachable
337+ // S-52 §10.1.10.2; display priority 3, viewing group 21030.
338+ //
339+ // `finerPresent` is the spec gate: emit ONLY when a finer band is loaded, so a real
340+ // chart-scale boundary exists and this band can show through a finer band's hole
341+ // (grossly overscaled → pattern). When this band IS the finest available, plain
342+ // zoom-in is "deliberate overscale of best-available" and must show ONLY the ×N
343+ // overscale indication, never the pattern (§10.1.10.1) — so we emit nothing and the
344+ // HUD ×N stands alone. No-op for the merged "all" set (no per-band layering).
345+ function _pushOverscale ( out , source , band , layerVis , showOverscale , bandsHidden , finerPresent ) {
348346 const nm = CHART_BANDS . find ( ( b ) => b . slug === band ) ;
349- if ( ! nm || band === "all" || nm . max >= 18 ) return ;
347+ if ( ! nm || band === "all" || nm . max >= 18 || ! finerPresent ) return ;
350348 const id = "overscale@" + source ;
351349 const vis = showOverscale === false ? "none" : "visible" ;
352350 layerVis [ id ] = vis ;
@@ -386,6 +384,7 @@ export function buildChartLayers({
386384 scheme, // active scheme branch ("day"/"dusk"/"night") = this._active
387385 server, serverSets, scaminValues, scaminLat, // chart-source state (already resolved)
388386 bandsHidden, // Set (this._bandsHidden)
387+ bandsPresent = new Set ( ) , // Set of band slugs that have data — gates the overscale pattern
389388 ignoreScamin, // DEBUG: drop the per-SCAMIN display gate (show everything in-band)
390389 sizeScale = 1 , // px→true-physical feature-size multiplier (0.35278/pxPitch); see _scaleSizes
391390 pxPitch, // calibrated CSS-pixel pitch (mm) → SCAMIN gates on the true physical scale
@@ -394,6 +393,23 @@ export function buildChartLayers({
394393 const layerBase = { } , variants = { } , layerVis = { } ;
395394 const tmpl = buildLayers ( mariner , palette , atlasPpu , osm , sizeScale ) ;
396395 const out = [ ] ;
396+ // Overscale-pattern gate (S-52 §10.1.10.2): a band gets the AP(OVERSC01) hatch only
397+ // when a strictly-FINER band is present in the loaded set — i.e. a real chart-scale
398+ // boundary exists for it to show through. The finest band present is the
399+ // best-available data, so its plain zoom-in is the ×N-only case (§10.1.10.1).
400+ const _bandRank = ( slug ) => CHART_BANDS . findIndex ( ( b ) => b . slug === slug ) ;
401+ const _presentRanks = [ ...bandsPresent ]
402+ . filter ( ( slug ) => slug && slug !== "all" )
403+ . map ( _bandRank )
404+ . filter ( ( i ) => i >= 0 ) ;
405+ const _finestPresentRank = _presentRanks . length ? Math . max ( ..._presentRanks ) : - 1 ;
406+ // Emit the hatch for `slug` only when this band is itself present AND a strictly
407+ // finer band is also present (the pmtiles path iterates ALL bands, so the present
408+ // check matters). The finest present band never qualifies — it's best-available.
409+ const finerBandPresent = ( slug ) => {
410+ const r = _bandRank ( slug ) ;
411+ return r >= 0 && bandsPresent . has ( slug ) && r < _finestPresentRank ;
412+ } ;
397413 // Group each base template layer with the *_scamin clone that _withScamin placed
398414 // immediately after it (tagged _baseId), so the pair expands TOGETHER per band
399415 // below — both fill paths iterate group-outer, band-mid, member-inner. Expanding
@@ -479,7 +495,7 @@ export function buildChartLayers({
479495 // interleaved per band, so a finer band's opaque fill covers it where finer
480496 // data exists — the hatch is left only on the coarse-only (overscale) patches
481497 // such as open water shown enlarged. S-52 §10.1.10.2.
482- if ( L . id === "areas" ) _pushOverscale ( out , "chart-" + set . name , set . band , layerVis , undefined , bandsHidden ) ;
498+ if ( L . id === "areas" ) _pushOverscale ( out , "chart-" + set . name , set . band , layerVis , undefined , bandsHidden , finerBandPresent ( set . band ) ) ;
483499 }
484500 }
485501 }
@@ -543,7 +559,7 @@ export function buildChartLayers({
543559 } else {
544560 mk ( "" , base , dmin || undefined ) ;
545561 }
546- if ( L . id === "areas" ) _pushOverscale ( out , "chart-" + band . slug , band . slug , layerVis , undefined , bandsHidden ) ;
562+ if ( L . id === "areas" ) _pushOverscale ( out , "chart-" + band . slug , band . slug , layerVis , undefined , bandsHidden , finerBandPresent ( band . slug ) ) ;
547563 }
548564 }
549565 }
0 commit comments