diff --git a/EXPLORER_STATE.md b/EXPLORER_STATE.md index 1e9725c..6567ba9 100644 --- a/EXPLORER_STATE.md +++ b/EXPLORER_STATE.md @@ -525,27 +525,30 @@ the page and count queries now include dateline-crossing handling, including the post-padding wrap case where a non-wrapping rect spills past ±180 after the 30% expansion). -**Known pre-existing wrap bug NOT fixed in this PR.** +**Antimeridian wrap bug — FIXED in #220 (was scoped out of #219).** `loadViewportSamples()` in `zoomWatcher` (the point-mode sample -loader) has its own padding logic that does *not* split the +loader) previously had its own padding logic that did *not* split the longitude predicate when the padded rectangle wraps the antimeridian — `bounds.east - bounds.west` is meaningless for a -wrapping viewport, and the resulting single `BETWEEN` clause can -return zero matches. This PR's table queries now route through +wrapping viewport, and the resulting single `BETWEEN` clause +returned zero matches. #219's table queries route through `viewerBboxSQL()` and split the wrapped predicate correctly, so the -**table is fine** at the dateline; the bug is now only in the -point-mode count (phase-msg "Samples in View"). At wrapping -viewports the two surfaces will therefore diverge: table shows the -correct row set, phase-msg can read zero or undercount. Scoped out -as a follow-up because the user's primary complaint (table=6M vs -phase-msg=153 at Crete) is unrelated to the dateline. Right fix is -to share the `viewerBboxSQL`-style normalization with point-mode. +**table was already fine** at the dateline; the bug remained only in +the point-mode loader + its count (phase-msg "Samples in View", +computed off the same WHERE). #220 fixes it by routing +`loadViewportSamples()` through the SAME `viewerBboxSQL('latitude', +'longitude', VIEWPORT_PAD_FACTOR)` helper, so point mode, the +"Samples in View" count, and the table now all use the identical +wrap-aware, post-padding-normalized bbox. (Originally scoped out of +#219 because the user's primary complaint — table=6M vs phase-msg=153 +at Crete — was unrelated to the dateline.) The shared `VIEWPORT_PAD_FACTOR` (0.3) is applied by the table query, the point-mode loader, and the cluster-mode `countInViewport()` call sites, so all three surfaces use the same padded bbox for their "in view" counts in the non-dateline, non-facet-filtered case. Caveats -called out below: point mode still has the antimeridian padding bug -(#220), cluster counts are H3-cell-granularity approximations, and +called out below: the antimeridian padding bug is now fixed (#220 — +point mode shares `viewerBboxSQL`), cluster counts are +H3-cell-granularity approximations, and cluster H3 loads don't apply the material/context/object-type facet filters — so under those conditions the counts can still diverge independent of this PR. Issue #221 was three sources of divergence: diff --git a/explorer.qmd b/explorer.qmd index d52666d..855da4f 100644 --- a/explorer.qmd +++ b/explorer.qmd @@ -1323,6 +1323,8 @@ function paddedViewportBounds(padFactor) { // - tableView (PR #219): scopes the samples table to the current viewport // so "samples in view" in cluster/point mode == table row count. // - doSearch('area') (#178 light path): exact-viewport text search. +// - loadViewportSamples() (#220): point-mode sample loader + "Samples in View" +// count, so point mode uses the same wrap-aware bbox as the table. function viewerBboxSQL(latCol, lngCol, padFactor) { const b = paddedViewportBounds(padFactor); if (!b) return null; @@ -3024,17 +3026,19 @@ zoomWatcher = { const bounds = getViewportBounds(); if (!bounds) return; - // Fetch with VIEWPORT_PAD_FACTOR (30%) padding so this fetch - // covers the same bbox as the table query and the cluster-mode - // "Samples in View" count (issue #221). - const latPad = (bounds.north - bounds.south) * VIEWPORT_PAD_FACTOR; - const lngPad = (bounds.east - bounds.west) * VIEWPORT_PAD_FACTOR; - const padded = { - south: bounds.south - latPad, - north: bounds.north + latPad, - west: bounds.west - lngPad, - east: bounds.east + lngPad - }; + // #220: bbox via the shared `viewerBboxSQL()` (VIEWPORT_PAD_FACTOR padding), + // the SAME helper the table uses (since #219). The previous inline padding + // emitted a single `longitude BETWEEN padded.west AND padded.east`, which is + // WRONG at the antimeridian: a viewport that wraps the dateline has + // west > east (and, post-padding, `bounds.east - bounds.west` is negative, so + // the inline lngPad math was meaningless too), so `BETWEEN a-larger AND a- + // smaller` matched ZERO rows — point mode + the "Samples in View" count + // (computed off the same WHERE) read 0 / undercounted near the dateline while + // the table (via viewerBboxSQL) showed the correct set. viewerBboxSQL emits + // the split `(lng BETWEEN west AND 180 OR lng BETWEEN -180 AND east)` for the + // wrap case and also normalizes post-padding overflow back into [-180,180]. + const bboxSQL = viewerBboxSQL('latitude', 'longitude', VIEWPORT_PAD_FACTOR); + if (!bboxSQL) return; // off-globe (rare) — same bail as the !bounds guard updatePhaseMsg('Loading individual samples...', 'loading'); @@ -3047,8 +3051,7 @@ zoomWatcher = { // POINT_BUDGET-worth of rows the LIMIT returns is undefined and // can differ across browsers/sessions for the same query. const whereClause = ` - WHERE latitude BETWEEN ${padded.south} AND ${padded.north} - AND longitude BETWEEN ${padded.west} AND ${padded.east} + WHERE 1=1${bboxSQL} ${sourceFilterSQL('source')} ${facetFilterSQL()} ${searchFilterSQL('pid')}