@@ -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 = `
`;
}
@@ -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 ? `
` : ''}`;
}
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 += `
${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');
}
}