Loading flag for band views and downloads
Browse files- app.js +23 -5
- bands.js +12 -6
- download.js +2 -2
app.js
CHANGED
|
@@ -16,6 +16,7 @@ import { DATASETS, loadCoverage, samplesAt } from './footprints.js'
|
|
| 16 |
import { placeLabels } from './labels.js'
|
| 17 |
import { imageUrl, loadThumbnail } from './samples.js'
|
| 18 |
import { bandNames, render, views } from './bands.js'
|
|
|
|
| 19 |
import { downloadAll, downloadBand, pythonSnippet } from './download.js'
|
| 20 |
import { cellAt, cellIndex, renderTile } from './tiles.js'
|
| 21 |
|
|
@@ -401,10 +402,11 @@ async function showView(id) {
|
|
| 401 |
if (id === 'thumbnail') { pick.shown = { src: t.src, values: null, sample: t.sample, label: 'Thumbnail' }; refreshShown(); return }
|
| 402 |
const view = pick.views.find(v => v.id === id), key = `${state.name}/${t.sample.part}/${t.sample.parquet_row}/${id}`
|
| 403 |
$('pick-stats').textContent = `Loading ${view.label}…`
|
| 404 |
-
const fresh = !renders.has(key)
|
| 405 |
-
if (fresh) renders.set(key, render(state.info, t.sample, view).catch(e => { renders.delete(key); throw e }))
|
|
|
|
| 406 |
try {
|
| 407 |
-
const r = await renders.get(key)
|
| 408 |
if (ticket !== pick.viewTicket) return
|
| 409 |
pick.shown = { ...r, sample: t.sample, label: view.label }
|
| 410 |
refreshShown()
|
|
@@ -415,6 +417,17 @@ async function showView(id) {
|
|
| 415 |
}
|
| 416 |
}
|
| 417 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
function refreshShown() {
|
| 419 |
showThumbnail(pick.shown.src)
|
| 420 |
showOnMap()
|
|
@@ -447,8 +460,13 @@ async function downloading(button, job) {
|
|
| 447 |
|
| 448 |
$('dl-band').addEventListener('click', () => downloading($('dl-band'),
|
| 449 |
() => downloadBand(state.info, state.name, pick.cell.name, pick.thumbnail.sample, shownBand())))
|
| 450 |
-
$('dl-all').addEventListener('click', () => downloading($('dl-all'),
|
| 451 |
-
() =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
$('dl-python').addEventListener('click', async () => {
|
| 453 |
const code = pythonSnippet(state.info, state.name, pick.cell.name, pick.thumbnail.sample, pick.bands.length ? pick.bands : ['thumbnail'])
|
| 454 |
window.lastSnippet = code // for automated checks
|
|
|
|
| 16 |
import { placeLabels } from './labels.js'
|
| 17 |
import { imageUrl, loadThumbnail } from './samples.js'
|
| 18 |
import { bandNames, render, views } from './bands.js'
|
| 19 |
+
import { newStats } from './parquet.js'
|
| 20 |
import { downloadAll, downloadBand, pythonSnippet } from './download.js'
|
| 21 |
import { cellAt, cellIndex, renderTile } from './tiles.js'
|
| 22 |
|
|
|
|
| 402 |
if (id === 'thumbnail') { pick.shown = { src: t.src, values: null, sample: t.sample, label: 'Thumbnail' }; refreshShown(); return }
|
| 403 |
const view = pick.views.find(v => v.id === id), key = `${state.name}/${t.sample.part}/${t.sample.parquet_row}/${id}`
|
| 404 |
$('pick-stats').textContent = `Loading ${view.label}…`
|
| 405 |
+
const fresh = !renders.has(key), stats = newStats()
|
| 406 |
+
if (fresh) renders.set(key, render(state.info, t.sample, view, stats).catch(e => { renders.delete(key); throw e }))
|
| 407 |
+
const stop = fresh ? followLoad(`${view.label} · ${pick.cell.name}`, stats) : () => {}
|
| 408 |
try {
|
| 409 |
+
const r = await renders.get(key).finally(stop)
|
| 410 |
if (ticket !== pick.viewTicket) return
|
| 411 |
pick.shown = { ...r, sample: t.sample, label: view.label }
|
| 412 |
refreshShown()
|
|
|
|
| 417 |
}
|
| 418 |
}
|
| 419 |
|
| 420 |
+
/** The loading flag over the map while a band read runs: bytes of the expected total, then its phase (decoding).
|
| 421 |
+
* Returns the function that ends it. */
|
| 422 |
+
function followLoad(text, stats) {
|
| 423 |
+
startLoader(text)
|
| 424 |
+
const timer = setInterval(() => {
|
| 425 |
+
loaderProgress(stats.bytes, stats.expected)
|
| 426 |
+
if (stats.phase) $('loader-phase').textContent = stats.phase
|
| 427 |
+
}, 120)
|
| 428 |
+
return () => { clearInterval(timer); stopLoader() }
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
function refreshShown() {
|
| 432 |
showThumbnail(pick.shown.src)
|
| 433 |
showOnMap()
|
|
|
|
| 460 |
|
| 461 |
$('dl-band').addEventListener('click', () => downloading($('dl-band'),
|
| 462 |
() => downloadBand(state.info, state.name, pick.cell.name, pick.thumbnail.sample, shownBand())))
|
| 463 |
+
$('dl-all').addEventListener('click', () => downloading($('dl-all'), async () => {
|
| 464 |
+
const stats = newStats(), stop = followLoad(`All bands · ${pick.cell.name} · for download`, stats)
|
| 465 |
+
try {
|
| 466 |
+
return await downloadAll(state.info, state.name, pick.cell.name, pick.thumbnail.sample, pick.thumbnail.src,
|
| 467 |
+
pick.thumbnail.productId, stats)
|
| 468 |
+
} finally { stop() }
|
| 469 |
+
}))
|
| 470 |
$('dl-python').addEventListener('click', async () => {
|
| 471 |
const code = pythonSnippet(state.info, state.name, pick.cell.name, pick.thumbnail.sample, pick.bands.length ? pick.bands : ['thumbnail'])
|
| 472 |
window.lastSnippet = code // for automated checks
|
bands.js
CHANGED
|
@@ -33,14 +33,18 @@ export async function bandNames(info, sample) {
|
|
| 33 |
return metadata.schema.slice(1).map(c => c.name).filter(n => !NOT_BANDS.has(n))
|
| 34 |
}
|
| 35 |
|
| 36 |
-
/** The GeoTIFF bytes of `bands` of one sample (read once, then kept): {name: Uint8Array}, and the read's stats.
|
| 37 |
-
|
| 38 |
-
|
|
|
|
| 39 |
const missing = bands.filter(b => !raw.has(key(b)))
|
| 40 |
if (missing.length) {
|
| 41 |
const { file, metadata } = await openFile(url, stats)
|
| 42 |
let rowStart = 0
|
| 43 |
for (let g = 0; g < sample.parquet_row; g++) rowStart += Number(metadata.row_groups[g].num_rows)
|
|
|
|
|
|
|
|
|
|
| 44 |
await parquetRead({ file, metadata, compressors, columns: missing, rowStart, rowEnd: rowStart + 1, rowFormat: 'object',
|
| 45 |
utf8: false, onComplete(rows) { for (const b of missing) raw.set(key(b), rows[0][b]) } })
|
| 46 |
}
|
|
@@ -79,10 +83,12 @@ function terrain(t) {
|
|
| 79 |
return TERRAIN.at(-1)[1]
|
| 80 |
}
|
| 81 |
|
| 82 |
-
/** One view of a sample as a PNG object URL: {src, size, values (single band: for the cursor), stats}.
|
| 83 |
-
|
|
|
|
| 84 |
const needed = [...new Set(view.use.flatMap(b => (b === 'ratio' ? ['vv', 'vh'] : [b])))]
|
| 85 |
-
const [bytes
|
|
|
|
| 86 |
const bands = Object.fromEntries(await Promise.all(needed.map(async b => [b, await decode(bytes[b])])))
|
| 87 |
const size = Math.max(...needed.map(b => bands[b].width))
|
| 88 |
const s1 = 'vv' in bands, at = (band, x, y) => { // value at composite pixel (x, y), nearest
|
|
|
|
| 33 |
return metadata.schema.slice(1).map(c => c.name).filter(n => !NOT_BANDS.has(n))
|
| 34 |
}
|
| 35 |
|
| 36 |
+
/** The GeoTIFF bytes of `bands` of one sample (read once, then kept): {name: Uint8Array}, and the read's stats.
|
| 37 |
+
* stats.expected: the bytes the missing columns take in the row group (for a progress display), set before reading. */
|
| 38 |
+
export async function readBands(info, sample, bands, stats = newStats()) {
|
| 39 |
+
const url = imageUrl(info, sample.part), key = b => `${url}#${sample.parquet_row}/${b}`
|
| 40 |
const missing = bands.filter(b => !raw.has(key(b)))
|
| 41 |
if (missing.length) {
|
| 42 |
const { file, metadata } = await openFile(url, stats)
|
| 43 |
let rowStart = 0
|
| 44 |
for (let g = 0; g < sample.parquet_row; g++) rowStart += Number(metadata.row_groups[g].num_rows)
|
| 45 |
+
stats.expected = metadata.row_groups[sample.parquet_row].columns
|
| 46 |
+
.filter(c => missing.includes(c.meta_data.path_in_schema[0]))
|
| 47 |
+
.reduce((sum, c) => sum + Number(c.meta_data.total_compressed_size), 0)
|
| 48 |
await parquetRead({ file, metadata, compressors, columns: missing, rowStart, rowEnd: rowStart + 1, rowFormat: 'object',
|
| 49 |
utf8: false, onComplete(rows) { for (const b of missing) raw.set(key(b), rows[0][b]) } })
|
| 50 |
}
|
|
|
|
| 83 |
return TERRAIN.at(-1)[1]
|
| 84 |
}
|
| 85 |
|
| 86 |
+
/** One view of a sample as a PNG object URL: {src, size, values (single band: for the cursor), stats}.
|
| 87 |
+
* stats (passed in to follow progress): bytes as they arrive, expected, then phase 'Decoding'. */
|
| 88 |
+
export async function render(info, sample, view, stats = newStats()) {
|
| 89 |
const needed = [...new Set(view.use.flatMap(b => (b === 'ratio' ? ['vv', 'vh'] : [b])))]
|
| 90 |
+
const [bytes] = await readBands(info, sample, needed, stats)
|
| 91 |
+
stats.phase = 'Decoding'
|
| 92 |
const bands = Object.fromEntries(await Promise.all(needed.map(async b => [b, await decode(bytes[b])])))
|
| 93 |
const size = Math.max(...needed.map(b => bands[b].width))
|
| 94 |
const s1 = 'vv' in bands, at = (band, x, y) => { // value at composite pixel (x, y), nearest
|
download.js
CHANGED
|
@@ -25,9 +25,9 @@ export async function downloadBand(info, name, cell, sample, band) {
|
|
| 25 |
}
|
| 26 |
|
| 27 |
/** Every band, the thumbnail and metadata.json, in one zip (stored, not recompressed: the GeoTIFFs are LZW). */
|
| 28 |
-
export async function downloadAll(info, name, cell, sample, thumbnailSrc, productId) {
|
| 29 |
const bands = await bandNames(info, sample)
|
| 30 |
-
const [bytes
|
| 31 |
const base = stem(cell, name, sample), entries = {}
|
| 32 |
for (const b of bands) entries[`${base}_${b}.tif`] = bytes[b]
|
| 33 |
const thumb = new Uint8Array(await (await fetch(thumbnailSrc)).arrayBuffer())
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
/** Every band, the thumbnail and metadata.json, in one zip (stored, not recompressed: the GeoTIFFs are LZW). */
|
| 28 |
+
export async function downloadAll(info, name, cell, sample, thumbnailSrc, productId, stats) {
|
| 29 |
const bands = await bandNames(info, sample)
|
| 30 |
+
const [bytes] = await readBands(info, sample, bands, stats)
|
| 31 |
const base = stem(cell, name, sample), entries = {}
|
| 32 |
for (const b of bands) entries[`${base}_${b}.tif`] = bytes[b]
|
| 33 |
const thumb = new Uint8Array(await (await fetch(thumbnailSrc)).arrayBuffer())
|