UNPKG

@loaders.gl/zarr

Version:

Framework-independent loaders for Zarr

136 lines (116 loc) 4.06 kB
// 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 {RawArray} from 'zarr/types/rawArray'; import type { PixelSource, Labels, RasterSelection, PixelSourceSelection, PixelData, TileSelection } from '../types'; import {BoundsCheckError, slice} from 'zarr'; import {getImageSize, isInterleaved, getIndexer} from './utils'; export const DTYPE_LOOKUP = { u1: 'Uint8', u2: 'Uint16', u4: 'Uint32', f4: 'Float32', f8: 'Float64', i1: 'Int8', i2: 'Int16', i4: 'Int32' } as const; type ZarrIndexer<S extends string[]> = (sel: {[K in S[number]]: number} | number[]) => number[]; interface ZarrTileSelection { x: number; y: number; selection: number[]; signal?: AbortSignal; } class ZarrPixelSource<S extends string[]> implements PixelSource<S> { public labels: Labels<S>; public tileSize: number; private _data: ZarrArray; private _indexer: ZarrIndexer<S>; private _readChunks: boolean; constructor(data: ZarrArray, labels: Labels<S>, tileSize: number) { this._indexer = getIndexer(labels); this._data = data; const xChunkSize = data.chunks[this._xIndex]; const yChunkSize = data.chunks[this._xIndex - 1]; this._readChunks = tileSize === xChunkSize && tileSize === yChunkSize; this.labels = labels; this.tileSize = tileSize; } get shape() { return this._data.shape; } get dtype() { const suffix = this._data.dtype.slice(1) as keyof typeof DTYPE_LOOKUP; if (!(suffix in DTYPE_LOOKUP)) { throw Error(`Zarr dtype not supported, got ${suffix}.`); } return DTYPE_LOOKUP[suffix]; } private get _xIndex() { const interleave = isInterleaved(this._data.shape); return this._data.shape.length - (interleave ? 2 : 1); } private _chunkIndex<T>(selection: PixelSourceSelection<S> | number[], x: T, y: T) { const sel: (number | T)[] = this._indexer(selection); sel[this._xIndex] = x; sel[this._xIndex - 1] = y; return sel; } /** * Converts x, y tile indices to zarr dimension Slices within image bounds. */ private _getSlices(x: number, y: number) { const {height, width} = getImageSize(this); const [xStart, xStop] = [x * this.tileSize, Math.min((x + 1) * this.tileSize, width)]; const [yStart, yStop] = [y * this.tileSize, Math.min((y + 1) * this.tileSize, height)]; // Deck.gl can sometimes request edge tiles that don't exist. We throw // a BoundsCheckError which is picked up in `ZarrPixelSource.onTileError` // and ignored by deck.gl. if (xStart === xStop || yStart === yStop) { throw new BoundsCheckError('Tile slice is zero-sized.'); } return [slice(xStart, xStop), slice(yStart, yStop)]; } async getRaster({selection}: RasterSelection<S> | {selection: number[]}) { const sel = this._chunkIndex(selection, null, null); const {data, shape} = (await this._data.getRaw(sel)) as RawArray; const [height, width] = shape; return {data, width, height} as PixelData; } async getTile(props: TileSelection<S> | ZarrTileSelection) { const {x, y, selection, signal} = props; let res; if (this._readChunks) { // Can read chunks directly by key since tile size matches chunk shape const sel = this._chunkIndex(selection, x, y); res = await this._data.getRawChunk(sel, {storeOptions: {signal}}); } else { // Need to use zarr fancy indexing to get desired tile size. const [xSlice, ySlice] = this._getSlices(x, y); const sel = this._chunkIndex(selection, xSlice, ySlice); res = await this._data.getRaw(sel); } const { data, shape: [height, width] } = res as RawArray; return {data, width, height} as PixelData; } onTileError(err: Error) { if (!(err instanceof BoundsCheckError)) { // Rethrow error if something other than tile being requested is out of bounds. throw err; } } } export default ZarrPixelSource;