diff --git a/tutorials/progressive_globe.qmd b/tutorials/progressive_globe.qmd index 26f5bd7..a8b8668 100644 --- a/tutorials/progressive_globe.qmd +++ b/tutorials/progressive_globe.qmd @@ -69,9 +69,11 @@ Circle size = log(sample count). Color = dominant data source. } .stat-box .val { font-weight: bold; font-size: 16px; font-family: monospace; display: block; } .stat-box .lbl { color: #aaa; font-size: 11px; } - .legend { display: flex; gap: 10px; flex-wrap: wrap; font-size: 12px; } - .legend-item { display: flex; align-items: center; gap: 3px; } + .legend { display: flex; gap: 8px; flex-wrap: wrap; font-size: 12px; } + .legend-item { display: flex; align-items: center; gap: 3px; cursor: pointer; user-select: none; } + .legend-item.disabled { opacity: 0.3; } .legend-dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; } + .legend-item input[type="checkbox"] { margin: 0; width: 12px; height: 12px; cursor: pointer; } .source-badge { color: white; padding: 2px 8px; border-radius: 10px; font-size: 0.8em; white-space: nowrap; } .cluster-card { border-left: 4px solid #ccc; padding: 10px 12px; background: white; border-radius: 0 6px 6px 0; } .sample-row { padding: 6px 0; border-bottom: 1px solid #eee; line-height: 1.4; } @@ -99,16 +101,16 @@ Circle size = log(sample count). Color = dominant data source.
Loading...Resolution
-
0Clusters
-
0Samples
+
0Clusters Loaded
+
0Samples Loaded
-Load Time
-
- SESAR - OpenContext - GEOME - Smithsonian +
+ + + +
@@ -152,6 +154,29 @@ SOURCE_NAMES = ({ GEOME: 'GEOME', SMITHSONIAN: 'Smithsonian' }) +// === Source URL: resolve pid to original repository === +function sourceUrl(pid) { + if (!pid) return null; + // All sources resolve via n2t.net: + // ARK pids (OpenContext, GEOME, Smithsonian) → n2t.net/ark:/... + // IGSN pids (SESAR) → n2t.net/IGSN:... + return `https://n2t.net/${pid}`; +} + +// === Source Filter: get active sources and build SQL clause === +function getActiveSources() { + const checks = document.querySelectorAll('#sourceFilter input[type="checkbox"]'); + return Array.from(checks).filter(c => c.checked).map(c => c.value); +} + +function sourceFilterSQL(col) { + const active = getActiveSources(); + if (active.length === 0) return ' AND 1=0'; // nothing checked = show nothing + if (active.length === 4) return ''; // all checked = no filter + const list = active.map(s => `'${s}'`).join(','); + return ` AND ${col} IN (${list})`; +} + // === URL State: encode/decode globe state in hash fragment === function parseNum(val, def, min, max) { if (val == null) return def; @@ -195,13 +220,14 @@ function buildHash(v) { } // === Helpers: update DOM imperatively (no OJS reactivity) === -function updateStats(phase, points, samples, time, pointsLabel) { +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); } function updatePhaseMsg(text, type) { @@ -245,6 +271,7 @@ function updateSampleCard(sample) { const placeStr = Array.isArray(placeParts) && placeParts.length > 0 ? placeParts.filter(Boolean).join(' › ') : ''; + const srcUrl = sourceUrl(sample.pid); el.innerHTML = `

Sample

@@ -258,6 +285,7 @@ function updateSampleCard(sample) {
${placeStr ? `
${placeStr}
` : ''} ${sample.result_time ? `
Date: ${sample.result_time}
` : ''} + ${srcUrl ? `
View at ${name} →
` : ''}
Loading full details...
`; } @@ -272,10 +300,7 @@ function updateSampleDetail(detail) { const desc = detail.description ? (detail.description.length > 300 ? detail.description.slice(0, 300) + '...' : detail.description) : ''; - el.innerHTML = `${desc ? `
${desc}
` : ''} -
- Open in Analysis Tool → -
`; + el.innerHTML = `${desc ? `
${desc}
` : ''}`; } function updateSamples(samples) { @@ -293,9 +318,10 @@ function updateSamples(samples) { const desc = Array.isArray(placeParts) && placeParts.length > 0 ? placeParts.filter(Boolean).join(' › ') : ''; + const sUrl = sourceUrl(s.pid); h += `
- ${s.label || s.pid} + ${sUrl ? `${s.label || s.pid}` : `${s.label || s.pid}`} ${name}
${desc ? `
${desc}
` : ''} @@ -442,6 +468,7 @@ viewer = { FROM read_parquet('${lite_url}') WHERE latitude BETWEEN ${meta.lat - delta} AND ${meta.lat + delta} AND longitude BETWEEN ${meta.lng - delta} AND ${meta.lng + delta} + ${sourceFilterSQL('source')} LIMIT 30 `); updateSamples(samples); @@ -468,6 +495,7 @@ phase1 = { SELECT h3_cell, sample_count, center_lat, center_lng, dominant_source, source_count FROM read_parquet('${h3_res4_url}') + WHERE 1=1${sourceFilterSQL('dominant_source')} `); const scalar = new Cesium.NearFarScalar(1.5e2, 1.5, 8.0e6, 0.5); @@ -494,7 +522,7 @@ phase1 = { performance.measure('p1', 'p1-start', 'p1-end'); const elapsed = performance.getEntriesByName('p1').pop().duration; - updateStats('H3 Res4', data.length, totalSamples, `${(elapsed/1000).toFixed(1)}s`, 'Global Clusters'); + updateStats('H3 Res4', data.length, totalSamples, `${(elapsed/1000).toFixed(1)}s`, 'Clusters Loaded', 'Samples Loaded'); updatePhaseMsg(`${data.length.toLocaleString()} clusters, ${totalSamples.toLocaleString()} samples. Zoom in for finer detail.`, 'done'); console.log(`Phase 1: ${data.length} clusters in ${elapsed.toFixed(0)}ms`); @@ -527,8 +555,9 @@ zoomWatcher = { let cachedData = null; // array of rows // --- H3 cluster loading (existing logic) --- + let loadResGen = 0; // generation counter to discard stale results const loadRes = async (res, url) => { - if (loading) return; + const gen = ++loadResGen; // claim a generation loading = true; updatePhaseMsg(`Loading H3 res${res}...`, 'loading'); @@ -538,8 +567,10 @@ zoomWatcher = { SELECT h3_cell, sample_count, center_lat, center_lng, dominant_source, source_count FROM read_parquet('${url}') + WHERE 1=1${sourceFilterSQL('dominant_source')} `); + if (gen !== loadResGen) return; // stale — a newer call superseded this one viewer.h3Points.removeAll(); const scalar = new Cesium.NearFarScalar(1.5e2, 1.5, 8.0e6, 0.3); let total = 0; @@ -567,7 +598,7 @@ zoomWatcher = { // Show viewport count immediately const bounds = getViewportBounds(); const inView = countInViewport(bounds); - updateStats(`H3 Res${res}`, `${inView.clusters.toLocaleString()} / ${data.length.toLocaleString()}`, inView.samples.toLocaleString(), `${(elapsed/1000).toFixed(1)}s`, 'In View / Total'); + updateStats(`H3 Res${res}`, `${inView.clusters.toLocaleString()} / ${data.length.toLocaleString()}`, inView.samples.toLocaleString(), `${(elapsed/1000).toFixed(1)}s`, 'Clusters in View / Loaded', 'Samples in View'); updatePhaseMsg(`${data.length.toLocaleString()} clusters, ${total.toLocaleString()} samples. ${res < 8 ? 'Zoom in for finer detail.' : 'Zoom closer for individual samples.'}`, 'done'); currentRes = res; @@ -649,6 +680,7 @@ zoomWatcher = { FROM read_parquet('${lite_url}') WHERE latitude BETWEEN ${padded.south} AND ${padded.north} AND longitude BETWEEN ${padded.west} AND ${padded.east} + ${sourceFilterSQL('source')} LIMIT ${POINT_BUDGET} `); performance.mark('sp-e'); @@ -667,7 +699,7 @@ zoomWatcher = { renderSamplePoints(cachedData, bounds); - updateStats('Samples', cachedData.length, cachedData.length, `${(elapsed/1000).toFixed(1)}s`, 'In View'); + updateStats('Samples', cachedData.length, cachedData.length, `${(elapsed/1000).toFixed(1)}s`, 'Samples in View', 'Samples in View'); updatePhaseMsg(`${cachedData.length.toLocaleString()} individual samples. Click one for details.`, 'done'); console.log(`Point mode: ${cachedData.length} samples in ${elapsed.toFixed(0)}ms`); @@ -730,14 +762,31 @@ zoomWatcher = { const inView = countInViewport(bounds); const total = viewer._clusterTotal; if (total) { - updateStats(`H3 Res${currentRes}`, `${inView.clusters.toLocaleString()} / ${total.clusters.toLocaleString()}`, inView.samples.toLocaleString(), '—', 'In View / Total'); + updateStats(`H3 Res${currentRes}`, `${inView.clusters.toLocaleString()} / ${total.clusters.toLocaleString()}`, inView.samples.toLocaleString(), '—', 'Clusters in View / Loaded', 'Samples in View'); } else { - updateStats(`H3 Res${currentRes}`, viewer.h3Points.length, '—', '—', 'Global Clusters'); + updateStats(`H3 Res${currentRes}`, viewer.h3Points.length, '—', '—', 'Clusters Loaded', 'Samples Loaded'); } updatePhaseMsg(`${inView.clusters.toLocaleString()} clusters in view. Zoom closer for individual samples.`, 'done'); console.log('Exited point mode'); } + // --- Source filter change handler --- + const resUrls = { 4: h3_res4_url, 6: h3_res6_url, 8: h3_res8_url }; + document.getElementById('sourceFilter').addEventListener('change', async () => { + // Toggle visual state on labels + document.querySelectorAll('#sourceFilter .legend-item').forEach(li => { + const cb = li.querySelector('input'); + li.classList.toggle('disabled', !cb.checked); + }); + if (mode === 'cluster') { + loading = false; // allow loadRes to run (gen counter discards stale results) + await loadRes(currentRes, resUrls[currentRes]); + } else { + cachedBounds = null; // force re-query + await loadViewportSamples(); + } + }); + // --- Camera change handler --- let timer = null; viewer.camera.changed.addEventListener(() => { @@ -780,7 +829,7 @@ zoomWatcher = { const inView = countInViewport(bounds); const total = viewer._clusterTotal; if (total) { - updateStats(`H3 Res${currentRes}`, `${inView.clusters.toLocaleString()} / ${total.clusters.toLocaleString()}`, inView.samples.toLocaleString(), null, 'In View / Total'); + updateStats(`H3 Res${currentRes}`, `${inView.clusters.toLocaleString()} / ${total.clusters.toLocaleString()}`, inView.samples.toLocaleString(), null, 'Clusters in View / Loaded', 'Samples in View'); } }