Skip to content
Open
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
29 changes: 16 additions & 13 deletions EXPLORER_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
29 changes: 16 additions & 13 deletions explorer.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');

Expand All @@ -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')}
Expand Down
Loading