Skip to content
Merged
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
135 changes: 105 additions & 30 deletions explorer.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -67,36 +67,93 @@ format:
min-height: 0;
aspect-ratio: auto;
}
/* Slim top-right search overlay (Hana Figma node 222:456). The earlier
multi-row treatment (M-1A, PR #200) ate ~480px × ~100px on the left
side of the map; this collapses to one row at the right. The
Cesium toolbar is still at top-left (left:5px), so positioning at
top-right keeps both surfaces clear of each other. */
.map-search-overlay {
position: absolute;
top: 8px;
left: 50px; /* clear of Cesium toolbar column at left: 5px */
width: min(480px, calc(100% - 60px));
right: 8px;
z-index: 1000;
display: flex;
align-items: center;
gap: 4px;
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(4px);
padding: 8px 10px;
padding: 0 4px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
gap: 6px;
height: 32px; /* slim row; Figma 222:456 target is ~28 + 4 chrome */
box-sizing: border-box;
}
.map-search-overlay input[type="text"] {
width: 260px;
max-width: 100%;
height: 24px;
padding: 0 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 13px;
outline: none;
background: white;
box-sizing: border-box;
}
.map-search-overlay .search-results { color: #333; }
@media (max-width: 900px) {
/* Keep the overlay clear of the vertical Cesium toolbar column at
left: 5px. Don't switch to left: 8px here — that overlaps the
toolbar hit targets at narrow widths. */
.map-search-overlay {
width: calc(100% - 58px);
}
.map-search-overlay input[type="text"]:focus {
border-color: #1565c0;
box-shadow: 0 0 0 2px rgba(21, 101, 192, 0.15);
}
.map-search-overlay #searchSubmitBtn {
background: #1565c0;
color: white;
border: none;
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
font-size: 14px;
box-sizing: border-box;
flex-shrink: 0;
}
.map-search-overlay #searchSubmitBtn:hover { background: #0d47a1; }
.map-search-overlay #searchSubmitBtn:focus-visible {
outline: 2px solid #0d47a1;
outline-offset: 2px;
}
/* Scope buttons + help paragraph are preserved in the DOM (handlers in
zoomWatcher / search wiring bind by id) but hidden in the slim
layout. Area-scoped search is still reachable via `?search_scope=area`
URL hydration, which calls doSearch('area') on boot. */
.map-search-overlay .search-actions,
.map-search-overlay .search-help { display: none; }
/* The aria-live results-count line sits just under the overlay at the
same right edge. Hidden visually when empty so it doesn't add a
blank strip; aria-live still announces. */
.map-search-results-line {
position: absolute;
top: 46px;
right: 8px;
max-width: 340px;
z-index: 1000;
font-size: 12px;
color: #333;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(4px);
padding: 3px 8px;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12);
}
.map-search-results-line:empty { display: none; }
@media (max-width: 600px) {
.map-search-overlay input[type="text"] { width: 160px; }
}
@media (max-width: 400px) {
/* At iPhone-SE-ish widths the two scope buttons would otherwise
overflow because of their nowrap labels. Stack them vertically. */
.map-search-overlay .search-actions {
flex-direction: column;
}
.map-search-overlay input[type="text"] { width: 120px; }
}
/* Display-only color legend at bottom-center of the map. The functional
source filter (toggle/show-hide) lives in #sourceFilter in the side
Expand Down Expand Up @@ -401,20 +458,32 @@ Circle size = log(sample count). Color = dominant data source.
<div class="globe-layout">
<div class="map-wrap">
<div id="cesiumContainer"></div>
<!-- Slim search overlay (Hana Figma 222:456). One-line input + magnifier
button at the top-right of the map. The two scope buttons and the
help paragraph are preserved in the DOM (display:none via CSS) so
existing zoomWatcher handlers bind by id and the `?search_scope=area`
URL hydration path still works — area-scoped search is no longer
surfaced in the in-map UI but remains reachable via the URL. -->
<div class="map-search-overlay">
<div class="search-bar">
<input type="text" id="sampleSearch" placeholder="Search samples — multiple words narrow results (e.g., pottery Cyprus)" />
</div>
<div class="search-actions">
<button id="searchAreaBtn" type="button" title="Limit results to samples within the current map view">Search Selected Areas</button>
<button id="searchWorldBtn" type="button" title="Search all samples globally">Search Entire World</button>
</div>
<div class="search-help">
Searches labels, descriptions, and place names. <strong>First search can take 10-15 seconds</strong> while data loads; subsequent searches are faster.
<a href="https://github.com/isamplesorg/isamplesorg.github.io/issues/169" target="_blank" rel="noopener noreferrer" style="color: #888; text-decoration: underline;">Tracking issue: faster substrate FTS in progress</a>.
<input type="text" id="sampleSearch"
placeholder="Search samples — e.g., pottery Cyprus"
title="Searches labels, descriptions, and place names. First search can take 10-15 seconds while data loads; subsequent searches are faster. Press Enter or click the search button to submit." />
<button id="searchSubmitBtn" type="button" aria-label="Search samples" title="Search (Enter)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"></circle><line x1="20" y1="20" x2="16.65" y2="16.65"></line></svg>
</button>
<!-- Preserved-but-hidden scope buttons: kept so existing zoomWatcher
`searchAreaBtn.addEventListener('click', ...)` / `searchWorldBtn`
bindings continue to compile and so any external code (tests,
bookmarklets) that clicks them programmatically still works.
`?search_scope=area` URL hydration does NOT depend on these — it
sets `_searchScope` directly and the boot auto-search reads it. -->
<div class="search-actions" aria-hidden="true">
<button id="searchAreaBtn" type="button" tabindex="-1">Search Selected Areas</button>
<button id="searchWorldBtn" type="button" tabindex="-1">Search Entire World</button>
</div>
<div id="searchResults" class="search-results" aria-live="polite"></div>
<div class="search-help" aria-hidden="true"></div>
</div>
<div id="searchResults" class="map-search-results-line" aria-live="polite"></div>
<div class="map-color-legend" aria-hidden="true">
<span><span class="map-color-legend-dot" style="background:#3366CC"></span>SESAR</span>
<span><span class="map-color-legend-dot" style="background:#DC3912"></span>OpenContext</span>
Expand Down Expand Up @@ -3100,6 +3169,12 @@ zoomWatcher = {

if (searchAreaBtn) searchAreaBtn.addEventListener('click', () => doSearch('area'));
if (searchWorldBtn) searchWorldBtn.addEventListener('click', () => doSearch('world'));
// Slim-overlay submit button — same behavior as Enter on `#sampleSearch`.
// Uses `_searchScope` so URL-hydrated `?search_scope=area` still routes
// to the area-scoped query path even though the scope toggle UI is
// hidden in the slim treatment.
const searchSubmitBtn = document.getElementById('searchSubmitBtn');
if (searchSubmitBtn) searchSubmitBtn.addEventListener('click', () => doSearch(_searchScope));
// Enter key uses the last-clicked scope (or the URL-hydrated scope if
// no button has been clicked yet). Defaults to 'world' for keyboard-only
// users on first invocation.
Expand Down
33 changes: 22 additions & 11 deletions tests/test_search_perf.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@
{
# Viewport-scoped search per #178 Light path. Camera position is
# set via the URL hash (Mediterranean / Cyprus area) so the
# area-scope predicate has a meaningful rect. Clicks the
# "Search Selected Areas" button instead of "Search Entire World".
# area-scope predicate has a meaningful rect. Area scope is routed
# via ?search_scope=area in the URL (see _measure_one_query).
"label": "area-scope",
"term": "pottery",
"filters": {"scope": "area"},
Expand Down Expand Up @@ -172,17 +172,22 @@ def _run_search(
*,
captured: list,
expected_id_after: int,
scope: str = "world",
) -> dict:
"""Type term, click the appropriate scope button, wait for the console event."""
"""Type term, click the visible submit button, wait for the console event.

The slim overlay (PR #224) hides the per-scope buttons (`display: none`),
so Playwright can no longer click them. Scope is instead routed through
`_searchScope`, which the page hydrates from `?search_scope=area` in the
URL — `_measure_one_query` sets that param up front. `#searchSubmitBtn`
is the same public path a keyboard/mouse user now exercises.
"""
search_input = page.locator("#sampleSearch")
search_input.click()
# Clear via select-all + delete (faster + works around platform shortcuts).
search_input.press("ControlOrMeta+a")
search_input.press("Delete")
search_input.fill(term)
button_id = "#searchAreaBtn" if scope == "area" else "#searchWorldBtn"
page.locator(button_id).click()
page.locator("#searchSubmitBtn").click()

# Wait for an isamples.search log whose id is strictly greater than the
# last one we observed. Polling is simpler than promise-based waits here.
Expand Down Expand Up @@ -226,27 +231,33 @@ def _measure_one_query(browser, query: dict) -> dict:
captured: list = []
_collect_search_logs(page, captured)

filters = query["filters"]
scope = filters.get("scope", "world")

# Area scope is hydrated from `?search_scope=area` (EXPLORER_URL
# already carries `?perf=1`, so append with `&`). The slim overlay
# hides the scope buttons, so the URL param is the public path the
# UI now uses to route to the area-scoped query.
scope_param = "&search_scope=area" if scope == "area" else ""
# Optional URL hash for area-scope cases (#178) — sets the camera
# before the search runs so the area predicate has a meaningful rect.
url_hash = query.get("url_hash")
target_url = EXPLORER_URL + (f"#{url_hash}" if url_hash else "")
target_url = (
EXPLORER_URL + scope_param + (f"#{url_hash}" if url_hash else "")
)
page.goto(target_url, wait_until="domcontentloaded", timeout=60_000)
_wait_for_explorer_ready(page)

filters = query["filters"]
if "source_only" in filters:
_apply_source_filter(page, filters["source_only"])
if "material_first_n" in filters:
_apply_material_first_n(page, filters["material_first_n"])
scope = filters.get("scope", "world")

cold = _run_search(
page, query["term"], captured=captured, expected_id_after=0,
scope=scope,
)
warm = _run_search(
page, query["term"], captured=captured, expected_id_after=cold["id"],
scope=scope,
)
finally:
context.close()
Expand Down
Loading