@loaders.gl/zarr
Version:
Framework-independent loaders for Zarr
155 lines (138 loc) • 4.8 kB
text/typescript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import type {ZarrArray} from 'zarr';
// eslint-disable-next-line import/no-unresolved
import type {Store} from 'zarr/types/storage/types';
import type {PixelSource, RootAttrs, Labels} from '../types';
import {openGroup, HTTPStore} from 'zarr';
export function normalizeStore(source: string | Store): Store {
if (typeof source === 'string') {
return new HTTPStore(source);
}
return source;
}
export async function loadMultiscales(store: Store, path = '') {
const grp = await openGroup(store, path);
const rootAttrs = (await grp.attrs.asObject()) as RootAttrs;
// 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)) as Promise<ZarrArray>[];
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<S extends string>(labels: S[]) {
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: S) => {
const index = lookup.get(name);
if (index === undefined) {
throw Error('Invalid dimension.');
}
return index;
};
}
function prevPowerOf2(x: number) {
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: number[]) {
const lastDimSize = shape[shape.length - 1];
return lastDimSize === 3 || lastDimSize === 4;
}
export function guessTileSize(arr: ZarrArray) {
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: RootAttrs) {
if ('omero' in rootAttrs) {
return ['t', 'c', 'z', 'y', 'x'] as Labels<['t', 'c', 'z']>;
}
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<T extends string>(labels: T[]) {
const size = labels.length;
const dims = getDims(labels);
return (sel: {[K in T]: number} | number[]) => {
if (Array.isArray(sel)) {
return [...sel];
}
const selection: number[] = Array(size).fill(0);
for (const [key, value] of Object.entries(sel)) {
selection[dims(key as T)] = value as number;
}
return selection;
};
}
export function getImageSize<T extends string[]>(
source: PixelSource<T>
): {height: number; width: number} {
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: string[]) {
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: string[], shape: number[]): labels is Labels<string[]> {
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';
}