Background loading of the other datasets
Browse files
README.md
CHANGED
|
@@ -10,7 +10,7 @@ short_description: Major TOM Core samples on a 3D globe, read in the browser
|
|
| 10 |
|
| 11 |
> **CLAUDE-GENERATED DOCUMENT** — automated LLM-generated content. Verify before relying on it.
|
| 12 |
|
| 13 |
-
> **Review status:** draft for review, 2026-11-02. Space
|
| 14 |
|
| 15 |
# Major TOM Raster Viewer
|
| 16 |
|
|
@@ -30,6 +30,8 @@ panel: dataset
|
|
| 30 |
-> app.js: MapLibre globe; cursor shows the cell and whether it has a sample
|
| 31 |
click a cell -> its footprint rows (page index) -> the image file on Hugging Face (one row group per sample)
|
| 32 |
-> samples.js: that row group's thumbnail column -> panel, with acquisition time, cloud cover or orbit, product
|
|
|
|
|
|
|
| 33 |
```
|
| 34 |
|
| 35 |
## Files
|
|
|
|
| 10 |
|
| 11 |
> **CLAUDE-GENERATED DOCUMENT** — automated LLM-generated content. Verify before relying on it.
|
| 12 |
|
| 13 |
+
> **Review status:** draft for review, 2026-11-02. Space created (private) and deployed 2026-11-02.
|
| 14 |
|
| 15 |
# Major TOM Raster Viewer
|
| 16 |
|
|
|
|
| 30 |
-> app.js: MapLibre globe; cursor shows the cell and whether it has a sample
|
| 31 |
click a cell -> its footprint rows (page index) -> the image file on Hugging Face (one row group per sample)
|
| 32 |
-> samples.js: that row group's thumbnail column -> panel, with acquisition time, cloud cover or orbit, product
|
| 33 |
+
in the background: the other datasets' footprints once the first is drawn; after a click, the same cell's samples
|
| 34 |
+
and first thumbnail in the other datasets (kept, so switching dataset shows them at once)
|
| 35 |
```
|
| 36 |
|
| 37 |
## Files
|
app.js
CHANGED
|
@@ -16,7 +16,7 @@ import { cellAt, cellIndex, renderTile } from './tiles.js'
|
|
| 16 |
const $ = id => document.getElementById(id)
|
| 17 |
const COLOURS = { S2L2A: [57, 181, 74], S2L1C: [0, 113, 188], S1RTC: [241, 90, 36], DEM: [180, 140, 90] }
|
| 18 |
|
| 19 |
-
const state = { name: null, info: null, coverage: null, version: 0, loading: 0 }
|
| 20 |
|
| 21 |
// ---- map ---------------------------------------------------------------------------------------------------
|
| 22 |
|
|
@@ -67,6 +67,7 @@ map.on('load', async () => {
|
|
| 67 |
map.getSource('coast').setData(coastLines(feature(topo, topo.objects.land)))
|
| 68 |
map.getSource('land').setData(continuousRings(feature(topo, topo.objects.land)))
|
| 69 |
const datasets = await DATASETS
|
|
|
|
| 70 |
buildDatasetList(datasets)
|
| 71 |
setDataset(Object.keys(datasets)[0], datasets)
|
| 72 |
})
|
|
@@ -220,11 +221,47 @@ async function setDataset(name, datasets) {
|
|
| 220 |
$('stat-draw').textContent = secs(performance.now() - t0)
|
| 221 |
document.body.dataset.loaded = name // for automated checks
|
| 222 |
stopLoader()
|
|
|
|
| 223 |
})
|
| 224 |
}
|
| 225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
// ---- selected cell -----------------------------------------------------------------------------------------
|
| 227 |
// click -> cell -> its samples (footprint rows) -> buttons when there are several -> thumbnail of the chosen one
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
const pick = { cell: null, samples: [], chosen: 0, thumbnail: null, ticket: 0 }
|
| 230 |
|
|
@@ -251,14 +288,14 @@ async function pickCell() {
|
|
| 251 |
$('choices').replaceChildren(); $('pick-meta').replaceChildren(); $('pick-stats').textContent = ''
|
| 252 |
let samples
|
| 253 |
try {
|
| 254 |
-
samples = await
|
| 255 |
} catch (error) {
|
| 256 |
if (ticket === pick.ticket) showThumbnail(null, `Could not load: ${error.message}`)
|
| 257 |
return
|
| 258 |
}
|
| 259 |
if (ticket !== pick.ticket) return
|
| 260 |
pick.samples = samples
|
| 261 |
-
if (!samples.length) { showThumbnail(null, `No ${state.info.label} sample in this cell`); return }
|
| 262 |
if (samples.length > 1) {
|
| 263 |
$('choices').replaceChildren(...samples.map((s, i) => {
|
| 264 |
const b = document.createElement('button')
|
|
@@ -277,13 +314,14 @@ async function choose(i) {
|
|
| 277 |
meta(sample, null, info)
|
| 278 |
showThumbnail(null, 'Loading the thumbnail…')
|
| 279 |
try {
|
| 280 |
-
const
|
| 281 |
-
if (ticket !== pick.ticket)
|
| 282 |
-
if (pick.thumbnail) URL.revokeObjectURL(pick.thumbnail.src)
|
| 283 |
pick.thumbnail = t
|
| 284 |
showThumbnail(t.src)
|
| 285 |
meta(sample, t.productId, info)
|
| 286 |
-
$('pick-stats').textContent = `Thumbnail: ${kb(t.stats.bytes)} in ${t.stats.requests} requests · ${secs(t.stats.read)}`
|
|
|
|
|
|
|
| 287 |
} catch (error) {
|
| 288 |
if (ticket === pick.ticket) showThumbnail(null, `Could not load: ${error.message}`)
|
| 289 |
}
|
|
|
|
| 16 |
const $ = id => document.getElementById(id)
|
| 17 |
const COLOURS = { S2L2A: [57, 181, 74], S2L1C: [0, 113, 188], S1RTC: [241, 90, 36], DEM: [180, 140, 90] }
|
| 18 |
|
| 19 |
+
const state = { name: null, info: null, datasets: null, coverage: null, version: 0, loading: 0 }
|
| 20 |
|
| 21 |
// ---- map ---------------------------------------------------------------------------------------------------
|
| 22 |
|
|
|
|
| 67 |
map.getSource('coast').setData(coastLines(feature(topo, topo.objects.land)))
|
| 68 |
map.getSource('land').setData(continuousRings(feature(topo, topo.objects.land)))
|
| 69 |
const datasets = await DATASETS
|
| 70 |
+
state.datasets = datasets
|
| 71 |
buildDatasetList(datasets)
|
| 72 |
setDataset(Object.keys(datasets)[0], datasets)
|
| 73 |
})
|
|
|
|
| 221 |
$('stat-draw').textContent = secs(performance.now() - t0)
|
| 222 |
document.body.dataset.loaded = name // for automated checks
|
| 223 |
stopLoader()
|
| 224 |
+
prefetchFootprints()
|
| 225 |
})
|
| 226 |
}
|
| 227 |
|
| 228 |
+
/** The other datasets' footprints, one after another, once the shown one is drawn: switching is then instant. */
|
| 229 |
+
async function prefetchFootprints() {
|
| 230 |
+
for (const name of Object.keys(state.datasets)) {
|
| 231 |
+
try { await loadCoverage(name) } catch { /* loaded again, with its error shown, when picked */ }
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
// ---- selected cell -----------------------------------------------------------------------------------------
|
| 236 |
// click -> cell -> its samples (footprint rows) -> buttons when there are several -> thumbnail of the chosen one
|
| 237 |
+
// then, in the background, the same cell in the other datasets: samples and first thumbnail, one dataset at a time
|
| 238 |
+
// lookups and thumbnails are kept (promises, so a load under way is joined, not repeated)
|
| 239 |
+
|
| 240 |
+
const lookups = new Map() // "<dataset>/<row>/<col>" -> samples
|
| 241 |
+
const thumbs = new Map() // "<dataset>/<part>/<parquet_row>" -> thumbnail
|
| 242 |
+
|
| 243 |
+
function samplesOf(name, row, col) {
|
| 244 |
+
const key = `${name}/${row}/${col}`
|
| 245 |
+
if (!lookups.has(key)) lookups.set(key, samplesAt(name, row, col).catch(e => { lookups.delete(key); throw e }))
|
| 246 |
+
return lookups.get(key)
|
| 247 |
+
}
|
| 248 |
+
function thumbnailOf(name, sample) {
|
| 249 |
+
const key = `${name}/${sample.part}/${sample.parquet_row}`, fresh = !thumbs.has(key)
|
| 250 |
+
if (fresh) thumbs.set(key, loadThumbnail(state.datasets[name], sample).catch(e => { thumbs.delete(key); throw e }))
|
| 251 |
+
return [thumbs.get(key), fresh]
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/** The selected cell in every other dataset, in the panel's order, after the shown one. */
|
| 255 |
+
async function prefetchCell(cell) {
|
| 256 |
+
for (const name of Object.keys(state.datasets)) {
|
| 257 |
+
if (pick.cell !== cell) return // another cell was picked
|
| 258 |
+
if (name === state.name) continue
|
| 259 |
+
try {
|
| 260 |
+
const samples = await samplesOf(name, cell.row, cell.col)
|
| 261 |
+
if (samples.length) await thumbnailOf(name, samples[0])[0]
|
| 262 |
+
} catch { /* loaded again, with its error shown, when that dataset is shown */ }
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
|
| 266 |
const pick = { cell: null, samples: [], chosen: 0, thumbnail: null, ticket: 0 }
|
| 267 |
|
|
|
|
| 288 |
$('choices').replaceChildren(); $('pick-meta').replaceChildren(); $('pick-stats').textContent = ''
|
| 289 |
let samples
|
| 290 |
try {
|
| 291 |
+
samples = await samplesOf(name, row, col)
|
| 292 |
} catch (error) {
|
| 293 |
if (ticket === pick.ticket) showThumbnail(null, `Could not load: ${error.message}`)
|
| 294 |
return
|
| 295 |
}
|
| 296 |
if (ticket !== pick.ticket) return
|
| 297 |
pick.samples = samples
|
| 298 |
+
if (!samples.length) { showThumbnail(null, `No ${state.info.label} sample in this cell`); prefetchCell(pick.cell); return }
|
| 299 |
if (samples.length > 1) {
|
| 300 |
$('choices').replaceChildren(...samples.map((s, i) => {
|
| 301 |
const b = document.createElement('button')
|
|
|
|
| 314 |
meta(sample, null, info)
|
| 315 |
showThumbnail(null, 'Loading the thumbnail…')
|
| 316 |
try {
|
| 317 |
+
const [loading, fresh] = thumbnailOf(state.name, sample), t = await loading
|
| 318 |
+
if (ticket !== pick.ticket) return
|
|
|
|
| 319 |
pick.thumbnail = t
|
| 320 |
showThumbnail(t.src)
|
| 321 |
meta(sample, t.productId, info)
|
| 322 |
+
$('pick-stats').textContent = `Thumbnail: ${kb(t.stats.bytes)} in ${t.stats.requests} requests · ${secs(t.stats.read)}` +
|
| 323 |
+
(fresh ? '' : ' (loaded before)')
|
| 324 |
+
prefetchCell(pick.cell)
|
| 325 |
} catch (error) {
|
| 326 |
if (ticket === pick.ticket) showThumbnail(null, `Could not load: ${error.message}`)
|
| 327 |
}
|