mikonvergence commited on
Commit
5ad4e5a
·
verified ·
1 Parent(s): 8240fca

Loading flag for band views and downloads

Browse files
Files changed (3) hide show
  1. app.js +23 -5
  2. bands.js +12 -6
  3. 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
- () => downloadAll(state.info, state.name, pick.cell.name, pick.thumbnail.sample, pick.thumbnail.src, pick.thumbnail.productId)))
 
 
 
 
 
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
- export async function readBands(info, sample, bands) {
38
- const stats = newStats(), url = imageUrl(info, sample.part), key = b => `${url}#${sample.parquet_row}/${b}`
 
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
- export async function render(info, sample, view) {
 
84
  const needed = [...new Set(view.use.flatMap(b => (b === 'ratio' ? ['vv', 'vh'] : [b])))]
85
- const [bytes, stats] = await readBands(info, sample, needed)
 
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, stats] = await readBands(info, sample, bands)
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())