@loaders.gl/zarr
Version:
Framework-independent loaders for Zarr
132 lines (131 loc) • 4.43 kB
JavaScript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import { openGroup, HTTPStore } from 'zarr';
export function normalizeStore(source) {
if (typeof source === 'string') {
return new HTTPStore(source);
}
return source;
}
export async function loadMultiscales(store, path = '') {
const grp = await openGroup(store, path);
const rootAttrs = (await grp.attrs.asObject());
// Root of Zarr store must implement multiscales extension.
// https://github.com/zarr-developers/zarr-specs/issues/50
if (!Array.isArray(rootAttrs.multiscales)) {
throw new Error('Cannot find Zarr multiscales metadata.');
}
const { datasets } = rootAttrs.multiscales[0];
const promises = datasets.map((d) => grp.getItem(d.path));
return {
data: await Promise.all(promises),
rootAttrs
};
}
/*
* Creates an ES6 map of 'label' -> index
* > const labels = ['a', 'b', 'c', 'd'];
* > const dims = getDims(labels);
* > dims('a') === 0;
* > dims('b') === 1;
* > dims('c') === 2;
* > dims('hi!'); // throws
*/
export function getDims(labels) {
const lookup = new Map(labels.map((name, i) => [name, i]));
if (lookup.size !== labels.length) {
throw Error('Labels must be unique, found duplicated label.');
}
return (name) => {
const index = lookup.get(name);
if (index === undefined) {
throw Error('Invalid dimension.');
}
return index;
};
}
function prevPowerOf2(x) {
return 2 ** Math.floor(Math.log2(x));
}
/*
* Helper method to determine whether pixel data is interleaved or not.
* > isInterleaved([1, 24, 24]) === false;
* > isInterleaved([1, 24, 24, 3]) === true;
*/
export function isInterleaved(shape) {
const lastDimSize = shape[shape.length - 1];
return lastDimSize === 3 || lastDimSize === 4;
}
export function guessTileSize(arr) {
const interleaved = isInterleaved(arr.shape);
const [yChunk, xChunk] = arr.chunks.slice(interleaved ? -3 : -2);
const size = Math.min(yChunk, xChunk);
// deck.gl requirement for power-of-two tile size.
return prevPowerOf2(size);
}
export function guessLabels(rootAttrs) {
if ('omero' in rootAttrs) {
return ['t', 'c', 'z', 'y', 'x'];
}
throw new Error('Could not infer dimension labels for Zarr source. Must provide dimension labels.');
}
/*
* The 'indexer' for a Zarr-based source translates
* a 'selection' to an array of indices that align to
* the labeled dimensions.
*
* > const labels = ['a', 'b', 'y', 'x'];
* > const indexer = getIndexer(labels);
* > console.log(indexer({ a: 10, b: 20 }));
* > // [10, 20, 0, 0]
*/
export function getIndexer(labels) {
const size = labels.length;
const dims = getDims(labels);
return (sel) => {
if (Array.isArray(sel)) {
return [...sel];
}
const selection = Array(size).fill(0);
for (const [key, value] of Object.entries(sel)) {
selection[dims(key)] = value;
}
return selection;
};
}
export function getImageSize(source) {
const interleaved = isInterleaved(source.shape);
// 2D image data in Zarr are represented as (..., rows, columns [, bands])
// If an image is interleaved (RGB/A), we need to ignore the last dimension (bands)
// to get the height and weight of the image.
const [height, width] = source.shape.slice(interleaved ? -3 : -2);
return { height, width };
}
/**
* Preserves (double) slashes earlier in the path, so this works better
* for URLs. From https://stackoverflow.com/a/46427607
* @param args parts of a path or URL to join.
*/
export function joinUrlParts(...args) {
return args
.map((part, i) => {
if (i === 0)
return part.trim().replace(/[/]*$/g, '');
return part.trim().replace(/(^[/]*|[/]*$)/g, '');
})
.filter((x) => x.length)
.join('/');
}
export function validLabels(labels, shape) {
if (labels.length !== shape.length) {
throw new Error('Labels do not match Zarr array shape.');
}
const n = shape.length;
if (isInterleaved(shape)) {
// last three dimensions are [row, column, bands]
return labels[n - 3] === 'y' && labels[n - 2] === 'x' && labels[n - 1] === '_c';
}
// last two dimensions are [row, column]
return labels[n - 2] === 'y' && labels[n - 1] === 'x';
}