+function facetFilterSQL() {
+ let sql = '';
+ const mat = getCheckedValues('materialFilterBody');
+ if (mat.length > 0) {
+ const list = mat.map(s => `'${s.replace(/'/g, "''")}'`).join(',');
+ sql += ` AND f.material IN (${list})`;
+ }
+ const ctx = getCheckedValues('contextFilterBody');
+ if (ctx.length > 0) {
+ const list = ctx.map(s => `'${s.replace(/'/g, "''")}'`).join(',');
+ sql += ` AND f.context IN (${list})`;
+ }
+ const ot = getCheckedValues('objectTypeFilterBody');
+ if (ot.length > 0) {
+ const list = ot.map(s => `'${s.replace(/'/g, "''")}'`).join(',');
+ sql += ` AND f.object_type IN (${list})`;
+ }
+ return sql;
+}
-### Filters
+// === URL State: encode/decode globe state in hash fragment ===
+function parseNum(val, def, min, max) {
+ if (val == null) return def;
+ const n = parseFloat(val);
+ if (!Number.isFinite(n)) return def;
+ if (min != null && n < min) return min;
+ if (max != null && n > max) return max;
+ return n;
+}
-```{ojs}
-facetSummariesWarning
-```
+function readHash() {
+ const params = new URLSearchParams(location.hash.slice(1));
+ return {
+ v: parseInt(params.get('v')) || 0,
+ lat: parseNum(params.get('lat'), null, -90, 90),
+ lng: parseNum(params.get('lng'), null, -180, 180),
+ alt: parseNum(params.get('alt'), null, 100, 40000000),
+ heading: parseNum(params.get('heading'), 0, 0, 360),
+ pitch: parseNum(params.get('pitch'), -90, -90, 0),
+ mode: params.get('mode') || null,
+ pid: params.get('pid') || null,
+ };
+}
-**Source**
+function buildHash(v) {
+ const cam = v.camera;
+ const carto = cam.positionCartographic;
+ const params = new URLSearchParams();
+ params.set('v', '1');
+ params.set('lat', Cesium.Math.toDegrees(carto.latitude).toFixed(4));
+ params.set('lng', Cesium.Math.toDegrees(carto.longitude).toFixed(4));
+ params.set('alt', Math.round(carto.height).toString());
+ const heading = Cesium.Math.toDegrees(cam.heading) % 360;
+ const pitch = Cesium.Math.toDegrees(cam.pitch);
+ if (Math.abs(heading) > 1) params.set('heading', heading.toFixed(1));
+ if (Math.abs(pitch + 90) > 1) params.set('pitch', pitch.toFixed(1));
+ const gs = v._globeState;
+ if (gs.mode === 'point') params.set('mode', 'point');
+ if (gs.selectedPid) params.set('pid', gs.selectedPid);
+ return '#' + params.toString();
+}
-```{ojs}
-// Source checkboxes with counts - uses pre-computed summaries for instant load
-viewof sourceCheckboxes = {
- const counts = facetsByType.source;
- const options = counts.map(r => r.value);
-
- return Inputs.checkbox(options, {
- value: initialParams.sources.filter(s => options.includes(s)),
- format: (x) => {
- const r = counts.find(s => s.value === x);
- const color = SOURCE_COLORS[x] || SOURCE_COLORS.default;
- const count = r ? Number(r.count).toLocaleString() : "0";
- return html`
-
- ${x} (${count})
- `;
- }
- });
+// === Helpers: update DOM imperatively (no OJS reactivity) ===
+function updateStats(phase, points, samples, time, pointsLabel, samplesLabel) {
+ const s = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = v; };
+ s('sPhase', phase);
+ s('sPoints', typeof points === 'string' ? points : points.toLocaleString());
+ s('sSamples', typeof samples === 'string' ? samples : samples.toLocaleString());
+ if (time != null) s('sTime', time);
+ if (pointsLabel) s('sPointsLbl', pointsLabel);
+ if (samplesLabel) s('sSamplesLbl', samplesLabel);
}
-```
-**Material**
+function updatePhaseMsg(text, type) {
+ const m = document.getElementById('phaseMsg');
+ if (!m) return;
+ m.textContent = text;
+ if (type === 'loading') { m.style.background = '#e3f2fd'; m.style.color = '#1565c0'; }
+ else { m.style.background = '#e8f5e9'; m.style.color = '#2e7d32'; }
+}
-```{ojs}
-// Material filter - loaded from pre-computed summaries
-viewof materialCheckboxes = {
- const counts = facetsByType.material;
- const options = counts.map(r => r.value);
- return Inputs.checkbox(options, {
- value: [],
- format: (x) => {
- const r = counts.find(s => s.value === x);
- const count = r ? Number(r.count).toLocaleString() : "0";
- return html`
- ${prettyLabel(x)} (${count})
- `;
+function updateClusterCard(info) {
+ const el = document.getElementById('clusterSection');
+ if (!el) return;
+ if (!info) {
+ el.innerHTML = '
Click a cluster or sample on the globe
';
+ return;
}
- });
+ const color = SOURCE_COLORS[info.source] || '#666';
+ const name = SOURCE_NAMES[info.source] || info.source;
+ el.innerHTML = `
Selected Cluster
+
+
+ ${name}
+ H3 res${info.resolution}
+
+
+ ${info.count.toLocaleString()} samples
+
+
+ ${info.lat.toFixed(4)}, ${info.lng.toFixed(4)}
+
+
`;
}
-```
-**Sampled Feature**
+function updateSampleCard(sample) {
+ const el = document.getElementById('clusterSection');
+ if (!el) return;
+ const color = SOURCE_COLORS[sample.source] || '#666';
+ const name = SOURCE_NAMES[sample.source] || sample.source;
+ const placeParts = sample.place_name;
+ const placeStr = Array.isArray(placeParts) && placeParts.length > 0
+ ? placeParts.filter(Boolean).join(' › ')
+ : '';
+ const srcUrl = sourceUrl(sample.pid);
+ el.innerHTML = `
Sample
+
+
+ ${name}
+
+
+ ${sample.label || sample.pid || 'Unnamed'}
+
+
+ ${sample.lat.toFixed(5)}, ${sample.lng.toFixed(5)}
+
+ ${placeStr ? `
${placeStr}
` : ''}
+ ${sample.result_time ? `
Date: ${sample.result_time}
` : ''}
+ ${srcUrl ? `
` : ''}
+
Loading full details...
+
`;
+}
-```{ojs}
-// Context filter - loaded from pre-computed summaries
-viewof contextCheckboxes = {
- const counts = facetsByType.context;
- const options = counts.map(r => r.value);
- return Inputs.checkbox(options, {
- value: [],
- format: (x) => {
- const r = counts.find(s => s.value === x);
- const count = r ? Number(r.count).toLocaleString() : "0";
- return html`
- ${prettyLabel(x)} (${count})
- `;
+function updateSampleDetail(detail) {
+ const el = document.getElementById('sampleDetail');
+ if (!el) return;
+ if (!detail) {
+ el.innerHTML = '
Detail query failed';
+ return;
}
- });
+ const desc = detail.description
+ ? (detail.description.length > 300 ? detail.description.slice(0, 300) + '...' : detail.description)
+ : '';
+ el.innerHTML = `${desc ? `
${desc}
` : ''}`;
}
-```
-
-**Specimen Type**
-```{ojs}
-// Object type filter - loaded from pre-computed summaries
-viewof objectTypeCheckboxes = {
- const counts = facetsByType.object_type;
- const options = counts.map(r => r.value);
- return Inputs.checkbox(options, {
- value: [],
- format: (x) => {
- const r = counts.find(s => s.value === x);
- const count = r ? Number(r.count).toLocaleString() : "0";
- return html`
- ${prettyLabel(x)} (${count})
- `;
+function updateSamples(samples) {
+ const el = document.getElementById('samplesSection');
+ if (!el) return;
+ if (!samples || samples.length === 0) {
+ el.innerHTML = '';
+ return;
}
- });
+ let h = `
Nearby Samples (${samples.length})
`;
+ for (const s of samples) {
+ const color = SOURCE_COLORS[s.source] || '#666';
+ const name = SOURCE_NAMES[s.source] || s.source;
+ const placeParts = s.place_name;
+ const desc = Array.isArray(placeParts) && placeParts.length > 0
+ ? placeParts.filter(Boolean).join(' › ')
+ : '';
+ const sUrl = sourceUrl(s.pid);
+ h += `
+
+ ${desc ? `
${desc}
` : ''}
+
`;
+ }
+ el.innerHTML = h;
}
-```
-
-```{ojs}
-html`
Clear All Filters`
-```
-
-**Max Samples**
-```{ojs}
-viewof maxSamples = Inputs.range([1000, 100000], {
- value: 25000,
- step: 1000
-})
-```
-
-
-
-```{ojs}
-// Update URL without reloading
-{
- const params = new URLSearchParams();
- if (searchInput) params.set("q", searchInput);
- if (sourceCheckboxes?.length) params.set("sources", sourceCheckboxes.join(","));
- if (materialCheckboxes?.length) params.set("material", materialCheckboxes.join(","));
- if (contextCheckboxes?.length) params.set("context", contextCheckboxes.join(","));
- if (objectTypeCheckboxes?.length) params.set("object_type", objectTypeCheckboxes.join(","));
-
- const newUrl = params.toString() ? `?${params.toString()}` : window.location.pathname;
- if (window.location.search !== `?${params.toString()}`) {
- history.replaceState(null, "", newUrl);
- }
+// === Results table (below globe) ===
+// Renders the first N samples matching current filters, regardless of camera.
+// Stable, sourceless ordering (no ORDER BY RANDOM) keeps it cheap on lite_url.
+function renderResultsTable(rows) {
+ const el = document.getElementById('resultsTable');
+ if (!el) return;
+ if (!rows || rows.length === 0) {
+ el.innerHTML = '
No samples match the current filters.
';
+ return;
+ }
+ const head = `
| Source | Label | Place | Lat | Lon |
|---|
`;
+ const body = rows.map(r => {
+ const color = SOURCE_COLORS[r.source] || '#666';
+ const name = SOURCE_NAMES[r.source] || r.source;
+ const placeParts = r.place_name;
+ const place = Array.isArray(placeParts) && placeParts.length > 0
+ ? placeParts.filter(Boolean).join(' › ')
+ : '';
+ const lat = (r.latitude != null) ? r.latitude.toFixed(4) : '';
+ const lng = (r.longitude != null) ? r.longitude.toFixed(4) : '';
+ const sUrl = sourceUrl(r.pid);
+ const labelHtml = sUrl
+ ? `
${r.label || r.pid}`
+ : (r.label || r.pid);
+ return `
+ | ${name} |
+ ${labelHtml} |
+ ${place} |
+ ${lat} |
+ ${lng} |
+
`;
+ }).join('');
+ el.innerHTML = `
`;
}
-```
-
-
-
-
- SESAR
- OpenContext
- GEOME
- Smithsonian
-
-
-Loading data...
-
-
-```{ojs}
-// Show result count
-html`
- Showing ${sampleData.length.toLocaleString()} of ${Number(totalCount).toLocaleString()} matching samples
-
`
+function updateResultsTableMeta(text, isLoading) {
+ const el = document.getElementById('resultsTableMeta');
+ if (!el) return;
+ el.textContent = text;
+ el.style.color = isLoading ? '#1565c0' : '#555';
+}
```
-