File size: 3,354 Bytes
42b53b4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// SPDX-License-Identifier: Apache-2.0
// Where the CT volume comes from: the public OME-Zarr scans in the Vesuvius Challenge open data bucket,
// read straight from S3 over HTTPS (the bucket answers GET with Access-Control-Allow-Origin: *).
// The global endpoint is used: the us-east-1 regional host answers 403 for the same keys from here.
export const BUCKET = 'https://vesuvius-challenge-open-data.s3.amazonaws.com';
// scan volumes whose voxel grid the meshes of a scroll were traced on (tifxyz x, y, z = zarr x, y, z at level 0)
export const VOLUMES = {
  PHerc0139: { zarr: `${BUCKET}/PHerc0139/volumes/20250728140407-9.362um-1.2m-113keV-masked.zarr`, voxelUM: 9.362 },
};
export function volumeFor(scroll, manifest) {
  const q = new URLSearchParams(location.search).get('ctzarr');
  if (q && q.startsWith(BUCKET + '/')) return { zarr: q.replace(/\/$/, ''), voxelUM: +(new URLSearchParams(location.search).get('ctum')) || 9.362 };
  if (manifest?.ctZarr?.startsWith?.(BUCKET + '/')) return { zarr: manifest.ctZarr, voxelUM: manifest.ctVoxelUM || 9.362 };
  return VOLUMES[scroll] || null;
}

// level metadata (.zarray, zarr v2)
const META = new Map();
export async function levelMeta(zarr, level) {
  const k = `${zarr}/${level}`;
  if (!META.has(k)) META.set(k, fetch(`${zarr}/${level}/.zarray`).then((r) => { if (!r.ok) throw new Error(`no level ${level} (${r.status})`); return r.json(); }));
  return META.get(k);
}

// Read the box [start, start + size) (z, y, x) of one level into a Uint8Array (C order, x fastest).
// Simple path: uncompressed uint8 chunks fetched in parallel on the main thread; onChunk(box) reports each one.
export async function readRegion(zarr, level, start, size, onChunk, signal) {
  const m = await levelMeta(zarr, level);
  if (m.dtype !== '|u1' || m.compressor) throw new Error(`level ${level}: ${m.dtype} ${m.compressor?.id || 'raw'} needs the streaming reader`);
  const [Z, Y, X] = size, out = new Uint8Array(Z * Y * X), C = m.chunks, sep = m.dimension_separator || '.';
  const jobs = [];
  for (let cz = Math.floor(start[0] / C[0]); cz * C[0] < start[0] + Z; cz++)
    for (let cy = Math.floor(start[1] / C[1]); cy * C[1] < start[1] + Y; cy++)
      for (let cx = Math.floor(start[2] / C[2]); cx * C[2] < start[2] + X; cx++) jobs.push([cz, cy, cx]);
  let i = 0, n = 0;
  const one = async ([cz, cy, cx]) => {
    const r = await fetch(`${zarr}/${level}/${[cz, cy, cx].join(sep)}`, { signal });
    const buf = r.ok ? new Uint8Array(await r.arrayBuffer()) : null;   // a missing chunk is fill_value (0)
    const z0 = Math.max(start[0], cz * C[0]), z1 = Math.min(start[0] + Z, (cz + 1) * C[0]);
    const y0 = Math.max(start[1], cy * C[1]), y1 = Math.min(start[1] + Y, (cy + 1) * C[1]);
    const x0 = Math.max(start[2], cx * C[2]), x1 = Math.min(start[2] + X, (cx + 1) * C[2]);
    if (buf) for (let z = z0; z < z1; z++) for (let y = y0; y < y1; y++) {
      const src = ((z - cz * C[0]) * C[1] + (y - cy * C[1])) * C[2] + (x0 - cx * C[2]);
      out.set(buf.subarray(src, src + (x1 - x0)), ((z - start[0]) * Y + (y - start[1])) * X + (x0 - start[2]));
    }
    n++; onChunk?.({ done: n, total: jobs.length, bytes: buf ? buf.length : 0 });
  };
  await Promise.all(Array.from({ length: 6 }, async () => { while (i < jobs.length) await one(jobs[i++]); }));
  return { data: out, chunks: jobs.length };
}