@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
package/src/utils.ts
DELETED
|
@@ -1,655 +0,0 @@
|
|
|
1
|
-
import { Matrix4 } from "math.gl";
|
|
2
|
-
import * as zarr from "zarrita";
|
|
3
|
-
|
|
4
|
-
import type * as viv from "@vivjs/types";
|
|
5
|
-
import type { ZarrPixelSource } from "./ZarrPixelSource";
|
|
6
|
-
import type { GridLayerProps } from "./layers/grid-layer";
|
|
7
|
-
import type { LabelLayerProps } from "./layers/label-layer";
|
|
8
|
-
import type { ImageLayerProps, MultiscaleImageLayerProps } from "./layers/viv-layers";
|
|
9
|
-
import { lru } from "./lru-store";
|
|
10
|
-
import type { ViewState, VizarrLayer } from "./state";
|
|
11
|
-
|
|
12
|
-
export const MAX_CHANNELS = 6;
|
|
13
|
-
|
|
14
|
-
export const COLORS = {
|
|
15
|
-
cyan: "#00FFFF",
|
|
16
|
-
yellow: "#FFFF00",
|
|
17
|
-
magenta: "#FF00FF",
|
|
18
|
-
red: "#FF0000",
|
|
19
|
-
green: "#00FF00",
|
|
20
|
-
blue: "#0000FF",
|
|
21
|
-
white: "#FFFFFF",
|
|
22
|
-
};
|
|
23
|
-
export const MAGENTA_GREEN = [COLORS.magenta, COLORS.green];
|
|
24
|
-
export const RGB = [COLORS.red, COLORS.green, COLORS.blue];
|
|
25
|
-
export const CYMRGB = Object.values(COLORS).slice(0, MAX_CHANNELS);
|
|
26
|
-
export const OME_VALIDATOR_URL = "https://ome.github.io/ome-ngff-validator/";
|
|
27
|
-
|
|
28
|
-
async function normalizeStore(source: string | zarr.Readable): Promise<zarr.Location<zarr.Readable>> {
|
|
29
|
-
if (typeof source === "string") {
|
|
30
|
-
let store: zarr.Readable;
|
|
31
|
-
let path: `/${string}` = "/";
|
|
32
|
-
if (source.endsWith(".json")) {
|
|
33
|
-
// import custom store implementation
|
|
34
|
-
const [{ default: ReferenceStore }, json] = await Promise.all([
|
|
35
|
-
import("@zarrita/storage/ref"),
|
|
36
|
-
fetch(source).then((res) => res.json()),
|
|
37
|
-
]);
|
|
38
|
-
store = ReferenceStore.fromSpec(json);
|
|
39
|
-
} else {
|
|
40
|
-
const url = new URL(source);
|
|
41
|
-
// grab the path and then set the URL to the root
|
|
42
|
-
path = ensureAbosolutePath(url.pathname);
|
|
43
|
-
url.pathname = "/";
|
|
44
|
-
store = new zarr.FetchStore(url.href);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Wrap remote stores in a cache
|
|
48
|
-
return new zarr.Location(lru(store), path);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return zarr.root(source);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function ensureAbosolutePath(path: string): `/${string}` {
|
|
55
|
-
if (path === "/") return path;
|
|
56
|
-
// @ts-expect-error - path always starts with '/'
|
|
57
|
-
return path.startsWith("/") ? path : `/${path}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function open(source: string | zarr.Readable) {
|
|
61
|
-
const location = await normalizeStore(source);
|
|
62
|
-
return zarr.open(location);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function getAttrsOnly<T = unknown>(
|
|
66
|
-
location: zarr.Location<zarr.Readable>,
|
|
67
|
-
options: { path?: string; zarrVersion: 2 | 3 },
|
|
68
|
-
) {
|
|
69
|
-
const decoder = new TextDecoder();
|
|
70
|
-
if (options.path) {
|
|
71
|
-
location = location.resolve(options.path);
|
|
72
|
-
}
|
|
73
|
-
if (options.zarrVersion === 3) {
|
|
74
|
-
const attrs = await zarr.open.v3(location).then((node) => node.attrs);
|
|
75
|
-
return resolveAttrs(attrs) as T;
|
|
76
|
-
}
|
|
77
|
-
const v2AttrsLocation = location.resolve(".zattrs");
|
|
78
|
-
const maybeBytes = await location.store.get(v2AttrsLocation.path);
|
|
79
|
-
const attrs = maybeBytes ? JSON.parse(decoder.decode(maybeBytes)) : {};
|
|
80
|
-
return resolveAttrs(attrs) as T;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Loads the multiscales from a group and returns the datasets.
|
|
85
|
-
*
|
|
86
|
-
* NOTE: We avoid loading the attributes here because we don't need them.
|
|
87
|
-
*/
|
|
88
|
-
export async function loadMultiscales(
|
|
89
|
-
grp: zarr.Group<zarr.Readable>,
|
|
90
|
-
multiscales: Ome.Multiscale[],
|
|
91
|
-
): Promise<Array<zarr.Array<zarr.DataType, zarr.Readable>>> {
|
|
92
|
-
const { datasets } = multiscales[0] || [{ path: "0" }];
|
|
93
|
-
return Promise.all(
|
|
94
|
-
// TODO(Trevor): TS is not happy about { attrs: false }.
|
|
95
|
-
// This is just missing from zarrita types, but it is ok and
|
|
96
|
-
// avoids making unecessary requests for v2 (see: https://github.com/manzt/zarrita.js/blob/7edffbeefb0eb877df48f54c7e8def4219c69c59/packages/zarrita/CHANGELOG.md?plain=1#L214)
|
|
97
|
-
datasets.map(({ path }) => zarr.open(grp.resolve(path), { kind: "array", attrs: false } as { kind: "array" })),
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function hexToRGB(hex: string): [r: number, g: number, b: number] {
|
|
102
|
-
if (hex.startsWith("#")) hex = hex.slice(1);
|
|
103
|
-
const r = Number.parseInt(hex.slice(0, 2), 16);
|
|
104
|
-
const g = Number.parseInt(hex.slice(2, 4), 16);
|
|
105
|
-
const b = Number.parseInt(hex.slice(4, 6), 16);
|
|
106
|
-
return [r, g, b];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function range(length: number): number[] {
|
|
110
|
-
return Array.from({ length }, (_, i) => i);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// similar to Python's rstrip
|
|
114
|
-
export function rstrip(str: string, remove = " "): string {
|
|
115
|
-
// if the last character is in 'remove', truncate
|
|
116
|
-
while (str.length > 0 && remove.includes(str.charAt(str.length - 1))) {
|
|
117
|
-
str = str.substr(0, str.length - 1);
|
|
118
|
-
}
|
|
119
|
-
return str;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function join(...args: (string | undefined)[]) {
|
|
123
|
-
return args
|
|
124
|
-
.filter((s) => s !== undefined)
|
|
125
|
-
.map((s) => rstrip(s, "/"))
|
|
126
|
-
.join("/");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function getAxisLabels(
|
|
130
|
-
arr: zarr.Array<zarr.DataType, zarr.Readable>,
|
|
131
|
-
axis_labels?: string[],
|
|
132
|
-
): [...string[], "y", "x"] {
|
|
133
|
-
if (!axis_labels || axis_labels.length !== arr.shape.length) {
|
|
134
|
-
// default axis_labels are e.g. ['0', '1', 'y', 'x']
|
|
135
|
-
const nonXYaxisLabels = arr.shape.slice(0, -2).map((_, i) => `${i}`);
|
|
136
|
-
axis_labels = nonXYaxisLabels.concat(["y", "x"]);
|
|
137
|
-
}
|
|
138
|
-
return axis_labels as [...string[], "y", "x"];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function getNgffAxes(multiscales: Ome.Multiscale[]): Ome.Axis[] {
|
|
142
|
-
// Returns axes in the latest v0.4+ format.
|
|
143
|
-
// defaults for v0.1 & v0.2
|
|
144
|
-
const default_axes = [
|
|
145
|
-
{ type: "time", name: "t" },
|
|
146
|
-
{ type: "channel", name: "c" },
|
|
147
|
-
{ type: "space", name: "z" },
|
|
148
|
-
{ type: "space", name: "y" },
|
|
149
|
-
{ type: "space", name: "x" },
|
|
150
|
-
];
|
|
151
|
-
function getDefaultType(name: string): string {
|
|
152
|
-
if (name === "t") return "time";
|
|
153
|
-
if (name === "c") return "channel";
|
|
154
|
-
return "space";
|
|
155
|
-
}
|
|
156
|
-
let axes = default_axes;
|
|
157
|
-
// v0.3 & v0.4+
|
|
158
|
-
if (multiscales[0].axes) {
|
|
159
|
-
axes = multiscales[0].axes.map((axis) => {
|
|
160
|
-
// axis may be string 'x' (v0.3) or object
|
|
161
|
-
if (typeof axis === "string") {
|
|
162
|
-
return { name: axis, type: getDefaultType(axis) };
|
|
163
|
-
}
|
|
164
|
-
const { name, type, unit } = axis;
|
|
165
|
-
return { name, type: type ?? getDefaultType(name), unit };
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return axes;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function getNgffAxisLabels(axes: Ome.Axis[]): [...string[], "y", "x"] {
|
|
172
|
-
const axes_names = axes.map((axis) => axis.name);
|
|
173
|
-
return axes_names as [...string[], "y", "x"];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function getDefaultVisibilities(n: number, visibilities?: boolean[]): boolean[] {
|
|
177
|
-
if (!visibilities) {
|
|
178
|
-
if (n <= MAX_CHANNELS) {
|
|
179
|
-
// Default to all on if visibilities not specified and less than 6 channels.
|
|
180
|
-
visibilities = Array(n).fill(true);
|
|
181
|
-
} else {
|
|
182
|
-
// If more than MAX_CHANNELS, only make first set on by default.
|
|
183
|
-
visibilities = [...Array(MAX_CHANNELS).fill(true), ...Array(n - MAX_CHANNELS).fill(false)];
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return visibilities;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function getDefaultColors(n: number, visibilities: boolean[]): string[] {
|
|
190
|
-
let colors = [];
|
|
191
|
-
if (n === 1) {
|
|
192
|
-
colors = [COLORS.white];
|
|
193
|
-
} else if (n === 2) {
|
|
194
|
-
colors = MAGENTA_GREEN;
|
|
195
|
-
} else if (n === 3) {
|
|
196
|
-
colors = RGB;
|
|
197
|
-
} else if (n <= MAX_CHANNELS) {
|
|
198
|
-
colors = CYMRGB.slice(0, n);
|
|
199
|
-
} else {
|
|
200
|
-
// Default color for non-visible is white
|
|
201
|
-
colors = Array(n).fill(COLORS.white);
|
|
202
|
-
// Get visible indices
|
|
203
|
-
const visibleIndices = visibilities.flatMap((bool, i) => (bool ? i : []));
|
|
204
|
-
// Set visible indices to CYMRGB colors. visibleIndices.length === MAX_CHANNELS from above.
|
|
205
|
-
for (const [i, visibleIndex] of visibleIndices.entries()) {
|
|
206
|
-
colors[visibleIndex] = CYMRGB[i];
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return colors;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
export function isInterleaved(shape: number[]) {
|
|
213
|
-
const lastDimSize = shape[shape.length - 1];
|
|
214
|
-
return lastDimSize === 3 || lastDimSize === 4;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function guessTileSize(arr: zarr.Array<zarr.DataType, zarr.Readable>) {
|
|
218
|
-
const interleaved = isInterleaved(arr.shape);
|
|
219
|
-
const [ySize, xSize] = arr.chunks.slice(interleaved ? -3 : -2);
|
|
220
|
-
const size = Math.min(ySize, xSize);
|
|
221
|
-
// Needs to be a power of 2 for deck.gl
|
|
222
|
-
return 2 ** Math.floor(Math.log2(size));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Scales the real image size to the target viewport.
|
|
226
|
-
export function fitImageToViewport(options: {
|
|
227
|
-
image: { width: number; height: number };
|
|
228
|
-
viewport: { width: number; height: number };
|
|
229
|
-
padding: number;
|
|
230
|
-
matrix?: Matrix4;
|
|
231
|
-
}): ViewState {
|
|
232
|
-
const { image, viewport, padding, matrix = new Matrix4().identity() } = options;
|
|
233
|
-
const corners = [
|
|
234
|
-
[0, 0, 0],
|
|
235
|
-
[image.width, 0, 0],
|
|
236
|
-
[image.width, image.height, 0],
|
|
237
|
-
[0, image.height, 0],
|
|
238
|
-
].map((corner) => matrix.transformAsPoint(corner));
|
|
239
|
-
|
|
240
|
-
const minX = Math.min(...corners.map((p) => p[0]));
|
|
241
|
-
const maxX = Math.max(...corners.map((p) => p[0]));
|
|
242
|
-
const minY = Math.min(...corners.map((p) => p[1]));
|
|
243
|
-
const maxY = Math.max(...corners.map((p) => p[1]));
|
|
244
|
-
|
|
245
|
-
const availableWidth = viewport.width - 2 * padding;
|
|
246
|
-
const availableHeight = viewport.height - 2 * padding;
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
zoom: Math.log2(
|
|
250
|
-
Math.min(
|
|
251
|
-
availableWidth / (maxX - minX), // scaleX
|
|
252
|
-
availableHeight / (maxY - minY), // scaleY
|
|
253
|
-
),
|
|
254
|
-
),
|
|
255
|
-
target: [(minX + maxX) / 2, (minY + maxY) / 2],
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
type Array16 = [
|
|
260
|
-
number,
|
|
261
|
-
number,
|
|
262
|
-
number,
|
|
263
|
-
number,
|
|
264
|
-
number,
|
|
265
|
-
number,
|
|
266
|
-
number,
|
|
267
|
-
number,
|
|
268
|
-
number,
|
|
269
|
-
number,
|
|
270
|
-
number,
|
|
271
|
-
number,
|
|
272
|
-
number,
|
|
273
|
-
number,
|
|
274
|
-
number,
|
|
275
|
-
number,
|
|
276
|
-
];
|
|
277
|
-
|
|
278
|
-
function isArray16(o: unknown): o is Array16 {
|
|
279
|
-
if (!Array.isArray(o)) return false;
|
|
280
|
-
return o.length === 16 && o.every((i) => typeof i === "number");
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function parseMatrix(model_matrix?: string | number[]): Matrix4 {
|
|
284
|
-
if (!model_matrix) {
|
|
285
|
-
return Matrix4.IDENTITY;
|
|
286
|
-
}
|
|
287
|
-
const matrix = new Matrix4();
|
|
288
|
-
try {
|
|
289
|
-
const arr = typeof model_matrix === "string" ? JSON.parse(model_matrix) : model_matrix;
|
|
290
|
-
assert(isArray16(arr), "Invalid modelMatrix size. Must be 16.");
|
|
291
|
-
matrix.setRowMajor(...arr);
|
|
292
|
-
} catch {
|
|
293
|
-
const msg = `Failed to parse modelMatrix. Got ${JSON.stringify(model_matrix)}, using identity.`;
|
|
294
|
-
console.warn(msg);
|
|
295
|
-
}
|
|
296
|
-
return matrix;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export async function calcDataRange(
|
|
300
|
-
source: ZarrPixelSource,
|
|
301
|
-
selection: Array<number>,
|
|
302
|
-
): Promise<[min: number, max: number]> {
|
|
303
|
-
if (source.dtype === "Uint8") return [0, 255];
|
|
304
|
-
const { data } = await source.getRaster({ selection });
|
|
305
|
-
let minVal = Number.POSITIVE_INFINITY;
|
|
306
|
-
let maxVal = Number.NEGATIVE_INFINITY;
|
|
307
|
-
for (let i = 0; i < data.length; i++) {
|
|
308
|
-
if (data[i] > maxVal) maxVal = data[i];
|
|
309
|
-
if (data[i] < minVal) minVal = data[i];
|
|
310
|
-
}
|
|
311
|
-
if (minVal === maxVal) {
|
|
312
|
-
minVal = 0;
|
|
313
|
-
maxVal = 1;
|
|
314
|
-
}
|
|
315
|
-
return [minVal, maxVal];
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export async function calcConstrastLimits(
|
|
319
|
-
source: ZarrPixelSource,
|
|
320
|
-
channelAxis: number,
|
|
321
|
-
visibilities: boolean[],
|
|
322
|
-
defaultSelection?: number[],
|
|
323
|
-
): Promise<([min: number, max: number] | undefined)[]> {
|
|
324
|
-
const def = defaultSelection ?? source.shape.map(() => 0);
|
|
325
|
-
const csize = source.shape[channelAxis];
|
|
326
|
-
|
|
327
|
-
// channelAxis can be -1 if there is no 'c' dimension
|
|
328
|
-
if (channelAxis !== -1) {
|
|
329
|
-
assert(csize === visibilities.length, "visibilities do not match number of channels");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
return Promise.all(
|
|
333
|
-
visibilities.map(async (isVisible, i) => {
|
|
334
|
-
if (!isVisible) return undefined; // don't compute non-visible channels
|
|
335
|
-
const selection = [...def];
|
|
336
|
-
if (channelAxis > -1) {
|
|
337
|
-
selection[channelAxis] = i;
|
|
338
|
-
}
|
|
339
|
-
return calcDataRange(source, selection);
|
|
340
|
-
}),
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Create a promise that can be resolved or rejected externally.
|
|
346
|
-
*
|
|
347
|
-
* TODO: Switch to Promise.withResolvers when it's available
|
|
348
|
-
*/
|
|
349
|
-
export function defer<T>() {
|
|
350
|
-
let resolve: (value: T | PromiseLike<T>) => void;
|
|
351
|
-
let reject: (reason?: unknown) => void;
|
|
352
|
-
const promise = new Promise<T>((res, rej) => {
|
|
353
|
-
resolve = res;
|
|
354
|
-
reject = rej;
|
|
355
|
-
});
|
|
356
|
-
// @ts-expect-error - resolve and reject are OK
|
|
357
|
-
return { promise, resolve, reject };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* A simple event emitter that allows for typed events.
|
|
362
|
-
*
|
|
363
|
-
* @example
|
|
364
|
-
* ```ts
|
|
365
|
-
* const emitter = typedEmitter<{ foo: string, bar: number }>();
|
|
366
|
-
* emitter.on('foo', (data) => console.log(data));
|
|
367
|
-
* emitter.emit('foo', 'hello');
|
|
368
|
-
* ```
|
|
369
|
-
*
|
|
370
|
-
* TODO: Add support for removing listeners.
|
|
371
|
-
*/
|
|
372
|
-
export function typedEmitter<T>() {
|
|
373
|
-
let target = new EventTarget();
|
|
374
|
-
return {
|
|
375
|
-
on: <E extends keyof T & string>(event: E, cb: (data: T[E]) => void) => {
|
|
376
|
-
target.addEventListener(event, (e) => cb((e as CustomEvent).detail));
|
|
377
|
-
},
|
|
378
|
-
emit: <E extends keyof T & string>(event: E, data: T[E]) => {
|
|
379
|
-
target.dispatchEvent(new CustomEvent(event, { detail: data }));
|
|
380
|
-
},
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Extracts the OME metadata from the zarr attributes
|
|
386
|
-
*
|
|
387
|
-
* TODO: We should use zod to handle this
|
|
388
|
-
*/
|
|
389
|
-
export function resolveAttrs(attrs: zarr.Attributes): zarr.Attributes {
|
|
390
|
-
if ("ome" in attrs) {
|
|
391
|
-
// @ts-expect-error - handles v0.5
|
|
392
|
-
return attrs.ome;
|
|
393
|
-
}
|
|
394
|
-
return attrs;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Error thrown when an assertion fails.
|
|
399
|
-
*/
|
|
400
|
-
export class AssertionError extends Error {
|
|
401
|
-
/** @param message The error message. */
|
|
402
|
-
constructor(message: string) {
|
|
403
|
-
super(message);
|
|
404
|
-
this.name = "AssertionError";
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Error thrown when we want to redirect.
|
|
410
|
-
*/
|
|
411
|
-
export class RedirectError extends Error {
|
|
412
|
-
url: string;
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* @param message The error message.
|
|
416
|
-
* @param url The url to redirect to.
|
|
417
|
-
*/
|
|
418
|
-
constructor(message: string, url: string) {
|
|
419
|
-
super(message);
|
|
420
|
-
this.name = "RedirectError";
|
|
421
|
-
this.url = url;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Make an assertion. An error is thrown if `expr` does not have truthy value.
|
|
427
|
-
*
|
|
428
|
-
* @param expr The expression to test.
|
|
429
|
-
* @param msg The message to display if the assertion fails.
|
|
430
|
-
*/
|
|
431
|
-
export function assert(expr: unknown, msg = ""): asserts expr {
|
|
432
|
-
if (!expr) {
|
|
433
|
-
throw new AssertionError(msg);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Guess the zarr version of a store.
|
|
439
|
-
*/
|
|
440
|
-
export async function guessZarrVersion(location: zarr.Location<zarr.Readable>): Promise<2 | 3> {
|
|
441
|
-
try {
|
|
442
|
-
await zarr.open.v3(location);
|
|
443
|
-
return 3;
|
|
444
|
-
} catch (err) {
|
|
445
|
-
if (!(err instanceof zarr.NodeNotFoundError)) {
|
|
446
|
-
// rethrow if not a NodeNotFoundError
|
|
447
|
-
throw err;
|
|
448
|
-
}
|
|
449
|
-
return 2;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
export function isOmePlate(attrs: zarr.Attributes): attrs is { plate: Ome.Plate } {
|
|
454
|
-
return "plate" in attrs;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
export function isOmeWell(attrs: zarr.Attributes): attrs is { well: Ome.Well } {
|
|
458
|
-
return "well" in attrs;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export function isOmeImageLabel(
|
|
462
|
-
attrs: zarr.Attributes,
|
|
463
|
-
): attrs is { "image-label": Ome.ImageLabel; multiscales: Ome.Multiscale[] } {
|
|
464
|
-
return "image-label" in attrs && isMultiscales(attrs);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export function isOmeMultiscales(attrs: zarr.Attributes): attrs is { omero: Ome.Omero; multiscales: Ome.Multiscale[] } {
|
|
468
|
-
return "omero" in attrs && isMultiscales(attrs);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
export function isMultiscales(attrs: zarr.Attributes): attrs is { multiscales: Ome.Multiscale[] } {
|
|
472
|
-
return "multiscales" in attrs;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
export function isBioformats2rawlayout(attrs: zarr.Attributes): attrs is { multiscales: Ome.Bioformats2rawlayout } {
|
|
476
|
-
return "bioformats2raw.layout" in attrs;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Ensures an error matches expected type(s), otherwise rethrows.
|
|
481
|
-
*
|
|
482
|
-
* Unmatched errors bubble up, like Python's `except`. Narrows error types for
|
|
483
|
-
* type-safe property access.
|
|
484
|
-
*
|
|
485
|
-
* Usage
|
|
486
|
-
* @example
|
|
487
|
-
* ```ts
|
|
488
|
-
* class DatabaseError extends Error {}
|
|
489
|
-
* class NetworkError extends Error {}
|
|
490
|
-
*
|
|
491
|
-
* try {
|
|
492
|
-
* await db.query();
|
|
493
|
-
* } catch (error) {
|
|
494
|
-
* rethrowUnless(error, DatabaseError, NetworkError);
|
|
495
|
-
* error; // DatabaseError | NetworkError
|
|
496
|
-
* }
|
|
497
|
-
* ```
|
|
498
|
-
*
|
|
499
|
-
* @param error - The error to check
|
|
500
|
-
* @param ErrorClasses - Expected error type(s)
|
|
501
|
-
* @throws The original error if it doesn't match expected type(s)
|
|
502
|
-
*
|
|
503
|
-
* @copyright Trevor Manz 2025
|
|
504
|
-
* @license MIT
|
|
505
|
-
* @see {@link https://github.com/manzt/manzt/blob/4b04f2/utils/rethrow-unless.js}
|
|
506
|
-
*/
|
|
507
|
-
// biome-ignore lint/suspicious/noExplicitAny: Ok to use any for generic constraint
|
|
508
|
-
export function rethrowUnless<E extends ReadonlyArray<new (...args: any[]) => Error>>(
|
|
509
|
-
error: unknown,
|
|
510
|
-
...ErrorClasses: E
|
|
511
|
-
// biome-ignore lint/suspicious/noExplicitAny: Ok to use any for generic constraint
|
|
512
|
-
): asserts error is E[number] extends new (...args: any[]) => infer R ? R : never {
|
|
513
|
-
if (!ErrorClasses.some((ErrorClass) => error instanceof ErrorClass)) {
|
|
514
|
-
throw error;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
export function isGridLayerProps(
|
|
519
|
-
props: GridLayerProps | ImageLayerProps | MultiscaleImageLayerProps | LabelLayerProps,
|
|
520
|
-
): props is GridLayerProps {
|
|
521
|
-
return "loaders" in props && "rows" in props && "columns" in props;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
export function resolveLoaderFromLayerProps(
|
|
525
|
-
layerProps: GridLayerProps | ImageLayerProps | MultiscaleImageLayerProps | LabelLayerProps,
|
|
526
|
-
) {
|
|
527
|
-
return isGridLayerProps(layerProps) ? layerProps.loaders[0].loader : layerProps.loader;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Convert an array of coordinateTransformations objects to a 16-element
|
|
532
|
-
* plain JS array using Matrix4 linear algebra transformation functions.
|
|
533
|
-
*
|
|
534
|
-
* Adapted from Vitessce: https://github.com/vitessce/vitessce/blob/c267ebecab1824dae68d6f2640a6c5ce7250efbb/packages/utils/spatial-utils/src/spatial.js#L403-L524
|
|
535
|
-
*
|
|
536
|
-
* @param coordinateTransformations List of objects matching the OME-NGFF v0.4 coordinateTransformations spec.
|
|
537
|
-
* @param axes - Axes in OME-NGFF v0.4 format
|
|
538
|
-
*
|
|
539
|
-
* @returns Array of 16 numbers representing the Matrix4.
|
|
540
|
-
*/
|
|
541
|
-
export function coordinateTransformationsToMatrix(multiscales: Array<Ome.Multiscale>) {
|
|
542
|
-
let mat = new Matrix4().identity();
|
|
543
|
-
const axes = getNgffAxes(multiscales);
|
|
544
|
-
const coordinateTransformations = multiscales[0].datasets[0]?.coordinateTransformations;
|
|
545
|
-
const xyzIndices = ["x", "y", "z"].map((name) =>
|
|
546
|
-
axes.findIndex((axisObj) => axisObj.type === "space" && axisObj.name === name),
|
|
547
|
-
);
|
|
548
|
-
|
|
549
|
-
// Apply each transformation sequentially and in order according to the OME-NGFF v0.4 spec.
|
|
550
|
-
// Reference: https://ngff.openmicroscopy.org/0.4/#trafo-md
|
|
551
|
-
for (const transform of coordinateTransformations ?? []) {
|
|
552
|
-
if (transform.type === "translation") {
|
|
553
|
-
const { translation: axisOrderedTranslation } = transform;
|
|
554
|
-
if (axisOrderedTranslation.length !== axes.length) {
|
|
555
|
-
throw new Error("Length of translation array was expected to match length of axes.");
|
|
556
|
-
}
|
|
557
|
-
const defaultValue = 0;
|
|
558
|
-
// Get the translation values for [x, y, z].
|
|
559
|
-
const xyzTranslation = xyzIndices.map((axisIndex) =>
|
|
560
|
-
axisIndex >= 0 ? axisOrderedTranslation[axisIndex] : defaultValue,
|
|
561
|
-
);
|
|
562
|
-
const nextMat = new Matrix4().translate(xyzTranslation);
|
|
563
|
-
mat = mat.multiplyLeft(nextMat);
|
|
564
|
-
}
|
|
565
|
-
if (transform.type === "scale") {
|
|
566
|
-
const { scale: axisOrderedScale } = transform;
|
|
567
|
-
// Add in z dimension needed for Matrix4 scale API.
|
|
568
|
-
if (axisOrderedScale.length !== axes.length) {
|
|
569
|
-
throw new Error("Length of scale array was expected to match length of axes.");
|
|
570
|
-
}
|
|
571
|
-
const defaultValue = 1;
|
|
572
|
-
// Get the scale values for [x, y, z].
|
|
573
|
-
const xyzScale = xyzIndices.map((axisIndex) => (axisIndex >= 0 ? axisOrderedScale[axisIndex] : defaultValue));
|
|
574
|
-
const nextMat = new Matrix4().scale(xyzScale);
|
|
575
|
-
mat = mat.multiplyLeft(nextMat);
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
return mat;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Builds N-tuples of elements from the given N arrays with matching indices,
|
|
584
|
-
* stopping when the smallest array's end is reached.
|
|
585
|
-
*/
|
|
586
|
-
export function zip<T extends unknown[]>(...arrays: { [K in keyof T]: ReadonlyArray<T[K]> }): T[] {
|
|
587
|
-
const minLength = arrays.reduce((minLength, arr) => Math.min(arr.length, minLength), Number.POSITIVE_INFINITY);
|
|
588
|
-
const result: T[] = new Array(minLength);
|
|
589
|
-
for (let i = 0; i < minLength; i += 1) {
|
|
590
|
-
const arr = arrays.map((it) => it[i]);
|
|
591
|
-
result[i] = arr as T;
|
|
592
|
-
}
|
|
593
|
-
return result;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
export function getLayerSize({ props }: VizarrLayer) {
|
|
597
|
-
const loader = resolveLoaderFromLayerProps(props);
|
|
598
|
-
const [baseResolution, maxZoom] = Array.isArray(loader) ? [loader[0], loader.length] : [loader, 0];
|
|
599
|
-
const interleaved = isInterleaved(baseResolution.shape);
|
|
600
|
-
let [height, width] = baseResolution.shape.slice(interleaved ? -3 : -2);
|
|
601
|
-
if (isGridLayerProps(props)) {
|
|
602
|
-
// TODO: Don't hardcode spacer size. Probably best to inspect the deck.gl Layers rather than
|
|
603
|
-
// the Layer Props.
|
|
604
|
-
const spacer = 5;
|
|
605
|
-
height = (height + spacer) * props.rows;
|
|
606
|
-
width = (width + spacer) * props.columns;
|
|
607
|
-
}
|
|
608
|
-
return { height, width, maxZoom };
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
export function getPhysicalSizes(attrs: zarr.Attributes) {
|
|
612
|
-
if (isMultiscales(attrs)) {
|
|
613
|
-
const axes = getNgffAxes(attrs.multiscales);
|
|
614
|
-
const ct = coordinateTransformationsToMatrix(attrs.multiscales);
|
|
615
|
-
const matrixIndices = {
|
|
616
|
-
x: 0,
|
|
617
|
-
y: 5,
|
|
618
|
-
z: 10,
|
|
619
|
-
};
|
|
620
|
-
const physicalSizes = axes
|
|
621
|
-
.filter((a) => a.type === "space")
|
|
622
|
-
.reduce((acc: { [key: string]: viv.PhysicalSize }, { name, unit }: Ome.Axis) => {
|
|
623
|
-
acc[name] = { size: ct[matrixIndices[name as keyof typeof matrixIndices]], unit: unit ?? "" };
|
|
624
|
-
return acc;
|
|
625
|
-
}, {});
|
|
626
|
-
// @TODO: get t size from multiscales.coordinateTransformations if axis is present
|
|
627
|
-
return physicalSizes;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// @TODO: remove after updating deck.gl
|
|
632
|
-
// From deck.gl geo-layers tileset-2d utils
|
|
633
|
-
export function transformBox(bbox: number[], modelMatrix: Matrix4): number[] {
|
|
634
|
-
const transformedCoords = [
|
|
635
|
-
// top-left
|
|
636
|
-
modelMatrix.transformAsPoint([bbox[0], bbox[1]]),
|
|
637
|
-
// top-right
|
|
638
|
-
modelMatrix.transformAsPoint([bbox[2], bbox[1]]),
|
|
639
|
-
// bottom-left
|
|
640
|
-
modelMatrix.transformAsPoint([bbox[0], bbox[3]]),
|
|
641
|
-
// bottom-right
|
|
642
|
-
modelMatrix.transformAsPoint([bbox[2], bbox[3]]),
|
|
643
|
-
];
|
|
644
|
-
const transformedBox = [
|
|
645
|
-
// Minimum x coord
|
|
646
|
-
Math.min(...transformedCoords.map((i) => i[0])),
|
|
647
|
-
// Minimum y coord
|
|
648
|
-
Math.min(...transformedCoords.map((i) => i[1])),
|
|
649
|
-
// Max x coord
|
|
650
|
-
Math.max(...transformedCoords.map((i) => i[0])),
|
|
651
|
-
// Max y coord
|
|
652
|
-
Math.max(...transformedCoords.map((i) => i[1])),
|
|
653
|
-
];
|
|
654
|
-
return transformedBox;
|
|
655
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "esnext",
|
|
4
|
-
"target": "esnext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"noEmit": true,
|
|
7
|
-
"strict": true,
|
|
8
|
-
"skipLibCheck": true,
|
|
9
|
-
"forceConsistentCasingInFileNames": true,
|
|
10
|
-
"verbatimModuleSyntax": true,
|
|
11
|
-
"jsx": "preserve",
|
|
12
|
-
"types": ["vite/client"]
|
|
13
|
-
}
|
|
14
|
-
}
|