Skip to content
Open
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
69 changes: 58 additions & 11 deletions explorer.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3644,8 +3644,32 @@ zoomWatcher = {
syncFacetNote();
writeQueryState();
refreshHeatmap();
if (getMode() === 'point') {
await loadViewportSamples();
// #267: facet selection must visibly drive the MAP, not just the
// table/legend. Cluster dots come from pre-aggregated H3 summaries
// that carry only `dominant_source`, so they cannot honor a
// material/context/object_type facet. Mirror the committed-search
// path (applySearchFilterChange / C3): when any such facet is
// active, FORCE point mode so the map shows the actual filtered
// dots (which already apply facetFilterSQL()). When facets are
// cleared, revert to the altitude-appropriate mode — unless a text
// search is still latching point mode.
if (hasFacetFilters()) {
if (getMode() !== 'point') {
await enterPointMode(false); // forces point; awaits filtered viewport load
} else {
await loadViewportSamples();
}
} else if (getMode() === 'point') {
const h = viewer.camera.positionCartographic.height;
if (!searchIsActive() && h >= EXIT_POINT_ALT) {
exitPointMode(false);
const target = h > 3000000 ? 4 : h > 300000 ? 6 : 8;
if (target !== currentRes) {
await loadRes(target, { 4: h3_res4_url, 6: h3_res6_url, 8: h3_res8_url }[target]);
}
} else {
await loadViewportSamples();
}
}
refreshFacetCounts();
await new Promise(r => setTimeout(r, 300));
Expand Down Expand Up @@ -3675,14 +3699,21 @@ zoomWatcher = {
syncSearchPanelState();
syncFacetNote();
refreshHeatmap();
if (searchIsActive()) {
// #267: point mode is latched by EITHER an active search OR an
// active facet — both filter the sample set and can't be shown as
// clusters. So clearing the search must NOT revert to clusters if a
// facet is still checked (and vice-versa, handled in
// handleFacetFilterChange). Use the same forced-point predicate as
// every other latch.
const forcePoint = searchIsActive() || hasFacetFilters();
if (forcePoint) {
if (getMode() !== 'point') {
await enterPointMode(false); // forces point; awaits filtered viewport load
} else {
await loadViewportSamples();
}
} else {
// Search cleared: revert to the altitude-appropriate mode.
// Neither search nor facet active: revert to altitude-appropriate mode.
const h = viewer.camera.positionCartographic.height;
if (getMode() === 'point' && h >= EXIT_POINT_ALT) {
exitPointMode(false);
Expand Down Expand Up @@ -3727,16 +3758,17 @@ zoomWatcher = {
// A1 (#234 Step 4) / C3: while a search is active, latch point
// mode regardless of altitude — clusters can't be text-filtered,
// so we keep showing the filtered sample dots even when zoomed out.
const targetMode = searchIsActive() ? 'point'
const targetMode = (searchIsActive() || hasFacetFilters()) ? 'point'
: h < ENTER_POINT_ALT ? 'point'
: h > EXIT_POINT_ALT ? 'cluster'
: getMode();

if (targetMode === 'point' && getMode() !== 'point') {
if (searchIsActive()) {
// Search forces point mode even above ENTER_POINT_ALT,
// where tryEnterPointModeIfNeeded() would refuse; enter
// directly so the filtered dots render at any zoom.
if (searchIsActive() || hasFacetFilters()) {
// Search or an active facet (#267) forces point mode even
// above ENTER_POINT_ALT, where tryEnterPointModeIfNeeded()
// would refuse; enter directly so the filtered dots render
// at any zoom.
await enterPointMode(false);
} else {
// Cold-cache deep-link: the res8 + samples_map_lite fetches
Expand Down Expand Up @@ -3877,7 +3909,7 @@ zoomWatcher = {
// moveEnd must NOT exit to clusters — otherwise the post-search
// flyTo (200 km, above EXIT_POINT_ALT) would immediately undo the
// forced point mode and the globe would show unfiltered clusters.
if (h > EXIT_POINT_ALT && !searchIsActive()) {
if (h > EXIT_POINT_ALT && !searchIsActive() && !hasFacetFilters()) {
// Sub-10% zoom-out from point mode (e.g. 175 km → 181 km) won't
// fire `camera.changed`, so without driving the exit here we'd
// be stuck in point mode above `EXIT_POINT_ALT` until a larger
Expand Down Expand Up @@ -4017,7 +4049,7 @@ zoomWatcher = {
// A1 (#234 Step 4): an active search forces point mode regardless
// of the restored altitude, so the back/forward globe state stays
// coherent with the (still-filtered) table/legend.
const wantsPoint = searchIsActive() || s.mode === 'point' || (s.alt != null && s.alt < ENTER_POINT_ALT);
const wantsPoint = searchIsActive() || hasFacetFilters() || s.mode === 'point' || (s.alt != null && s.alt < ENTER_POINT_ALT);
if (wantsPoint && getMode() !== 'point') await enterPointMode(false);
else if (!wantsPoint && getMode() === 'point') exitPointMode(false);
}, 2000);
Expand Down Expand Up @@ -5266,6 +5298,21 @@ zoomWatcher = {
await tryEnterPointModeIfNeeded({ pushHistory: false });
}

// #267: a shared `?material=`/`?context=`/`?object_type=` deep link must
// force point mode like `?search=` does — cluster H3 summaries can't honor
// those facets. The altitude-gated trigger above won't promote a high-alt
// facet deep link, so enter point mode directly. We read the URL params
// (not the hydrated checkboxes) so this is independent of facet-loader
// timing; the latch points (targetMode / moveEnd / wantsPoint) keep it
// pinned once the checkboxes hydrate.
const _urlHasFacets = ['material', 'context', 'object_type'].some(p => {
const v = new URLSearchParams(location.search).get(p);
return v != null && v.trim() !== '';
});
if (_urlHasFacets && getMode() !== 'point') {
await enterPointMode(false);
}

// #233 phase 1: hydrate heatmap overlay from `heatmap=1` URL param.
// Reported by RY 2026-05-27 on PR #240 staging — toggle state was
// missing from "Copy Link to Current View." `refreshHeatmap()` lives
Expand Down
Loading