Skip to content

Commit 7cb694c

Browse files
committed
Support OME "labels" metadata
1 parent ac8c770 commit 7cb694c

File tree

9 files changed

+599
-28
lines changed

9 files changed

+599
-28
lines changed

main.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ async function main() {
2020

2121
// Add event listener to sync viewState as query param.
2222
// Debounce to limit how quickly we are pushing to browser history
23-
viewer.on(
24-
"viewStateChange",
25-
debounce((update: vizarr.ViewState) => {
26-
const url = new URL(window.location.href);
27-
url.searchParams.set("viewState", JSON.stringify(update));
28-
window.history.pushState({}, "", decodeURIComponent(url.href));
29-
}, 200),
30-
);
23+
// viewer.on(
24+
// "viewStateChange",
25+
// debounce((update: vizarr.ViewState) => {
26+
// const url = new URL(window.location.href);
27+
// url.searchParams.set("viewState", JSON.stringify(update));
28+
// window.history.pushState({}, "", decodeURIComponent(url.href));
29+
// }, 200),
30+
// );
3131

3232
// parse image config
3333
// @ts-expect-error - TODO: validate config

src/components/LayerController/Content.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import AcquisitionController from "./AcquisitionController";
66
import AddChannelButton from "./AddChannelButton";
77
import AxisSliders from "./AxisSliders";
88
import ChannelController from "./ChannelController";
9+
import Labels from "./Labels";
910
import OpacitySlider from "./OpacitySlider";
1011

1112
import { useLayerState } from "../../hooks";
@@ -22,6 +23,7 @@ const Details = withStyles({
2223
function Content() {
2324
const [layer] = useLayerState();
2425
const nChannels = layer.layerProps.selections.length;
26+
const nLabels = layer.labels?.layerProps.length ?? 0;
2527
return (
2628
<Details>
2729
<Grid container direction="column">
@@ -51,6 +53,14 @@ function Content() {
5153
<ChannelController channelIndex={i} key={i} />
5254
))}
5355
</Grid>
56+
{nLabels > 0 && (
57+
<Grid>
58+
<Typography variant="caption">labels:</Typography>
59+
{range(nLabels).map((i) => (
60+
<Labels labelIndex={i} key={i} />
61+
))}
62+
</Grid>
63+
)}
5464
</Grid>
5565
</Details>
5666
);
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Divider, Typography } from "@material-ui/core";
2+
import { Slider } from "@material-ui/core";
3+
import { withStyles } from "@material-ui/styles";
4+
import React from "react";
5+
6+
import { useLayerState, useSourceData } from "../../hooks";
7+
import { assert } from "../../utils";
8+
9+
const DenseSlider = withStyles({
10+
root: {
11+
color: "white",
12+
padding: "10px 0px 5px 0px",
13+
marginRight: "5px",
14+
},
15+
active: {
16+
boxshadow: "0px 0px 0px 8px rgba(158, 158, 158, 0.16)",
17+
},
18+
})(Slider);
19+
20+
export default function Labels({ labelIndex }: { labelIndex: number }) {
21+
const [source] = useSourceData();
22+
const [layer, setLayer] = useLayerState();
23+
assert(source.labels && layer.kind === "multiscale" && layer.labels, "Missing image labels");
24+
25+
const handleOpacityChange = (_: unknown, value: number | number[]) => {
26+
setLayer((prev) => {
27+
assert(prev.kind === "multiscale" && prev.labels, "Missing image labels");
28+
return {
29+
...prev,
30+
labels: {
31+
...prev.labels,
32+
layerProps: prev.labels.layerProps.with(labelIndex, {
33+
...prev.labels.layerProps[labelIndex],
34+
opacity: value as number,
35+
}),
36+
},
37+
};
38+
});
39+
};
40+
41+
const { name } = source.labels[labelIndex];
42+
const { opacity } = layer.labels.layerProps[labelIndex];
43+
return (
44+
<>
45+
<Divider />
46+
<Typography variant="caption">{name}</Typography>
47+
<DenseSlider value={opacity} onChange={handleOpacityChange} min={0} max={1} step={0.01} />
48+
</>
49+
);
50+
}

src/io.ts

+61
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,73 @@ export function initLayerStateFromSource(source: SourceData & { id: string }): L
218218
};
219219
}
220220

221+
let labels = undefined;
222+
if (source.labels && source.labels.length > 0) {
223+
labels = {
224+
on: true,
225+
layerProps: source.labels.map((label, i) => ({
226+
id: `${source.id}_${i}`,
227+
loader: label.loader,
228+
modelMatrix: label.modelMatrix,
229+
opacity: 1,
230+
colors: label.colors,
231+
})),
232+
transformSourceSelection: getTransformSourceSelectionFromLabels(
233+
source.labels.map((label) => label.loader[0]),
234+
source.loader[0],
235+
),
236+
};
237+
}
238+
221239
return {
222240
kind: "multiscale",
223241
layerProps: {
224242
...layerProps,
225243
loader: source.loader,
226244
},
227245
on: true,
246+
labels,
247+
};
248+
}
249+
250+
function getTransformSourceSelectionFromLabels(
251+
labelsResolutions: Array<{ shape: Array<number>; labels: Array<string> }>,
252+
source: { shape: Array<number>; labels: Array<string> },
253+
) {
254+
// representative source for labels
255+
const labelsSource = labelsResolutions[0];
256+
utils.assert(
257+
source.shape.length === source.labels.length,
258+
`Image source axes and shape are not same rank. Got ${JSON.stringify(source)}`,
259+
);
260+
utils.assert(
261+
labelsSource.shape.length === labelsSource.labels.length,
262+
`Label axes and shape are not same rank. Got ${JSON.stringify(labelsSource)}`,
263+
);
264+
utils.assert(
265+
labelsSource.labels.every((label) => source.labels.includes(label)),
266+
`Label axes MUST be a subset of source. Source: ${JSON.stringify(source.labels)} Labels: ${JSON.stringify(labelsSource.labels)}`,
267+
);
268+
for (const { labels, shape } of labelsResolutions.slice(1)) {
269+
utils.assert(
270+
utils.zip(labels, labelsSource.labels).every(([a, b]) => a === b),
271+
`Error: All labels must share the same axes. Mismatched labels found: ${JSON.stringify(labels)}`,
272+
);
273+
utils.assert(
274+
utils.zip(shape, labelsSource.shape).every(([a, b]) => a === b),
275+
`Error: All labels must share the same shape. Mismatched labels found: ${JSON.stringify(shape)}`,
276+
);
277+
}
278+
// Identify labels that should always map to 0, regardless of the source selection.
279+
const excludeFromTransformedSelection = new Set(
280+
utils
281+
.zip(labelsSource.labels, labelsSource.shape)
282+
.filter(([_, size]) => size === 1)
283+
.map(([name, _]) => name),
284+
);
285+
return (sourceSelection: Array<number>): Array<number> => {
286+
return labelsSource.labels.map((name) =>
287+
excludeFromTransformedSelection.has(name) ? 0 : sourceSelection[source.labels.indexOf(name)],
288+
);
228289
};
229290
}

0 commit comments

Comments
 (0)