diff --git a/src/gridLayer.ts b/src/gridLayer.ts index 3d1087c4..c8323e74 100644 --- a/src/gridLayer.ts +++ b/src/gridLayer.ts @@ -1,9 +1,8 @@ import { CompositeLayer } from '@deck.gl/core'; import { SolidPolygonLayer, TextLayer } from '@deck.gl/layers'; import type { CompositeLayerProps } from '@deck.gl/core/lib/composite-layer'; -import pMap from 'p-map'; - -import { XRLayer } from '@hms-dbmi/viv'; +import { Matrix4 } from '@math.gl/core/dist/esm'; +import { MultiscaleImageLayer } from '@hms-dbmi/viv'; import type { GridLoader } from './state'; export interface GridLayerProps extends CompositeLayerProps { @@ -21,90 +20,27 @@ export interface GridLayerProps extends CompositeLayerProps { } const defaultProps = { - ...XRLayer.defaultProps, + ...MultiscaleImageLayer.defaultProps, // Special grid props loaders: { type: 'array', value: [], compare: true }, spacer: { type: 'number', value: 5, compare: true }, rows: { type: 'number', value: 0, compare: true }, columns: { type: 'number', value: 0, compare: true }, concurrency: { type: 'number', value: 10, compare: false }, // set concurrency for queue - text: { type: 'boolean', value: false, compare: true }, + text: { type: 'boolean', value: true, compare: true }, // Deck.gl onClick: { type: 'function', value: null, compare: true }, onHover: { type: 'function', value: null, compare: true }, }; -function scaleBounds(width: number, height: number, translate = [0, 0], scale = 1) { - const [left, top] = translate; - const right = width * scale + left; - const bottom = height * scale + top; - return [left, bottom, right, top]; -} - -function validateWidthHeight(d: { data: { width: number; height: number } }[]) { - const [first] = d; - // Return early if no grid data. Maybe throw an error? - const { width, height } = first.data; - // Verify that all grid data is same shape (ignoring undefined) - d.forEach(({ data }) => { - if (data?.width !== width || data?.height !== height) { - throw new Error('Grid data is not same shape.'); - } - }); - return { width, height }; -} - -function refreshGridData(props: { loaders: GridLoader[]; concurrency?: number; loaderSelection: number[][] }) { - const { loaders, loaderSelection = [] } = props; - let { concurrency } = props; - if (concurrency && loaderSelection.length > 0) { - // There are `loaderSelection.length` requests per loader. This block scales - // the provided concurrency to map to the number of actual requests. - concurrency = Math.ceil(concurrency / loaderSelection.length); - } - const mapper = async (d: GridLoader) => { - const promises = loaderSelection.map((selection) => d.loader.getRaster({ selection })); - const tiles = await Promise.all(promises); - return { - ...d, - data: { - data: tiles.map((d) => d.data), - width: tiles[0].width, - height: tiles[0].height, - }, - }; - }; - return pMap(loaders, mapper, { concurrency }); -} - export default class GridLayer

extends CompositeLayer { - initializeState() { - this.state = { gridData: [], width: 0, height: 0 }; - refreshGridData(this.props).then((gridData) => { - const { width, height } = validateWidthHeight(gridData); - this.setState({ gridData, width, height }); - }); - } - - updateState({ props, oldProps, changeFlags }: { props: GridLayerProps; oldProps: GridLayerProps; changeFlags: any }) { - const { propsChanged } = changeFlags; - const loaderChanged = typeof propsChanged === 'string' && propsChanged.includes('props.loaders'); - const loaderSelectionChanged = props.loaderSelection !== oldProps.loaderSelection; - if (loaderChanged || loaderSelectionChanged) { - // Only fetch new data to render if loader has changed - refreshGridData(this.props).then((gridData) => { - this.setState({ gridData }); - }); - } - } - getPickingInfo({ info }: { info: any }) { // provide Grid row and column info for mouse events (hover & click) if (!info.coordinate) { return info; } const spacer = this.props.spacer || 0; - const { width, height } = this.state; + const [height, width] = this.props.loaders[0].loader[0].shape.slice(-2); const [x, y] = info.coordinate; const row = Math.floor(y / (height + spacer)); const column = Math.floor(x / (width + spacer)); @@ -113,21 +49,19 @@ export default class GridLayer

extend } renderLayers() { - const { gridData, width, height } = this.state; - if (width === 0 || height === 0) return null; // early return if no data - + if (this.props.loaders.length === 0) return null; + const [height, width] = this.props.loaders[0].loader[0].shape.slice(-2); const { rows, columns, spacer = 0, id = '' } = this.props; - const layers = gridData.map((d: any) => { + const layers = this.props.loaders.map((d: any) => { const y = d.row * (height + spacer); const x = d.col * (width + spacer); const layerProps = { - channelData: d.data, // coerce to null if no data - bounds: scaleBounds(width, height, [x, y]), + loader: d.loader, + modelMatrix: new Matrix4().translate([x, y, 0]), id: `${id}-GridLayer-${d.row}-${d.col}`, - dtype: d.loader.dtype || 'Uint16', // fallback if missing, pickable: false, }; - return new (XRLayer as any)({ ...this.props, ...layerProps }); + return new (MultiscaleImageLayer as any)({ ...this.props, ...layerProps }); }); if (this.props.pickable) { @@ -155,7 +89,7 @@ export default class GridLayer

extend if (this.props.text) { const textLayer = new TextLayer({ id: `${id}-GridLayer-text`, - data: gridData, + data: this.props.loaders, getPosition: (d: any) => [d.col * (width + spacer), d.row * (height + spacer)], getText: (d: any) => d.name, getColor: [255, 255, 255, 255], diff --git a/src/ome.ts b/src/ome.ts index 4d3c9846..93ded80b 100644 --- a/src/ome.ts +++ b/src/ome.ts @@ -1,8 +1,7 @@ import { ZarrPixelSource } from '@hms-dbmi/viv'; -import pMap from 'p-map'; -import { Group as ZarrGroup, HTTPStore, openGroup, ZarrArray } from 'zarr'; +import { Group as ZarrGroup, HTTPStore, openGroup, ZarrArray, openArray } from 'zarr'; import type { ImageLayerConfig, SourceData } from './state'; -import { join, loadMultiscales, guessTileSize, range, parseMatrix } from './utils'; +import { join, loadMultiscales, guessTileSize, parseMatrix } from './utils'; export async function loadWell(config: ImageLayerConfig, grp: ZarrGroup, wellAttrs: Ome.Well): Promise { // Can filter Well fields by URL query ?acquisition=ID @@ -42,28 +41,31 @@ export async function loadWell(config: ImageLayerConfig, grp: ZarrGroup, wellAtt const rows = Math.ceil(imgPaths.length / cols); // Use first image for rendering settings, resolutions etc. - const imgAttrs = (await grp.getItem(imgPaths[0]).then((g) => g.attrs.asObject())) as Ome.Attrs; + const img = await grp.getItem(imgPaths[0]); + const imgAttrs = (await img.attrs.asObject()) as Ome.Attrs; if (!('omero' in imgAttrs)) { throw Error('Path for image is not valid.'); } - let resolution = imgAttrs.multiscales[0].datasets[0].path; + const { datasets } = imgAttrs.multiscales[0]; + const resolutions = datasets.map((d) => d.path); // Create loader for every Image. - const promises = imgPaths.map((p) => grp.getItem(join(p, resolution))); + const pyramid = resolutions.map((p) => grp.getItem(join(imgPaths[0], p))); const meta = parseOmeroMeta(imgAttrs.omero); - const data = (await Promise.all(promises)) as ZarrArray[]; + const data = (await Promise.all(pyramid)) as ZarrArray[]; const tileSize = guessTileSize(data[0]); - const loaders = range(rows).flatMap((row) => { - return range(cols).map((col) => { - const offset = col + row * cols; - return { name: String(offset), row, col, loader: new ZarrPixelSource(data[offset], meta.axis_labels, tileSize) }; + const loaders = imgPaths.map((p, i) => { + const loader = resolutions.map((res, level) => { + const arr: ZarrArray = new (ZarrArray as any)(grp.store, join(grp.path, p, res), data[level].meta); + return new ZarrPixelSource(arr, meta.axis_labels, tileSize); }); + return { name: String(i), row: Math.floor(i / cols), col: i % cols, loader }; }); const sourceData: SourceData = { loaders, ...meta, - loader: [loaders[0].loader], + loader: loaders[0].loader, model_matrix: parseMatrix(config.model_matrix), defaults: { selection: meta.defaultSelection, @@ -128,25 +130,26 @@ export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateA } // Lowest resolution is the 'path' of the last 'dataset' from the first multiscales const { datasets } = imgAttrs.multiscales[0]; - const resolution = datasets[datasets.length - 1].path; + const resolutions = datasets.map((d) => d.path); // Create loader for every Well. Some loaders may be undefined if Wells are missing. - const mapper = ([key, path]: string[]) => grp.getItem(path).then((arr) => [key, arr]) as Promise<[string, ZarrArray]>; - const promises = await pMap( - wellPaths.map((p) => [p, join(p, imgPath, resolution)]), - mapper, - { concurrency: 10 } + const promises = resolutions.map((res) => + openArray({ store: grp.store, path: join(grp.path, wellPaths[0], imgPath, res) }) ); - const meta = parseOmeroMeta(imgAttrs.omero); const data = await Promise.all(promises); - const tileSize = guessTileSize(data[0][1]); - const loaders = data.map((d) => { - const [row, col] = d[0].split('/'); + const meta = parseOmeroMeta(imgAttrs.omero); + const tileSize = guessTileSize(data[0]); + const loaders = wellPaths.map((d) => { + const [row, col] = d.split('/'); + const loader = resolutions.map((res, i) => { + const arr = new (ZarrArray as any)(grp.store, join(grp.path, d, imgPath, res), data[i].meta); + return new ZarrPixelSource(arr, meta.axis_labels, tileSize); + }); return { name: `${row}${col}`, row: rows.indexOf(row), col: columns.indexOf(col), - loader: new ZarrPixelSource(d[1], meta.axis_labels, tileSize), + loader: loader, }; }); @@ -154,7 +157,7 @@ export async function loadPlate(config: ImageLayerConfig, grp: ZarrGroup, plateA const sourceData: SourceData = { loaders, ...meta, - loader: [loaders[0].loader], + loader: loaders[0].loader, model_matrix: parseMatrix(config.model_matrix), defaults: { selection: meta.defaultSelection, diff --git a/src/state.ts b/src/state.ts index 2036573d..256fad9a 100644 --- a/src/state.ts +++ b/src/state.ts @@ -46,7 +46,7 @@ export interface SingleChannelConfig extends BaseConfig { export type ImageLayerConfig = MultichannelConfig | SingleChannelConfig; export interface GridLoader { - loader: ZarrPixelSource; + loader: ZarrPixelSource[]; row: number; col: number; name: string;