@biongff/vizarr 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/biongff-vizarr.cjs.js +1 -0
- package/dist/biongff-vizarr.es.js +7 -0
- package/dist/blosc-CeItQ6qj.cjs +17 -0
- package/dist/blosc-DaK8KnI4.js +719 -0
- package/dist/chunk-INHXZS53-DiyuLb3Z.js +14 -0
- package/dist/chunk-INHXZS53-z3BpFH8p.cjs +1 -0
- package/dist/deflate-BeJu5ryY.js +10 -0
- package/dist/deflate-Ctyqttht.cjs +1 -0
- package/dist/index-BR2zkK5Y.cjs +3811 -0
- package/dist/index-CU_d8468.js +49053 -0
- package/dist/index.d.ts +67 -0
- package/dist/jpeg-BWDIgcDK.js +533 -0
- package/dist/jpeg-Mp_UXZAJ.cjs +1 -0
- package/dist/jpeg2k-1tnzEScI.js +24 -0
- package/dist/jpeg2k-CcF6vCxI.cjs +1 -0
- package/dist/lerc-BJaDi7aO.js +1039 -0
- package/dist/lerc-D-6RihsZ.cjs +1 -0
- package/dist/lz4-CssV0LoA.js +643 -0
- package/dist/lz4-PFaIsPAh.cjs +15 -0
- package/dist/lzw-DBp1lk8U.js +84 -0
- package/dist/lzw-De76dcEC.cjs +1 -0
- package/dist/packbits-B6vt-6GN.cjs +1 -0
- package/dist/packbits-MT4dFhEA.js +24 -0
- package/dist/pako.esm-Bx5X36Wo.js +1074 -0
- package/dist/pako.esm-DZC2QrbJ.cjs +1 -0
- package/dist/raw-BF-NUDI1.js +9 -0
- package/dist/raw-vwCA5qWn.cjs +1 -0
- package/dist/ref-DAXKePD4.cjs +1 -0
- package/dist/ref-HqVJFnCG.js +196 -0
- package/dist/webimage-BQlxeHEQ.cjs +1 -0
- package/dist/webimage-DTyyK3Ev.js +19 -0
- package/dist/zstd-Cqadn9HA.js +610 -0
- package/dist/zstd-_xUhkGOV.cjs +15 -0
- package/package.json +25 -20
- package/.gitattributes +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -32
- package/.github/ISSUE_TEMPLATE/documentation_update.md +0 -16
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/ISSUE_TEMPLATE/maintenance.md +0 -19
- package/.github/ISSUE_TEMPLATE/optimisation_request.md +0 -23
- package/.github/ISSUE_TEMPLATE/support_request.md +0 -25
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -21
- package/.github/dependabot.yml +0 -13
- package/.github/workflows/deploy.yml +0 -56
- package/.github/workflows/npm-publish-dev.yml +0 -49
- package/.github/workflows/npm-publish.yml +0 -42
- package/.github/workflows/release.yml +0 -37
- package/.github/workflows/test.yml +0 -21
- package/.releaserc.json +0 -29
- package/CHANGELOG.md +0 -56
- package/CITATION.cff +0 -56
- package/CONTRIBUTING.md +0 -82
- package/README.md +0 -85
- package/assets/favicon.ico +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-wide.png +0 -0
- package/assets/logo-wide.svg +0 -60
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -36
- package/assets/robots.txt +0 -3
- package/assets/screenshot.png +0 -0
- package/biome.json +0 -34
- package/index.html +0 -24
- package/main.ts +0 -52
- package/patches/@vivjs__loaders@0.17.2.patch +0 -13
- package/python/.python-version +0 -1
- package/python/README.md +0 -14
- package/python/Untitled.ipynb +0 -40
- package/python/deno.json +0 -22
- package/python/notebooks/IDR_example.ipynb +0 -86
- package/python/notebooks/README.md +0 -35
- package/python/notebooks/create_fixture.py +0 -72
- package/python/notebooks/getting_started.ipynb +0 -217
- package/python/notebooks/mandelbrot.ipynb +0 -211
- package/python/notebooks/spatial_transformations.ipynb +0 -139
- package/python/pyproject.toml +0 -18
- package/python/src/vizarr/__init__.py +0 -10
- package/python/src/vizarr/_widget.js +0 -113
- package/python/src/vizarr/_widget.py +0 -67
- package/python/uv.lock +0 -2495
- package/src/ZarrPixelSource.ts +0 -182
- package/src/codecs/jpeg2k.ts +0 -22
- package/src/codecs/register.ts +0 -2
- package/src/components/LayerController/AcquisitionController.tsx +0 -46
- package/src/components/LayerController/AddChannelButton.tsx +0 -122
- package/src/components/LayerController/AxisOptions.tsx +0 -93
- package/src/components/LayerController/AxisSlider.tsx +0 -86
- package/src/components/LayerController/AxisSliders.tsx +0 -29
- package/src/components/LayerController/ChannelController.tsx +0 -84
- package/src/components/LayerController/ChannelOptions.tsx +0 -183
- package/src/components/LayerController/ColorPalette.tsx +0 -40
- package/src/components/LayerController/Content.tsx +0 -71
- package/src/components/LayerController/Header.tsx +0 -48
- package/src/components/LayerController/Labels.tsx +0 -73
- package/src/components/LayerController/LayerFitToViewportButton.tsx +0 -43
- package/src/components/LayerController/LayerVisibilityButton.tsx +0 -32
- package/src/components/LayerController/OpacitySlider.tsx +0 -25
- package/src/components/LayerController/index.tsx +0 -42
- package/src/components/Menu.tsx +0 -54
- package/src/components/Viewer.tsx +0 -136
- package/src/hooks.ts +0 -29
- package/src/index.tsx +0 -138
- package/src/io.ts +0 -276
- package/src/layers/grid-layer.ts +0 -214
- package/src/layers/label-layer.ts +0 -446
- package/src/layers/viv-layers.ts +0 -55
- package/src/lru-store.ts +0 -57
- package/src/ome.ts +0 -388
- package/src/state.ts +0 -202
- package/src/theme.ts +0 -80
- package/src/types.ts +0 -112
- package/src/utils.ts +0 -655
- package/tsconfig.json +0 -14
- package/vite.config.js +0 -47
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { ScaleBarLayer } from "@hms-dbmi/viv";
|
|
2
|
-
import DeckGL from "deck.gl";
|
|
3
|
-
import { OrthographicView } from "deck.gl";
|
|
4
|
-
import { useAtom, useAtomValue } from "jotai";
|
|
5
|
-
import * as React from "react";
|
|
6
|
-
import { useViewState } from "../hooks";
|
|
7
|
-
import { layerAtoms, viewportAtom } from "../state";
|
|
8
|
-
import { fitImageToViewport, getLayerSize, resolveLoaderFromLayerProps } from "../utils";
|
|
9
|
-
|
|
10
|
-
import type { DeckGLRef, OrthographicViewState, PickingInfo } from "deck.gl";
|
|
11
|
-
import type { GrayscaleBitmapLayerPickingInfo } from "../layers/label-layer";
|
|
12
|
-
import type { ViewState, VizarrLayer } from "../state";
|
|
13
|
-
|
|
14
|
-
export default function Viewer() {
|
|
15
|
-
const deckRef = React.useRef<DeckGLRef>(null);
|
|
16
|
-
const [viewport, setViewport] = useAtom(viewportAtom);
|
|
17
|
-
const [viewState, setViewState] = useViewState();
|
|
18
|
-
const layers = useAtomValue(layerAtoms);
|
|
19
|
-
const firstLayer = layers[0] as VizarrLayer;
|
|
20
|
-
|
|
21
|
-
const resetViewState = React.useCallback(
|
|
22
|
-
(layer: VizarrLayer) => {
|
|
23
|
-
const { deck } = deckRef.current || {};
|
|
24
|
-
if (deck) {
|
|
25
|
-
setViewState({
|
|
26
|
-
...fitImageToViewport({
|
|
27
|
-
image: getLayerSize(layer),
|
|
28
|
-
viewport: deck,
|
|
29
|
-
padding: deck.width < 400 ? 10 : deck.width < 600 ? 30 : 50,
|
|
30
|
-
matrix: layer?.props.modelMatrix,
|
|
31
|
-
}),
|
|
32
|
-
width: deck.width,
|
|
33
|
-
height: deck.height,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
[setViewState],
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
React.useEffect(() => {
|
|
41
|
-
if (!viewport && deckRef.current?.deck) {
|
|
42
|
-
setViewport(deckRef.current.deck);
|
|
43
|
-
}
|
|
44
|
-
if (viewport && firstLayer) {
|
|
45
|
-
if (!viewState) {
|
|
46
|
-
resetViewState(firstLayer);
|
|
47
|
-
} else if (!(viewState?.width || viewState?.height)) {
|
|
48
|
-
setViewState((vs) => ({
|
|
49
|
-
...(vs as ViewState),
|
|
50
|
-
width: viewport.width,
|
|
51
|
-
height: viewport.height,
|
|
52
|
-
}));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}, [viewport, setViewport, firstLayer, resetViewState, viewState, setViewState]);
|
|
56
|
-
|
|
57
|
-
const deckLayers = React.useMemo(() => {
|
|
58
|
-
if (!firstLayer || !(viewState?.width && viewState?.height)) {
|
|
59
|
-
return layers;
|
|
60
|
-
}
|
|
61
|
-
const loader = resolveLoaderFromLayerProps(firstLayer.props);
|
|
62
|
-
if (Array.isArray(loader) && loader?.[0]?.meta?.physicalSizes?.x) {
|
|
63
|
-
const { size, unit } = loader[0].meta.physicalSizes.x;
|
|
64
|
-
const scalebar = new ScaleBarLayer({
|
|
65
|
-
id: "scalebar",
|
|
66
|
-
size: size / firstLayer.props.modelMatrix[0],
|
|
67
|
-
unit: unit,
|
|
68
|
-
viewState: viewState,
|
|
69
|
-
snap: false,
|
|
70
|
-
});
|
|
71
|
-
return [...layers, scalebar];
|
|
72
|
-
}
|
|
73
|
-
return layers;
|
|
74
|
-
}, [layers, firstLayer, viewState]);
|
|
75
|
-
|
|
76
|
-
// Enables screenshots of the canvas: https://github.com/visgl/deck.gl/issues/2200
|
|
77
|
-
const glOptions: WebGLContextAttributes = {
|
|
78
|
-
preserveDrawingBuffer: true,
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const getTooltip = (info: GrayscaleBitmapLayerPickingInfo | PickingInfo) => {
|
|
82
|
-
const { layer, index } = info as PickingInfo;
|
|
83
|
-
const { label, value } = info as GrayscaleBitmapLayerPickingInfo;
|
|
84
|
-
if (!layer || index === null || index === undefined) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
return {
|
|
88
|
-
text: value !== null && value !== undefined ? `${label}: ${value}` : `${label}`,
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const { near, far } = React.useMemo(() => {
|
|
93
|
-
if (!firstLayer) {
|
|
94
|
-
return { near: 0.1, far: 1000 };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const zs = layers.flatMap((layer) => {
|
|
98
|
-
const matrix = (layer as VizarrLayer)?.props?.modelMatrix;
|
|
99
|
-
if (!matrix) {
|
|
100
|
-
return [];
|
|
101
|
-
}
|
|
102
|
-
const { width, height } = getLayerSize(firstLayer);
|
|
103
|
-
const corners = [
|
|
104
|
-
[0, 0, 0],
|
|
105
|
-
[width, 0, 0],
|
|
106
|
-
[width, height, 0],
|
|
107
|
-
[0, height, 0],
|
|
108
|
-
].map((corner) => matrix.transformAsPoint(corner)[2]);
|
|
109
|
-
return corners;
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const minZ = Math.min(...zs);
|
|
113
|
-
const maxZ = Math.max(...zs);
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
near: maxZ ? -10000 * Math.abs(maxZ) : 0.1,
|
|
117
|
-
far: minZ ? 10000 * Math.abs(minZ) : 1000,
|
|
118
|
-
};
|
|
119
|
-
}, [layers, firstLayer]);
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<DeckGL
|
|
123
|
-
ref={deckRef}
|
|
124
|
-
layers={deckLayers}
|
|
125
|
-
viewState={viewState && { ortho: viewState }}
|
|
126
|
-
onViewStateChange={(e: { viewState: OrthographicViewState }) =>
|
|
127
|
-
// @ts-expect-error - deck doesn't know this should be ok
|
|
128
|
-
setViewState(e.viewState)
|
|
129
|
-
}
|
|
130
|
-
views={[new OrthographicView({ id: "ortho", controller: true, near, far })]}
|
|
131
|
-
glOptions={glOptions}
|
|
132
|
-
getTooltip={getTooltip}
|
|
133
|
-
onDeviceInitialized={() => setViewport(deckRef.current?.deck || null)}
|
|
134
|
-
/>
|
|
135
|
-
);
|
|
136
|
-
}
|
package/src/hooks.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { type PrimitiveAtom, useAtom } from "jotai";
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
import type { LayerState, SourceData, ViewState } from "./state";
|
|
4
|
-
|
|
5
|
-
import * as utils from "./utils";
|
|
6
|
-
|
|
7
|
-
export const SourceDataContext = React.createContext<PrimitiveAtom<{ id: string } & SourceData> | null>(null);
|
|
8
|
-
|
|
9
|
-
export function useSourceData() {
|
|
10
|
-
const atom = React.useContext(SourceDataContext);
|
|
11
|
-
utils.assert(atom, "useSourceData hook must be used within SourceDataContext.");
|
|
12
|
-
return useAtom(atom);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const LayerStateContext = React.createContext<PrimitiveAtom<{ id: string } & LayerState> | null>(null);
|
|
16
|
-
|
|
17
|
-
export function useLayerState() {
|
|
18
|
-
const atom = React.useContext(LayerStateContext);
|
|
19
|
-
utils.assert(atom, "useLayerState hook must be used within LayerStateContext.");
|
|
20
|
-
return useAtom(atom);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const ViewStateContext = React.createContext<PrimitiveAtom<ViewState | null> | null>(null);
|
|
24
|
-
|
|
25
|
-
export function useViewState() {
|
|
26
|
-
const atom = React.useContext(ViewStateContext);
|
|
27
|
-
utils.assert(atom, "useViewState hook must be used within ViewStateContext.");
|
|
28
|
-
return useAtom(atom);
|
|
29
|
-
}
|
package/src/index.tsx
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { Box, Link, Typography } from "@mui/material";
|
|
2
|
-
import { ThemeProvider } from "@mui/material/styles";
|
|
3
|
-
import { type PrimitiveAtom, Provider, atom } from "jotai";
|
|
4
|
-
import { useAtomValue, useSetAtom } from "jotai";
|
|
5
|
-
import * as React from "react";
|
|
6
|
-
import ReactDOM from "react-dom/client";
|
|
7
|
-
|
|
8
|
-
import Menu from "./components/Menu";
|
|
9
|
-
import Viewer from "./components/Viewer";
|
|
10
|
-
import "./codecs/register";
|
|
11
|
-
import { ViewStateContext } from "./hooks";
|
|
12
|
-
import {
|
|
13
|
-
type ImageLayerConfig,
|
|
14
|
-
type ViewState,
|
|
15
|
-
addImageAtom,
|
|
16
|
-
redirectObjAtom,
|
|
17
|
-
sourceErrorAtom,
|
|
18
|
-
viewStateAtom,
|
|
19
|
-
} from "./state";
|
|
20
|
-
import theme from "./theme";
|
|
21
|
-
import { defer, typedEmitter } from "./utils";
|
|
22
|
-
|
|
23
|
-
export { version } from "../package.json";
|
|
24
|
-
|
|
25
|
-
type Events = {
|
|
26
|
-
viewStateChange: ViewState;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type { ViewState, ImageLayerConfig };
|
|
30
|
-
|
|
31
|
-
export interface VizarrViewer {
|
|
32
|
-
addImage(config: ImageLayerConfig): void;
|
|
33
|
-
setViewState(viewState: ViewState): void;
|
|
34
|
-
on<E extends keyof Events>(event: E, cb: (data: Events[E]) => void): void;
|
|
35
|
-
destroy(): void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function createViewer(element: HTMLElement, options: { menuOpen?: boolean } = {}): Promise<VizarrViewer> {
|
|
39
|
-
const ref = React.createRef<VizarrViewer>();
|
|
40
|
-
const emitter = typedEmitter<Events>();
|
|
41
|
-
const viewStateAtomWithEffect: PrimitiveAtom<ViewState | null> = atom(
|
|
42
|
-
(get) => get(viewStateAtom),
|
|
43
|
-
(get, set, update) => {
|
|
44
|
-
const viewState = typeof update === "function" ? update(get(viewStateAtom)) : update;
|
|
45
|
-
if (viewState)
|
|
46
|
-
emitter.emit("viewStateChange", {
|
|
47
|
-
target: viewState.target,
|
|
48
|
-
zoom: viewState.zoom,
|
|
49
|
-
});
|
|
50
|
-
set(viewStateAtom, update);
|
|
51
|
-
},
|
|
52
|
-
);
|
|
53
|
-
const { promise, resolve } = defer<VizarrViewer>();
|
|
54
|
-
|
|
55
|
-
function App() {
|
|
56
|
-
const sourceError = useAtomValue(sourceErrorAtom);
|
|
57
|
-
const redirectObj = useAtomValue(redirectObjAtom);
|
|
58
|
-
const addImage = useSetAtom(addImageAtom);
|
|
59
|
-
const setViewState = useSetAtom(viewStateAtomWithEffect);
|
|
60
|
-
React.useImperativeHandle(
|
|
61
|
-
ref,
|
|
62
|
-
() => ({
|
|
63
|
-
addImage,
|
|
64
|
-
setViewState,
|
|
65
|
-
on: emitter.on.bind(emitter),
|
|
66
|
-
destroy: () => root.unmount(),
|
|
67
|
-
}),
|
|
68
|
-
[setViewState, addImage],
|
|
69
|
-
);
|
|
70
|
-
React.useEffect(() => {
|
|
71
|
-
if (ref.current) {
|
|
72
|
-
resolve(ref.current);
|
|
73
|
-
}
|
|
74
|
-
}, []);
|
|
75
|
-
return (
|
|
76
|
-
<>
|
|
77
|
-
{sourceError === null && redirectObj === null && (
|
|
78
|
-
<ViewStateContext.Provider value={viewStateAtomWithEffect}>
|
|
79
|
-
<Menu open={options.menuOpen ?? true} />
|
|
80
|
-
<Viewer />
|
|
81
|
-
</ViewStateContext.Provider>
|
|
82
|
-
)}
|
|
83
|
-
{sourceError !== null && (
|
|
84
|
-
<Box
|
|
85
|
-
sx={{
|
|
86
|
-
position: "fixed",
|
|
87
|
-
top: 0,
|
|
88
|
-
bottom: 0,
|
|
89
|
-
left: 0,
|
|
90
|
-
right: 0,
|
|
91
|
-
color: "#fff",
|
|
92
|
-
display: "flex",
|
|
93
|
-
alignItems: "center",
|
|
94
|
-
textAlign: "center",
|
|
95
|
-
justifyContent: "center",
|
|
96
|
-
fontSize: "120%",
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
<p>{`Error: server replied with "${sourceError}" when loading the resource`}</p>
|
|
100
|
-
</Box>
|
|
101
|
-
)}
|
|
102
|
-
{redirectObj !== null && (
|
|
103
|
-
<Box
|
|
104
|
-
sx={{
|
|
105
|
-
position: "fixed",
|
|
106
|
-
top: 0,
|
|
107
|
-
bottom: 0,
|
|
108
|
-
left: 0,
|
|
109
|
-
right: 0,
|
|
110
|
-
color: "#fff",
|
|
111
|
-
display: "flex",
|
|
112
|
-
alignItems: "center",
|
|
113
|
-
textAlign: "center",
|
|
114
|
-
justifyContent: "center",
|
|
115
|
-
fontSize: "120%",
|
|
116
|
-
}}
|
|
117
|
-
>
|
|
118
|
-
<Typography variant="h5">
|
|
119
|
-
{redirectObj.message}
|
|
120
|
-
<Link href={redirectObj.url}> {redirectObj.url} </Link>
|
|
121
|
-
</Typography>
|
|
122
|
-
</Box>
|
|
123
|
-
)}
|
|
124
|
-
</>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
let root = ReactDOM.createRoot(element);
|
|
128
|
-
root.render(
|
|
129
|
-
<ThemeProvider theme={theme}>
|
|
130
|
-
<Provider>
|
|
131
|
-
<ViewStateContext.Provider value={viewStateAtomWithEffect}>
|
|
132
|
-
<App />
|
|
133
|
-
</ViewStateContext.Provider>
|
|
134
|
-
</Provider>
|
|
135
|
-
</ThemeProvider>,
|
|
136
|
-
);
|
|
137
|
-
return promise;
|
|
138
|
-
}
|
package/src/io.ts
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import * as zarr from "zarrita";
|
|
2
|
-
import { ZarrPixelSource } from "./ZarrPixelSource";
|
|
3
|
-
import { loadOmeMultiscales, loadPlate, loadWell } from "./ome";
|
|
4
|
-
import * as utils from "./utils";
|
|
5
|
-
|
|
6
|
-
import { DEFAULT_LABEL_OPACITY } from "./layers/label-layer";
|
|
7
|
-
import type { BaseLayerProps } from "./layers/viv-layers";
|
|
8
|
-
import type { ImageLayerConfig, LayerState, MultichannelConfig, SingleChannelConfig, SourceData } from "./state";
|
|
9
|
-
|
|
10
|
-
async function loadSingleChannel(config: SingleChannelConfig, data: Array<ZarrPixelSource>): Promise<SourceData> {
|
|
11
|
-
const { color, contrast_limits, visibility, name, colormap = "", opacity = 1 } = config;
|
|
12
|
-
const lowres = data[data.length - 1];
|
|
13
|
-
const selection = Array(data[0].shape.length).fill(0);
|
|
14
|
-
const limits = contrast_limits ?? (await (() => utils.calcDataRange(lowres, selection))());
|
|
15
|
-
return {
|
|
16
|
-
loader: data,
|
|
17
|
-
name,
|
|
18
|
-
channel_axis: null,
|
|
19
|
-
colors: [color ?? utils.COLORS.white],
|
|
20
|
-
names: ["channel_0"],
|
|
21
|
-
contrast_limits: [limits],
|
|
22
|
-
visibilities: [visibility ?? true],
|
|
23
|
-
model_matrix: utils.parseMatrix(config.model_matrix),
|
|
24
|
-
defaults: {
|
|
25
|
-
selection,
|
|
26
|
-
colormap,
|
|
27
|
-
opacity,
|
|
28
|
-
},
|
|
29
|
-
axis_labels: data[0].labels,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function loadMultiChannel(
|
|
34
|
-
config: MultichannelConfig,
|
|
35
|
-
data: Array<ZarrPixelSource>,
|
|
36
|
-
channelAxis: number,
|
|
37
|
-
): Promise<SourceData> {
|
|
38
|
-
const { names, contrast_limits, name, model_matrix, opacity = 1, colormap = "" } = config;
|
|
39
|
-
let { visibilities, colors } = config;
|
|
40
|
-
const n = data[0].shape[channelAxis];
|
|
41
|
-
|
|
42
|
-
for (const channelProp of [contrast_limits, visibilities, names, colors]) {
|
|
43
|
-
if (channelProp) {
|
|
44
|
-
const propertyName = Object.keys({ channelProp })[0];
|
|
45
|
-
utils.assert(
|
|
46
|
-
channelProp.length === n,
|
|
47
|
-
`channel_axis is length ${n} and provided channel_axis property ${propertyName} is different size.`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
visibilities = visibilities || utils.getDefaultVisibilities(n);
|
|
53
|
-
colors = colors || utils.getDefaultColors(n, visibilities);
|
|
54
|
-
|
|
55
|
-
const contrastLimits =
|
|
56
|
-
contrast_limits ?? (await (() => utils.calcConstrastLimits(data[data.length - 1], channelAxis, visibilities))());
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
loader: data,
|
|
60
|
-
name,
|
|
61
|
-
channel_axis: channelAxis,
|
|
62
|
-
colors,
|
|
63
|
-
names: names ?? utils.range(n).map((i) => `channel_${i}`),
|
|
64
|
-
contrast_limits: contrastLimits,
|
|
65
|
-
visibilities,
|
|
66
|
-
model_matrix: utils.parseMatrix(model_matrix),
|
|
67
|
-
defaults: {
|
|
68
|
-
selection: Array(data[0].shape.length).fill(0),
|
|
69
|
-
colormap,
|
|
70
|
-
opacity,
|
|
71
|
-
},
|
|
72
|
-
axis_labels: data[0].labels,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export async function createSourceData(config: ImageLayerConfig): Promise<SourceData> {
|
|
77
|
-
const node = await utils.open(config.source);
|
|
78
|
-
let data: zarr.Array<zarr.DataType, zarr.Readable>[];
|
|
79
|
-
let axes: Ome.Axis[] | undefined;
|
|
80
|
-
|
|
81
|
-
if (node instanceof zarr.Group) {
|
|
82
|
-
let attrs = utils.resolveAttrs(node.attrs);
|
|
83
|
-
|
|
84
|
-
if (utils.isOmePlate(attrs)) {
|
|
85
|
-
return loadPlate(config, node, attrs.plate);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (utils.isOmeWell(attrs)) {
|
|
89
|
-
return loadWell(config, node, attrs.well);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (utils.isMultiscales(attrs)) {
|
|
93
|
-
return loadOmeMultiscales(config, node, attrs);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (Object.keys(attrs).length === 0 && node.path) {
|
|
97
|
-
// No rootAttrs in this group.
|
|
98
|
-
const parent = await zarr.open(node.resolve(".."), { kind: "group" });
|
|
99
|
-
const parentAttrs = utils.resolveAttrs(parent.attrs);
|
|
100
|
-
if (utils.isOmePlate(parentAttrs)) {
|
|
101
|
-
return loadPlate(config, parent, parentAttrs.plate);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (utils.isBioformats2rawlayout(attrs)) {
|
|
106
|
-
let toUrl = `${utils.OME_VALIDATOR_URL}?source=${config.source}`;
|
|
107
|
-
throw new utils.RedirectError("Please open in ome-ngff-validator", toUrl);
|
|
108
|
-
}
|
|
109
|
-
utils.assert(utils.isMultiscales(attrs), "Group is missing multiscales specification.");
|
|
110
|
-
data = await utils.loadMultiscales(node, attrs.multiscales);
|
|
111
|
-
if (attrs.multiscales[0].axes) {
|
|
112
|
-
axes = utils.getNgffAxes(attrs.multiscales);
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
data = [node];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// explicit override in config > ngff > guessed from data shape
|
|
119
|
-
const { channel_axis, labels } = getAxisLabelsAndChannelAxis(config, axes, data[0]);
|
|
120
|
-
|
|
121
|
-
const tileSize = utils.guessTileSize(data[0]);
|
|
122
|
-
const loader = data.map((d) => new ZarrPixelSource(d, { labels, tileSize }));
|
|
123
|
-
const [base] = loader;
|
|
124
|
-
|
|
125
|
-
// If explicit channel axis is provided, try to load as multichannel.
|
|
126
|
-
|
|
127
|
-
if ("channel_axis" in config || channel_axis > -1) {
|
|
128
|
-
config = config as MultichannelConfig;
|
|
129
|
-
return loadMultiChannel(config, loader, Number(config.channel_axis ?? channel_axis));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const nDims = base.shape.length;
|
|
133
|
-
if (nDims === 2 || !("channel_axis" in config)) {
|
|
134
|
-
return loadSingleChannel(config as SingleChannelConfig, loader);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
throw Error("Failed to load image.");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
type Labels = [...string[], "y", "x"];
|
|
141
|
-
function getAxisLabelsAndChannelAxis(
|
|
142
|
-
config: ImageLayerConfig,
|
|
143
|
-
ngffAxes: Ome.Axis[] | undefined,
|
|
144
|
-
arr: zarr.Array<zarr.DataType, zarr.Readable>,
|
|
145
|
-
): { labels: Labels; channel_axis: number } {
|
|
146
|
-
// type cast string[] to Labels
|
|
147
|
-
const maybeAxisLabels = config.axis_labels as undefined | Labels;
|
|
148
|
-
// ensure numeric if provided
|
|
149
|
-
const maybeChannelAxis = "channel_axis" in config ? Number(config.channel_axis) : undefined;
|
|
150
|
-
// Use ngff axes metadata if labels or channel axis aren't explicitly provided
|
|
151
|
-
if (ngffAxes) {
|
|
152
|
-
const labels = maybeAxisLabels ?? utils.getNgffAxisLabels(ngffAxes);
|
|
153
|
-
const channel_axis = maybeChannelAxis ?? ngffAxes.findIndex((axis) => axis.type === "channel");
|
|
154
|
-
return { labels, channel_axis };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// create dummy axis labels if not provided and try to guess channel_axis if missing
|
|
158
|
-
const labels = maybeAxisLabels ?? utils.getAxisLabels(arr);
|
|
159
|
-
const channel_axis = maybeChannelAxis ?? labels.indexOf("c");
|
|
160
|
-
return { labels, channel_axis };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function initLayerStateFromSource(source: SourceData & { id: string }): LayerState {
|
|
164
|
-
const { selection, opacity, colormap } = source.defaults;
|
|
165
|
-
|
|
166
|
-
const selections: number[][] = [];
|
|
167
|
-
const colors: [number, number, number][] = [];
|
|
168
|
-
const contrastLimits: [start: number, end: number][] = [];
|
|
169
|
-
const channelsVisible: boolean[] = [];
|
|
170
|
-
|
|
171
|
-
const visibleIndices = source.visibilities.flatMap((bool, i) => (bool ? i : []));
|
|
172
|
-
// Limit the number of initial channels to the max allowed
|
|
173
|
-
for (const index of visibleIndices.slice(0, utils.MAX_CHANNELS)) {
|
|
174
|
-
const channelSelection = [...selection];
|
|
175
|
-
if (Number.isInteger(source.channel_axis)) {
|
|
176
|
-
channelSelection[source.channel_axis as number] = index;
|
|
177
|
-
}
|
|
178
|
-
selections.push(channelSelection);
|
|
179
|
-
colors.push(utils.hexToRGB(source.colors[index]));
|
|
180
|
-
// TODO: should never be undefined
|
|
181
|
-
contrastLimits.push(source.contrast_limits[index] ?? [0, 255]);
|
|
182
|
-
channelsVisible.push(true);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const layerProps = {
|
|
186
|
-
id: source.id,
|
|
187
|
-
selections,
|
|
188
|
-
colors,
|
|
189
|
-
contrastLimits,
|
|
190
|
-
contrastLimitsRange: [...contrastLimits],
|
|
191
|
-
channelsVisible,
|
|
192
|
-
opacity,
|
|
193
|
-
colormap,
|
|
194
|
-
modelMatrix: source.model_matrix,
|
|
195
|
-
onClick: source.onClick,
|
|
196
|
-
} satisfies BaseLayerProps;
|
|
197
|
-
|
|
198
|
-
if (source.loaders) {
|
|
199
|
-
return {
|
|
200
|
-
kind: "grid",
|
|
201
|
-
layerProps: {
|
|
202
|
-
...layerProps,
|
|
203
|
-
loaders: source.loaders,
|
|
204
|
-
columns: source.columns as number,
|
|
205
|
-
rows: source.rows as number,
|
|
206
|
-
},
|
|
207
|
-
on: true,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (source.loader.length === 1) {
|
|
212
|
-
return {
|
|
213
|
-
kind: "image",
|
|
214
|
-
layerProps: {
|
|
215
|
-
...layerProps,
|
|
216
|
-
loader: source.loader[0],
|
|
217
|
-
},
|
|
218
|
-
on: true,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
let labels = undefined;
|
|
223
|
-
if (source.labels && source.labels.length > 0) {
|
|
224
|
-
labels = source.labels.map((label, i) => ({
|
|
225
|
-
on: false,
|
|
226
|
-
transformSourceSelection: getSourceSelectionTransform(label.loader[0], source.loader[0]),
|
|
227
|
-
layerProps: {
|
|
228
|
-
id: `${source.id}_${i}`,
|
|
229
|
-
loader: label.loader,
|
|
230
|
-
modelMatrix: label.modelMatrix,
|
|
231
|
-
opacity: DEFAULT_LABEL_OPACITY,
|
|
232
|
-
colors: label.colors,
|
|
233
|
-
},
|
|
234
|
-
}));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
kind: "multiscale",
|
|
239
|
-
layerProps: {
|
|
240
|
-
...layerProps,
|
|
241
|
-
loader: source.loader,
|
|
242
|
-
},
|
|
243
|
-
on: true,
|
|
244
|
-
labels,
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function getSourceSelectionTransform(
|
|
249
|
-
labels: { shape: Array<number>; labels: Array<string> },
|
|
250
|
-
source: { shape: Array<number>; labels: Array<string> },
|
|
251
|
-
) {
|
|
252
|
-
utils.assert(
|
|
253
|
-
source.shape.length === source.labels.length,
|
|
254
|
-
`Image source axes and shape are not same rank. Got ${JSON.stringify(source)}`,
|
|
255
|
-
);
|
|
256
|
-
utils.assert(
|
|
257
|
-
labels.shape.length === labels.labels.length,
|
|
258
|
-
`Label axes and shape are not same rank. Got ${JSON.stringify(labels)}`,
|
|
259
|
-
);
|
|
260
|
-
utils.assert(
|
|
261
|
-
labels.labels.every((label) => source.labels.includes(label)),
|
|
262
|
-
`Label axes MUST be a subset of source. Source: ${JSON.stringify(source.labels)} Labels: ${JSON.stringify(labels.labels)}`,
|
|
263
|
-
);
|
|
264
|
-
// Identify labels that should always map to 0, regardless of the source selection.
|
|
265
|
-
const excludeFromTransformedSelection = new Set(
|
|
266
|
-
utils
|
|
267
|
-
.zip(labels.labels, labels.shape)
|
|
268
|
-
.filter(([_, size]) => size === 1)
|
|
269
|
-
.map(([name, _]) => name),
|
|
270
|
-
);
|
|
271
|
-
return (sourceSelection: Array<number>): Array<number> => {
|
|
272
|
-
return labels.labels.map((name) =>
|
|
273
|
-
excludeFromTransformedSelection.has(name) ? 0 : sourceSelection[source.labels.indexOf(name)],
|
|
274
|
-
);
|
|
275
|
-
};
|
|
276
|
-
}
|