Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/LayerController/AddChannelButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useState } from "react";
import type { ChangeEvent, MouseEvent } from "react";

import { useLayerState, useSourceData } from "../../hooks";
import { MAX_CHANNELS, calcDataRange, hexToRGB } from "../../utils";
import { MAX_CHANNELS, calcDataRange, hexToRGB, resolveLoaderFromLayerProps } from "../../utils";

function AddChannelButton() {
const [source, setSource] = useSourceData();
Expand Down Expand Up @@ -32,7 +32,7 @@ function AddChannelButton() {
if (source.contrast_limits[channelIndex]) {
lim = source.contrast_limits[channelIndex] as [number, number];
} else {
const { loader } = layer.layerProps;
const loader = resolveLoaderFromLayerProps(layer.layerProps);
const lowres = Array.isArray(loader) ? loader[loader.length - 1] : loader;
lim = await calcDataRange(lowres, channelSelection);
// Update source data with newly calculated limit
Expand Down
67 changes: 29 additions & 38 deletions src/components/Viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,31 @@ import DeckGL from "deck.gl";
import { OrthographicView } from "deck.gl";
import { useAtomValue } from "jotai";
import * as React from "react";

import type { DeckGLRef, Layer, LayerProps, OrthographicViewState } from "deck.gl";
import type { ZarrPixelSource } from "../ZarrPixelSource";
import { useViewState } from "../hooks";
import { layerAtoms } from "../state";
import { fitBounds, isInterleaved } from "../utils";

type Data = { loader: ZarrPixelSource; rows: number; columns: number };
type VizarrLayer = Layer<LayerProps & Data>;
import { fitImageToViewport, isGridLayerProps, isInterleaved, resolveLoaderFromLayerProps } from "../utils";

function getLayerSize(props: Data) {
const { loader } = props;
const [base, maxZoom] = Array.isArray(loader) ? [loader[0], loader.length] : [loader, 0];
const interleaved = isInterleaved(base.shape);
let [height, width] = base.shape.slice(interleaved ? -3 : -2);
if ("loaders" in props && props.rows && props.columns) {
// TODO: Don't hardcode spacer size. Probably best to inspect the deck.gl Layers rather than
// the Layer Props.
const spacer = 5;
height = (height + spacer) * props.rows;
width = (width + spacer) * props.columns;
}
return { height, width, maxZoom };
}
import type { DeckGLRef, OrthographicViewState } from "deck.gl";
import type { VizarrLayer } from "../state";

function WrappedViewStateDeck(props: { layers: Array<VizarrLayer | null> }) {
export default function Viewer() {
const deckRef = React.useRef<DeckGLRef>(null);
const [viewState, setViewState] = useViewState();
const firstLayerProps = props.layers[0]?.props;
const layers = useAtomValue(layerAtoms);
const firstLayer = layers[0];

// If viewState hasn't been updated, use the first loader to guess viewState
// TODO: There is probably a better place / way to set the intital view and this is a hack.
if (deckRef.current?.deck && !viewState && firstLayerProps?.loader) {
if (deckRef.current?.deck && !viewState && firstLayer) {
const { deck } = deckRef.current;
const { width, height, maxZoom } = getLayerSize(firstLayerProps);
const padding = deck.width < 400 ? 10 : deck.width < 600 ? 30 : 50; // Adjust depending on viewport width.
const bounds = fitBounds([width, height], [deck.width, deck.height], maxZoom, padding);
setViewState(bounds);
setViewState(
fitImageToViewport({
image: getLayerSize(firstLayer),
viewport: deck,
padding: deck.width < 400 ? 10 : deck.width < 600 ? 30 : 50, // Adjust depending on viewport width.
matrix: firstLayer.props.modelMatrix,
}),
);
}

// Enables screenshots of the canvas: https://github.com/visgl/deck.gl/issues/2200
Expand All @@ -50,7 +37,7 @@ function WrappedViewStateDeck(props: { layers: Array<VizarrLayer | null> }) {
return (
<DeckGL
ref={deckRef}
layers={props.layers}
layers={layers}
viewState={viewState && { ortho: viewState }}
onViewStateChange={(e: { viewState: OrthographicViewState }) =>
// @ts-expect-error - deck doesn't know this should be ok
Expand All @@ -62,13 +49,17 @@ function WrappedViewStateDeck(props: { layers: Array<VizarrLayer | null> }) {
);
}

function Viewer() {
const layerConstructors = useAtomValue(layerAtoms);
// @ts-expect-error - Viv types are giving up an issue
const layers: Array<VizarrLayer | null> = layerConstructors.map((layer) => {
return !layer.on ? null : new layer.Layer(layer.layerProps);
});
return <WrappedViewStateDeck layers={layers} />;
function getLayerSize({ props }: VizarrLayer) {
const loader = resolveLoaderFromLayerProps(props);
const [baseResolution, maxZoom] = Array.isArray(loader) ? [loader[0], loader.length] : [loader, 0];
const interleaved = isInterleaved(baseResolution.shape);
let [height, width] = baseResolution.shape.slice(interleaved ? -3 : -2);
if (isGridLayerProps(props)) {
// TODO: Don't hardcode spacer size. Probably best to inspect the deck.gl Layers rather than
// the Layer Props.
const spacer = 5;
height = (height + spacer) * props.rows;
width = (width + spacer) * props.columns;
}
return { height, width, maxZoom };
}

export default Viewer;
24 changes: 11 additions & 13 deletions src/io.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ImageLayer, MultiscaleImageLayer } from "@hms-dbmi/viv";
import * as zarr from "zarrita";

import { ZarrPixelSource } from "./ZarrPixelSource";
import GridLayer from "./gridLayer";
import { loadOmeroMultiscales, loadPlate, loadWell } from "./ome";
import type { ImageLayerConfig, LayerState, MultichannelConfig, SingleChannelConfig, SourceData } from "./state";
import { loadOmeMultiscales, loadPlate, loadWell } from "./ome";
import * as utils from "./utils";

import type { BaseLayerProps } from "./layers/viv-layers";
import type { ImageLayerConfig, LayerState, MultichannelConfig, SingleChannelConfig, SourceData } from "./state";

async function loadSingleChannel(config: SingleChannelConfig, data: Array<ZarrPixelSource>): Promise<SourceData> {
const { color, contrast_limits, visibility, name, colormap = "", opacity = 1 } = config;
const lowres = data[data.length - 1];
Expand Down Expand Up @@ -89,8 +88,8 @@ export async function createSourceData(config: ImageLayerConfig): Promise<Source
return loadWell(config, node, attrs.well);
}

if (utils.isOmeroMultiscales(attrs)) {
return loadOmeroMultiscales(config, node, attrs);
if (utils.isOmeMultiscales(attrs)) {
return loadOmeMultiscales(config, node, attrs);
}

if (Object.keys(attrs).length === 0 && node.path) {
Expand Down Expand Up @@ -193,14 +192,13 @@ export function initLayerStateFromSource(source: SourceData & { id: string }): L
colormap,
modelMatrix: source.model_matrix,
onClick: source.onClick,
};
} satisfies BaseLayerProps;

if ("loaders" in source) {
if (source.loaders) {
return {
Layer: GridLayer,
kind: "grid",
layerProps: {
...layerProps,
loader: source.loader,
loaders: source.loaders,
columns: source.columns as number,
rows: source.rows as number,
Expand All @@ -211,7 +209,7 @@ export function initLayerStateFromSource(source: SourceData & { id: string }): L

if (source.loader.length === 1) {
return {
Layer: ImageLayer,
kind: "image",
layerProps: {
...layerProps,
loader: source.loader[0],
Expand All @@ -221,7 +219,7 @@ export function initLayerStateFromSource(source: SourceData & { id: string }): L
}

return {
Layer: MultiscaleImageLayer,
kind: "multiscale",
layerProps: {
...layerProps,
loader: source.loader,
Expand Down
42 changes: 21 additions & 21 deletions src/gridLayer.ts → src/layers/grid-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import pMap from "p-map";
import { ColorPaletteExtension, XRLayer } from "@hms-dbmi/viv";
import type { SupportedTypedArray } from "@vivjs/types";
import type { CompositeLayerProps, PickingInfo, SolidPolygonLayerProps, TextLayerProps } from "deck.gl";
import type { ZarrPixelSource } from "./ZarrPixelSource";
import type { BaseLayerProps } from "./state";
import { assert } from "./utils";
import type { ZarrPixelSource } from "../ZarrPixelSource";
import { assert } from "../utils";
import type { BaseLayerProps } from "./viv-layers";

export interface GridLoader {
loader: ZarrPixelSource;
Expand All @@ -28,21 +28,6 @@ export interface GridLayerProps
concurrency?: number;
}

const defaultProps = {
// @ts-expect-error - XRLayer props are not typed
...XRLayer.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 },
// 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;
Expand Down Expand Up @@ -91,7 +76,23 @@ type SharedLayerState = {
height: number;
};

export default class GridLayer extends CompositeLayer<CompositeLayerProps & GridLayerProps> {
class GridLayer extends CompositeLayer<CompositeLayerProps & GridLayerProps> {
static layerName = "VizarrGridLayer";
static defaultProps = {
// @ts-expect-error - XRLayer props are not typed
...XRLayer.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 },
// Deck.gl
onClick: { type: "function", value: null, compare: true },
onHover: { type: "function", value: null, compare: true },
};

get #state(): SharedLayerState {
// @ts-expect-error - typed as any by deck
return this.state;
Expand Down Expand Up @@ -210,5 +211,4 @@ export default class GridLayer extends CompositeLayer<CompositeLayerProps & Grid
}
}

GridLayer.layerName = "GridLayer";
GridLayer.defaultProps = defaultProps;
export { GridLayer };
54 changes: 54 additions & 0 deletions src/layers/viv-layers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Type-complete interfaces to Viv's core layers
*
* @module
*/
import { ImageLayer as BaseImageLayer, MultiscaleImageLayer as BaseMultiscaleImageLayer } from "@hms-dbmi/viv";

import type { Layer } from "deck.gl";
import type { Matrix4 } from "math.gl";
import type { ZarrPixelSource } from "../ZarrPixelSource";

export interface BaseLayerProps {
id: string;
contrastLimits: Array<[min: number, max: number]>;
colors: [r: number, g: number, b: number][];
channelsVisible: Array<boolean>;
opacity: number;
colormap: string; // TODO: more precise
selections: number[][];
modelMatrix: Matrix4;
contrastLimitsRange: [min: number, max: number][];
onClick?: (e: Record<string, unknown>) => void;
}

export interface MultiscaleImageLayerProps extends BaseLayerProps {
loader: Array<ZarrPixelSource>;
}

// @ts-expect-error - Viv does faithfully implement the Layer interface, just not captured in the types
export class MultiscaleImageLayer
extends BaseMultiscaleImageLayer<string[]>
implements Layer<MultiscaleImageLayerProps>
{
static layerName = "VizarMultiscaleImageLayer";
// biome-ignore lint/complexity/noUselessConstructor: Necessary for TypeScript to get the types
constructor(props: MultiscaleImageLayerProps) {
// @ts-expect-error - Viv's types are not correct
super(props);
}
}

export interface ImageLayerProps extends BaseLayerProps {
loader: ZarrPixelSource;
}

// @ts-expect-error - Viv does faithfully implement the Layer interface, just not captured in the types
export class ImageLayer extends BaseImageLayer<string[]> implements Layer<ImageLayerProps> {
static layerName = "VizarrImageLayer";
// biome-ignore lint/complexity/noUselessConstructor: Necessary for TypeScript to get the types
constructor(props: ImageLayerProps) {
// @ts-expect-error - Viv's types are not correct
super(props);
}
}
8 changes: 5 additions & 3 deletions src/ome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export async function loadWell(
});

let meta: Meta;
if (utils.isOmeroMultiscales(imgAttrs)) {
if (utils.isOmeMultiscales(imgAttrs)) {
meta = parseOmeroMeta(imgAttrs.omero, axes);
} else {
meta = await defaultMeta(loaders[0].loader, axis_labels);
Expand Down Expand Up @@ -242,7 +242,7 @@ export async function loadPlate(
return sourceData;
}

export async function loadOmeroMultiscales(
export async function loadOmeMultiscales(
config: ImageLayerConfig,
grp: zarr.Group<zarr.Readable>,
attrs: { multiscales: Ome.Multiscale[]; omero: Ome.Omero },
Expand All @@ -258,7 +258,9 @@ export async function loadOmeroMultiscales(
return {
loader: loader,
axis_labels,
model_matrix: utils.parseMatrix(config.model_matrix),
model_matrix: config.model_matrix
? utils.parseMatrix(config.model_matrix)
: utils.coordinateTransformationsToMatrix(attrs.multiscales),
defaults: {
selection: meta.defaultSelection,
colormap,
Expand Down
Loading