diff --git a/src/ome.ts b/src/ome.ts index 4d3c9846..a0288cf0 100644 --- a/src/ome.ts +++ b/src/ome.ts @@ -2,9 +2,28 @@ import { ZarrPixelSource } from '@hms-dbmi/viv'; import pMap from 'p-map'; import { Group as ZarrGroup, HTTPStore, openGroup, ZarrArray } from 'zarr'; import type { ImageLayerConfig, SourceData } from './state'; -import { join, loadMultiscales, guessTileSize, range, parseMatrix } from './utils'; +import { + guessTileSize, + join, + loadMultiscales, + nested, + parseMatrix, + range +} from './utils'; -export async function loadWell(config: ImageLayerConfig, grp: ZarrGroup, wellAttrs: Ome.Well): Promise { +// OME-Zarr uses nested chunks since version 0.2 +function isNested(version: String | undefined) : boolean { + return version != undefined && version !== "0.1"; +} + +export async function loadWell( + config: ImageLayerConfig, + grp: ZarrGroup, + wellAttrs: Ome.Well +): Promise { + if (isNested(wellAttrs.version)) { + grp.store = nested(grp.store); + } // Can filter Well fields by URL query ?acquisition=ID const acquisitionId: number | undefined = config.acquisition ? parseInt(config.acquisition) : undefined; let acquisitions: Ome.Acquisition[] = []; @@ -104,7 +123,14 @@ export async function loadWell(config: ImageLayerConfig, grp: ZarrGroup, wellAtt return sourceData; } -export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateAttrs: Ome.Plate): Promise { +export async function loadPlate( + config: ImageLayerConfig, + grp: ZarrGroup, + plateAttrs: Ome.Plate +): Promise { + if (isNested(plateAttrs.version)) { + grp.store = nested(grp.store); + } if (!('columns' in plateAttrs) || !('rows' in plateAttrs)) { throw Error(`Plate .zattrs missing columns or rows`); } @@ -193,6 +219,9 @@ export async function loadOmeroMultiscales( grp: ZarrGroup, attrs: { multiscales: Ome.Multiscale[]; omero: Ome.Omero } ): Promise { + if (isNested(attrs.multiscales[0]?.version)) { + grp.store = nested(grp.store); + } const { name, opacity = 1, colormap = '' } = config; const data = await loadMultiscales(grp, attrs.multiscales); const meta = parseOmeroMeta(attrs.omero); diff --git a/src/utils.ts b/src/utils.ts index f7ab038f..773bb768 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -25,37 +25,17 @@ async function normalizeStore(source: string | ZarrArray['store']) { const store = await FileReferenceStore.fromUrl(source); return { store }; } - const [root, path] = source.split('.zarr'); - return { store: new HTTPStore(root + '.zarr'), path }; + // const [root, path] = source.split('.zarr'); + return { store: new HTTPStore(source), path:'' }; } return { store: source }; } export async function open(source: string | ZarrArray['store']) { const { store, path } = await normalizeStore(source); - - function nested(store) { - const get = (target, key) => { - if (key === 'getItem' || key === 'setItem' || key === 'containsItem') { - return (path, ...args) => { - if (path.endsWith('.zarray') || path.endsWith('.zattrs') || path.endsWith('.zgroup')) { - return target[key](path, ...args); - } - const prefix = path.split('/'); - const chunkKey = prefix.pop(); - const newPath = [...prefix, chunkKey.replaceAll('.', '/')].join('/'); - return target[key](newPath, ...args); - } - } - return Reflect.get(target, key); - }; - return new Proxy(store, { get }); - } - const nested_store = nested(store); - - return openGroup(nested_store, path).catch((err) => { + return openGroup(store, path).catch((err) => { if (err instanceof ContainsArrayError) { - return openArray({ nested_store, path }); + return openArray({ store, path }); } throw err; }); @@ -70,6 +50,33 @@ export async function loadMultiscales(grp: ZarrGroup, multiscales: Ome.Multiscal throw Error('Multiscales metadata included a path to a group.'); } +export function nested(store: ZarrArray['store']) { + const get = (target: ZarrArray['store'], key: string | number | symbol) => { + if (key === 'getItem' || key === 'containsItem' || key === 'setItem') { + return (path: string, ...args: any[]) => { + if (path.endsWith('.zarray') || path.endsWith('.zattrs') || path.endsWith('.zgroup')) { + if (key === 'setItem') { + // TypeScript: setItem() needs 'value' + return target[key](path, args[0]); + } else { + return target[key](path, ...args); + } + } + const prefix = path.split('/'); + const chunkKey = prefix.pop() as string; + const newPath = [...prefix, chunkKey.replaceAll('.', '/')].join('/'); + if (key === 'setItem') { + return target[key](newPath, args[0]); + } else { + return target[key](newPath, ...args); + } + } + } + return Reflect.get(target, key); + }; + return new Proxy(store, { get }); +} + export function hexToRGB(hex: string): number[] { if (hex.startsWith('#')) hex = hex.slice(1); const r = parseInt(hex.slice(0, 2), 16);