@vitessce/spatial-utils 3.6.7 → 3.6.9

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.
@@ -1,5 +1,5 @@
1
1
  import { i as inflate_1 } from "./pako.esm-SxljTded.js";
2
- import { B as BaseDecoder } from "./index-C6jVF_qi.js";
2
+ import { B as BaseDecoder } from "./index-RMBcVNTZ.js";
3
3
  class DeflateDecoder extends BaseDecoder {
4
4
  decodeBlock(buffer) {
5
5
  return inflate_1(new Uint8Array(buffer)).buffer;
@@ -120365,22 +120365,22 @@ function addDecoder(cases, importFn) {
120365
120365
  }
120366
120366
  cases.forEach((c) => registry$2.set(c, importFn));
120367
120367
  }
120368
- addDecoder([void 0, 1], () => import("./raw-DwHPYVQy.js").then((m) => m.default));
120369
- addDecoder(5, () => import("./lzw-Ckpw-zOq.js").then((m) => m.default));
120368
+ addDecoder([void 0, 1], () => import("./raw-B-RRtlid.js").then((m) => m.default));
120369
+ addDecoder(5, () => import("./lzw-CS9qpOLK.js").then((m) => m.default));
120370
120370
  addDecoder(6, () => {
120371
120371
  throw new Error("old style JPEG compression is not supported.");
120372
120372
  });
120373
- addDecoder(7, () => import("./jpeg-Bv9e5TsR.js").then((m) => m.default));
120374
- addDecoder([8, 32946], () => import("./deflate-Cumj_9kB.js").then((m) => m.default));
120375
- addDecoder(32773, () => import("./packbits-KgFJoulg.js").then((m) => m.default));
120373
+ addDecoder(7, () => import("./jpeg-Dp762_Sy.js").then((m) => m.default));
120374
+ addDecoder([8, 32946], () => import("./deflate-D50eseYg.js").then((m) => m.default));
120375
+ addDecoder(32773, () => import("./packbits-smBOUAP8.js").then((m) => m.default));
120376
120376
  addDecoder(
120377
120377
  34887,
120378
- () => import("./lerc-CN8oy9fE.js").then(async (m) => {
120378
+ () => import("./lerc-R19D-u1d.js").then(async (m) => {
120379
120379
  await m.zstd.init();
120380
120380
  return m;
120381
120381
  }).then((m) => m.default)
120382
120382
  );
120383
- addDecoder(50001, () => import("./webimage-BeD_xi6W.js").then((m) => m.default));
120383
+ addDecoder(50001, () => import("./webimage-CZyj01Qe.js").then((m) => m.default));
120384
120384
  function decodeRowAcc(row, stride) {
120385
120385
  let length2 = row.length - stride;
120386
120386
  let offset = 0;
@@ -137995,11 +137995,80 @@ function getNgffAxesForTiff(dimOrder) {
137995
137995
  type: getDefaultAxisType(name2)
137996
137996
  }));
137997
137997
  }
137998
+ function getSwapAxesMatrix(inputAxes, outputAxes) {
137999
+ const size2 = inputAxes.length;
138000
+ if (!(size2 === 2 || size2 === 3)) {
138001
+ throw new Error(`Expected input and output axes to have 2 or 3 elements, got ${size2}.`);
138002
+ }
138003
+ const matrix2 = [
138004
+ [0, 0, 0, 0],
138005
+ [0, 0, 0, 0],
138006
+ [0, 0, size2 === 2 ? 1 : 0, 0],
138007
+ // In 2D, leave Z unchanged.
138008
+ [0, 0, 0, 1]
138009
+ ];
138010
+ for (let outRow = 0; outRow < size2; outRow++) {
138011
+ const axis = outputAxes[outRow];
138012
+ const inCol = inputAxes.indexOf(axis);
138013
+ if (inCol === -1) {
138014
+ throw new Error(`Axis "${axis}" not found in inputAxes`);
138015
+ }
138016
+ matrix2[outRow][inCol] = 1;
138017
+ }
138018
+ return matrix2;
138019
+ }
137998
138020
  function coordinateTransformationsToMatrix(coordinateTransformations, axes) {
137999
138021
  let mat = new Matrix4().identity();
138000
138022
  if (coordinateTransformations && axes) {
138001
138023
  const xyzIndices = ["x", "y", "z"].map((name2) => axes.findIndex((axisObj) => axisObj.type === "space" && axisObj.name === name2));
138002
138024
  coordinateTransformations.forEach((transform2) => {
138025
+ if (transform2.type === "affine") {
138026
+ const { affine, input, output } = transform2;
138027
+ const excludeEntries = input.axes.map((axis, i2) => [axis, i2]).filter(([axis, i2]) => axis.type !== "space").map(([axis, i2]) => i2);
138028
+ const filteredAffine = affine.filter((_, i2) => !excludeEntries.includes(i2)).map((row) => row.filter((_, i2) => !excludeEntries.includes(i2)));
138029
+ const spatialInputAxes = input.axes.filter((axis) => axis.type === "space");
138030
+ const spatialOutputAxes = output.axes.filter((axis) => axis.type === "space");
138031
+ const inputAxisNames = spatialInputAxes.map((axis) => axis.name);
138032
+ const outputAxisNames = spatialOutputAxes.map((axis) => axis.name);
138033
+ if (spatialInputAxes.length === 3) {
138034
+ const nextMat = new Matrix4().fromArray([
138035
+ ...filteredAffine[0],
138036
+ ...filteredAffine[1],
138037
+ ...filteredAffine[2],
138038
+ 0,
138039
+ 0,
138040
+ 0,
138041
+ 1
138042
+ ]);
138043
+ mat = mat.multiplyLeft(nextMat);
138044
+ if (!isEqual$1(inputAxisNames, outputAxisNames)) {
138045
+ const swapMatNested = getSwapAxesMatrix(inputAxisNames, outputAxisNames);
138046
+ const swapMat = new Matrix4().fromArray(swapMatNested.flat());
138047
+ mat = mat.multiplyLeft(swapMat);
138048
+ }
138049
+ } else if (spatialOutputAxes.length === 2) {
138050
+ const nextMat = new Matrix4().fromArray([
138051
+ ...filteredAffine[0],
138052
+ ...filteredAffine[1],
138053
+ 0,
138054
+ 0,
138055
+ 1,
138056
+ 0,
138057
+ 0,
138058
+ 0,
138059
+ 0,
138060
+ 1
138061
+ ]);
138062
+ mat = mat.multiplyLeft(nextMat);
138063
+ if (!isEqual$1(inputAxisNames, outputAxisNames)) {
138064
+ const swapMatNested = getSwapAxesMatrix(inputAxisNames, outputAxisNames);
138065
+ const swapMat = new Matrix4().fromArray(swapMatNested.flat());
138066
+ mat = mat.multiplyLeft(swapMat);
138067
+ }
138068
+ } else {
138069
+ throw new Error("Affine transformation must have 2 or 3 rows.");
138070
+ }
138071
+ }
138003
138072
  if (transform2.type === "translation") {
138004
138073
  const { translation: axisOrderedTranslation } = transform2;
138005
138074
  if (axisOrderedTranslation.length !== axes.length) {
@@ -138059,6 +138128,16 @@ function normalizeCoordinateTransformations(coordinateTransformations, datasets)
138059
138128
  scale: transform2.scale
138060
138129
  };
138061
138130
  }
138131
+ if (type === "affine") {
138132
+ return {
138133
+ type,
138134
+ affine: transform2.affine,
138135
+ // Affine transformations can potentially swap axes,
138136
+ // so we need to include the input and output axes.
138137
+ input: transform2.input,
138138
+ output: transform2.output
138139
+ };
138140
+ }
138062
138141
  if (type === "identity") {
138063
138142
  return { type };
138064
138143
  }
@@ -139564,6 +139643,24 @@ function createZarrArrayAdapter(arr) {
139564
139643
  }
139565
139644
  });
139566
139645
  }
139646
+ class VitessceError extends Error {
139647
+ constructor(message) {
139648
+ super(message);
139649
+ this.name = "VitessceError";
139650
+ }
139651
+ }
139652
+ class DataLoaderError extends VitessceError {
139653
+ constructor(message) {
139654
+ super(message);
139655
+ this.name = "DataLoaderError";
139656
+ }
139657
+ }
139658
+ class ZarrNodeNotFoundError extends DataLoaderError {
139659
+ constructor(message) {
139660
+ super(message);
139661
+ this.name = "ZarrNodeNotFoundError";
139662
+ }
139663
+ }
139567
139664
  function prevPowerOf2(x) {
139568
139665
  return 2 ** Math.floor(Math.log2(x));
139569
139666
  }
@@ -139581,7 +139678,15 @@ function isAxis(axisOrLabel) {
139581
139678
  return typeof axisOrLabel[0] !== "string";
139582
139679
  }
139583
139680
  async function loadMultiscales(root2) {
139584
- const rootAttrs = (await open(root2)).attrs;
139681
+ let rootAttrs;
139682
+ try {
139683
+ rootAttrs = (await open(root2)).attrs;
139684
+ } catch (e2) {
139685
+ if (e2.name === "NodeNotFoundError") {
139686
+ throw new ZarrNodeNotFoundError(root2.path);
139687
+ }
139688
+ throw e2;
139689
+ }
139585
139690
  let paths = ["0"];
139586
139691
  let labels = ["t", "c", "z", "y", "x"];
139587
139692
  if ("multiscales" in rootAttrs) {
@@ -139594,6 +139699,8 @@ async function loadMultiscales(root2) {
139594
139699
  labels = axes;
139595
139700
  }
139596
139701
  }
139702
+ } else {
139703
+ throw new Error("Expected the Zarr store to contain OME-NGFF multiscales metadata.");
139597
139704
  }
139598
139705
  const data = paths.map((path) => open(root2.resolve(path), { kind: "array" }));
139599
139706
  return {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D, e, a, b, c, d, F, G, Z, C, r, n, E, A, y, l, m, z, x, h, q, v, o, i, f, j, k, u, t, s, p, w } from "./index-C6jVF_qi.js";
1
+ import { D, e, a, b, c, d, F, G, Z, C, r, n, E, A, y, l, m, z, x, h, q, v, o, i, f, j, k, u, t, s, p, w } from "./index-RMBcVNTZ.js";
2
2
  export {
3
3
  D as DEFAULT_CELLS_LAYER,
4
4
  e as DEFAULT_LAYER_TYPE_ORDERING,
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-C6jVF_qi.js";
1
+ import { B as BaseDecoder } from "./index-RMBcVNTZ.js";
2
2
  const dctZigZag = new Int32Array([
3
3
  0,
4
4
  1,
@@ -1,5 +1,5 @@
1
1
  import { i as inflate_1 } from "./pako.esm-SxljTded.js";
2
- import { g as getDefaultExportFromCjs, B as BaseDecoder } from "./index-C6jVF_qi.js";
2
+ import { g as getDefaultExportFromCjs, B as BaseDecoder } from "./index-RMBcVNTZ.js";
3
3
  const LercParameters = {
4
4
  AddCompression: 1
5
5
  };
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-C6jVF_qi.js";
1
+ import { B as BaseDecoder } from "./index-RMBcVNTZ.js";
2
2
  const MIN_BITS = 9;
3
3
  const CLEAR_CODE = 256;
4
4
  const EOI_CODE = 257;
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-C6jVF_qi.js";
1
+ import { B as BaseDecoder } from "./index-RMBcVNTZ.js";
2
2
  class PackbitsDecoder extends BaseDecoder {
3
3
  decodeBlock(buffer) {
4
4
  const dataView = new DataView(buffer);
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-C6jVF_qi.js";
1
+ import { B as BaseDecoder } from "./index-RMBcVNTZ.js";
2
2
  class RawDecoder extends BaseDecoder {
3
3
  decodeBlock(buffer) {
4
4
  return buffer;
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-C6jVF_qi.js";
1
+ import { B as BaseDecoder } from "./index-RMBcVNTZ.js";
2
2
  class WebImageDecoder extends BaseDecoder {
3
3
  constructor() {
4
4
  super();
@@ -1 +1 @@
1
- {"version":3,"file":"load-ome-zarr.d.ts","sourceRoot":"","sources":["../src/load-ome-zarr.js"],"names":[],"mappings":"AAkBA,gDAMC;AAiDD;;;GAaC;AA7BD;IACE,kDASC;IADC,qBAAwB;CAE3B;oBApEmB,cAAc"}
1
+ {"version":3,"file":"load-ome-zarr.d.ts","sourceRoot":"","sources":["../src/load-ome-zarr.js"],"names":[],"mappings":"AAmBA,gDAMC;AA6DD;;;GAaC;AA7BD;IACE,kDASC;IADC,qBAAwB;CAE3B;oBAjFmB,cAAc"}
@@ -1,6 +1,7 @@
1
1
  import { viv } from '@vitessce/gl';
2
2
  import { open as zarrOpen } from 'zarrita';
3
3
  import { createZarrArrayAdapter } from '@vitessce/zarr-utils';
4
+ import { ZarrNodeNotFoundError } from '@vitessce/error';
4
5
  function prevPowerOf2(x) {
5
6
  return 2 ** Math.floor(Math.log2(x));
6
7
  }
@@ -24,7 +25,17 @@ function isAxis(axisOrLabel) {
24
25
  return typeof axisOrLabel[0] !== 'string';
25
26
  }
26
27
  async function loadMultiscales(root) {
27
- const rootAttrs = (await zarrOpen(root)).attrs;
28
+ let rootAttrs;
29
+ try {
30
+ rootAttrs = (await zarrOpen(root)).attrs;
31
+ }
32
+ catch (e) {
33
+ if (e.name === 'NodeNotFoundError') {
34
+ throw new ZarrNodeNotFoundError(root.path);
35
+ }
36
+ // Re-throw the error if it is not a NodeNotFoundError.
37
+ throw e;
38
+ }
28
39
  let paths = ['0'];
29
40
  // Default axes used for v0.1 and v0.2.
30
41
  let labels = ['t', 'c', 'z', 'y', 'x'];
@@ -40,6 +51,10 @@ async function loadMultiscales(root) {
40
51
  }
41
52
  }
42
53
  }
54
+ else {
55
+ // TODO: Relax this after https://github.com/vitessce/vitessce/issues/2199
56
+ throw new Error('Expected the Zarr store to contain OME-NGFF multiscales metadata.');
57
+ }
43
58
  const data = paths
44
59
  .map(path => zarrOpen(root.resolve(path), { kind: 'array' }));
45
60
  return {
@@ -41,6 +41,14 @@ export function getNgffAxes(firstMultiscalesAxes: any): {
41
41
  name: string;
42
42
  }[];
43
43
  export function getNgffAxesForTiff(dimOrder: any): any;
44
+ /**
45
+ * Get a 4x4 matrix that swaps axes.
46
+ * TODO: add unit tests.
47
+ * @param {("x"|"y"|"z")[]} inputAxes
48
+ * @param {("x"|"y"|"z")[]} outputAxes
49
+ * @return {number[][]} 4x4 matrix that swaps axes.
50
+ */
51
+ export function getSwapAxesMatrix(inputAxes: ("x" | "y" | "z")[], outputAxes: ("x" | "y" | "z")[]): number[][];
44
52
  /**
45
53
  * Convert an array of coordinateTransformations objects to a 16-element
46
54
  * plain JS array using Matrix4 linear algebra transformation functions.
@@ -1 +1 @@
1
- {"version":3,"file":"spatial.d.ts","sourceRoot":"","sources":["../src/spatial.js"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,4CAJW,MAAM,SACN,MAAM,GAAG,SAAS,GAChB,MAAM,CAMlB;AAoCD;;;;GAIG;AACH,8BAHW,MAAM,YACN,KAAK,GAAC,IAAI,WAiBpB;AAID,sHAmCC;AAiHD;;GAEG;AACH,qCAFW,KAAK,CAAE,MAAM,CAAC,WAKxB;AAED;;;;;;GAMG;AACH,gDALW,MAAM,eAEJ,MAAM,EAAE,CAuCpB;AAED;;;;;;;;;GASG;AACH,gEAJW,MAAM,EAAE,sBAER,CAAC,MAAM,EAAE,GAAC,IAAI,CAAC,+CAuEzB;AASD;;;IAuBC;AAED,uDAQC;AAED;;;;;;;;GAQG;AACH,6EANW,MAAM,EAAE,GAAC,SAAS,QAElB,MAAM,EAAE,GAAC,SAAS,GAEhB,OAAO,CA4CnB;AAED,6CAOC;AAMD,8CASC;AAED;;;;;;;;;GASG;AACH,8EANW,MAAM,EAAE,GAAC,SAAS,YAGlB,MAAM,EAAE,GACN,MAAM,EAAE,CAoDpB;AAvgBM;;;;;EAYN;AAEM,yEAkBN;wBAnDuB,SAAS"}
1
+ {"version":3,"file":"spatial.d.ts","sourceRoot":"","sources":["../src/spatial.js"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,4CAJW,MAAM,SACN,MAAM,GAAG,SAAS,GAChB,MAAM,CAMlB;AAoCD;;;;GAIG;AACH,8BAHW,MAAM,YACN,KAAK,GAAC,IAAI,WAiBpB;AAID,sHAmCC;AAiHD;;GAEG;AACH,qCAFW,KAAK,CAAE,MAAM,CAAC,WAKxB;AAED;;;;;;GAMG;AACH,gDALW,MAAM,eAEJ,MAAM,EAAE,CAuCpB;AAED;;;;;;;;;GASG;AACH,gEAJW,MAAM,EAAE,sBAER,CAAC,MAAM,EAAE,GAAC,IAAI,CAAC,+CAuEzB;AASD;;;IAuBC;AAED,uDAQC;AAED;;;;;;GAMG;AACH,6CAJW,CAAC,GAAG,GAAC,GAAG,GAAC,GAAG,CAAC,EAAE,cACf,CAAC,GAAG,GAAC,GAAG,GAAC,GAAG,CAAC,EAAE,GACd,MAAM,EAAE,EAAE,CA6BrB;AAED;;;;;;;;GAQG;AACH,6EANW,MAAM,EAAE,GAAC,SAAS,QAElB,MAAM,EAAE,GAAC,SAAS,GAEhB,OAAO,CA0GnB;AAED,6CAOC;AAMD,8CASC;AAED;;;;;;;;;GASG;AACH,8EANW,MAAM,EAAE,GAAC,SAAS,YAGlB,MAAM,EAAE,GACN,MAAM,EAAE,CA+DpB;AApnBM;;;;;EAYN;AAEM,yEAkBN;wBAnDuB,SAAS"}
@@ -356,6 +356,38 @@ export function getNgffAxesForTiff(dimOrder) {
356
356
  type: getDefaultAxisType(name),
357
357
  }));
358
358
  }
359
+ /**
360
+ * Get a 4x4 matrix that swaps axes.
361
+ * TODO: add unit tests.
362
+ * @param {("x"|"y"|"z")[]} inputAxes
363
+ * @param {("x"|"y"|"z")[]} outputAxes
364
+ * @return {number[][]} 4x4 matrix that swaps axes.
365
+ */
366
+ export function getSwapAxesMatrix(inputAxes, outputAxes) {
367
+ const size = inputAxes.length;
368
+ if (!(size === 2 || size === 3)) {
369
+ throw new Error(`Expected input and output axes to have 2 or 3 elements, got ${size}.`);
370
+ }
371
+ // Initialize a 4x4 matrix.
372
+ const matrix = [
373
+ [0, 0, 0, 0],
374
+ [0, 0, 0, 0],
375
+ [0, 0, (size === 2 ? 1 : 0), 0], // In 2D, leave Z unchanged.
376
+ [0, 0, 0, 1],
377
+ ];
378
+ // Fill linear transformation (top-left 3x3)
379
+ // Iterate over output rows.
380
+ for (let outRow = 0; outRow < size; outRow++) {
381
+ // Get the axis name for the current output row.
382
+ const axis = outputAxes[outRow];
383
+ const inCol = inputAxes.indexOf(axis);
384
+ if (inCol === -1) {
385
+ throw new Error(`Axis "${axis}" not found in inputAxes`);
386
+ }
387
+ matrix[outRow][inCol] = 1;
388
+ }
389
+ return matrix;
390
+ }
359
391
  /**
360
392
  * Convert an array of coordinateTransformations objects to a 16-element
361
393
  * plain JS array using Matrix4 linear algebra transformation functions.
@@ -373,6 +405,58 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
373
405
  // Apply each transformation sequentially and in order according to the OME-NGFF v0.4 spec.
374
406
  // Reference: https://ngff.openmicroscopy.org/0.4/#trafo-md
375
407
  coordinateTransformations.forEach((transform) => {
408
+ if (transform.type === 'affine') {
409
+ const { affine, input, output } = transform;
410
+ // This is needed since axes can include "c" for example.
411
+ const excludeEntries = input.axes
412
+ .map((axis, i) => ([axis, i]))
413
+ // eslint-disable-next-line no-unused-vars
414
+ .filter(([axis, i]) => axis.type !== 'space')
415
+ // eslint-disable-next-line no-unused-vars
416
+ .map(([axis, i]) => i);
417
+ // We only want to keep the affine transformation entries for the spatial axes.
418
+ const filteredAffine = affine
419
+ .filter((_, i) => !excludeEntries.includes(i))
420
+ .map(row => row.filter((_, i) => !excludeEntries.includes(i)));
421
+ // TODO: if there was some affine transformation for the "c" axis, warn/error.
422
+ const spatialInputAxes = input.axes.filter(axis => axis.type === 'space');
423
+ const spatialOutputAxes = output.axes.filter(axis => axis.type === 'space');
424
+ const inputAxisNames = spatialInputAxes.map(axis => axis.name);
425
+ const outputAxisNames = spatialOutputAxes.map(axis => axis.name);
426
+ if (spatialInputAxes.length === 3) { // 3D case
427
+ const nextMat = (new Matrix4()).fromArray([
428
+ ...filteredAffine[0],
429
+ ...filteredAffine[1],
430
+ ...filteredAffine[2],
431
+ 0, 0, 0, 1,
432
+ ]);
433
+ mat = mat.multiplyLeft(nextMat);
434
+ if (!isEqual(inputAxisNames, outputAxisNames)) {
435
+ // Handle 3D axis swapping.
436
+ const swapMatNested = getSwapAxesMatrix(inputAxisNames, outputAxisNames);
437
+ const swapMat = (new Matrix4()).fromArray(swapMatNested.flat());
438
+ mat = mat.multiplyLeft(swapMat);
439
+ }
440
+ }
441
+ else if (spatialOutputAxes.length === 2) { // 2D case
442
+ const nextMat = (new Matrix4()).fromArray([
443
+ ...filteredAffine[0],
444
+ ...filteredAffine[1],
445
+ 0, 0, 1, 0,
446
+ 0, 0, 0, 1,
447
+ ]);
448
+ mat = mat.multiplyLeft(nextMat);
449
+ if (!isEqual(inputAxisNames, outputAxisNames)) {
450
+ // Handle 2D axis swapping.
451
+ const swapMatNested = getSwapAxesMatrix(inputAxisNames, outputAxisNames);
452
+ const swapMat = (new Matrix4()).fromArray(swapMatNested.flat());
453
+ mat = mat.multiplyLeft(swapMat);
454
+ }
455
+ }
456
+ else {
457
+ throw new Error('Affine transformation must have 2 or 3 rows.');
458
+ }
459
+ }
376
460
  if (transform.type === 'translation') {
377
461
  const { translation: axisOrderedTranslation } = transform;
378
462
  if (axisOrderedTranslation.length !== axes.length) {
@@ -385,6 +469,7 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
385
469
  : defaultValue));
386
470
  const nextMat = (new Matrix4()).translate(xyzTranslation);
387
471
  mat = mat.multiplyLeft(nextMat);
472
+ // TODO: error if the user tries to use a translation on the "c" axis.
388
473
  }
389
474
  if (transform.type === 'scale') {
390
475
  const { scale: axisOrderedScale } = transform;
@@ -399,6 +484,7 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
399
484
  : defaultValue));
400
485
  const nextMat = (new Matrix4()).scale(xyzScale);
401
486
  mat = mat.multiplyLeft(nextMat);
487
+ // TODO: error if the user tries to use a scale on the "c" axis.
402
488
  }
403
489
  });
404
490
  }
@@ -457,6 +543,16 @@ export function normalizeCoordinateTransformations(coordinateTransformations, da
457
543
  scale: transform.scale,
458
544
  };
459
545
  }
546
+ if (type === 'affine') {
547
+ return {
548
+ type,
549
+ affine: transform.affine,
550
+ // Affine transformations can potentially swap axes,
551
+ // so we need to include the input and output axes.
552
+ input: transform.input,
553
+ output: transform.output,
554
+ };
555
+ }
460
556
  if (type === 'identity') {
461
557
  return { type };
462
558
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { coordinateTransformationsToMatrix, normalizeCoordinateTransformations, } from './spatial.js';
2
+ import { coordinateTransformationsToMatrix, normalizeCoordinateTransformations, getSwapAxesMatrix, } from './spatial.js';
3
3
  const defaultAxes = [
4
4
  { type: 'time', name: 't' },
5
5
  { type: 'channel', name: 'c' },
@@ -35,6 +35,125 @@ describe('Spatial.js', () => {
35
35
  0, 0, 0, 1,
36
36
  ]);
37
37
  });
38
+ it('swap axes works for 2D', () => {
39
+ const inputAxes = ['x', 'y'];
40
+ const outputAxes = ['y', 'x'];
41
+ const swapMatrix = getSwapAxesMatrix(inputAxes, outputAxes);
42
+ expect(swapMatrix.flat()).toEqual([
43
+ 0, 1, 0, 0,
44
+ 1, 0, 0, 0,
45
+ 0, 0, 1, 0,
46
+ 0, 0, 0, 1,
47
+ ]);
48
+ });
49
+ it('swap axes works for 3D', () => {
50
+ const inputAxes = ['x', 'y', 'z'];
51
+ const outputAxes = ['z', 'y', 'x'];
52
+ const swapMatrix = getSwapAxesMatrix(inputAxes, outputAxes);
53
+ expect(swapMatrix.flat()).toEqual([
54
+ 0, 0, 1, 0,
55
+ 0, 1, 0, 0,
56
+ 1, 0, 0, 0,
57
+ 0, 0, 0, 1,
58
+ ]);
59
+ });
60
+ it('swap axes works for 3D, different order', () => {
61
+ const inputAxes = ['x', 'z', 'y'];
62
+ const outputAxes = ['x', 'y', 'z'];
63
+ const swapMatrix = getSwapAxesMatrix(inputAxes, outputAxes);
64
+ expect(swapMatrix.flat()).toEqual([
65
+ 1, 0, 0, 0,
66
+ 0, 0, 1, 0,
67
+ 0, 1, 0, 0,
68
+ 0, 0, 0, 1,
69
+ ]);
70
+ });
71
+ it('supports 3D affine transformation, for coordinate system with C axis', () => {
72
+ const coordinateTransformations = [
73
+ { type: 'scale', scale: [1, 1, 1, 1] },
74
+ {
75
+ type: 'affine',
76
+ affine: [
77
+ [1, 0, 0, 0, 0],
78
+ [0, 0, 0, 1, 0],
79
+ [0, 0, 1, 0, 0],
80
+ [0, 13.76, 0, 0, -13.76],
81
+ ],
82
+ input: {
83
+ name: 'czyx',
84
+ axes: [
85
+ { type: 'channel', name: 'c' },
86
+ { type: 'space', name: 'x' },
87
+ { type: 'space', name: 'y' },
88
+ { type: 'space', name: 'z' },
89
+ ],
90
+ },
91
+ output: {
92
+ name: 'global',
93
+ axes: [
94
+ // Notice: swap-axes (CXYZ to CZYX).
95
+ { type: 'channel', name: 'c' },
96
+ { type: 'space', name: 'z' },
97
+ { type: 'space', name: 'y' },
98
+ { type: 'space', name: 'x' },
99
+ ],
100
+ },
101
+ },
102
+ ];
103
+ const axes = [
104
+ { type: 'channel', name: 'c' },
105
+ { type: 'space', name: 'x' },
106
+ { type: 'space', name: 'y' },
107
+ { type: 'space', name: 'z' },
108
+ ];
109
+ expect(coordinateTransformationsToMatrix(coordinateTransformations, axes)).toEqual([
110
+ 1, 0, 0, 0,
111
+ 0, 1, 0, 0,
112
+ 0, 0, 13.76, -13.76, // scale in Z by 13.76; translate in Z by -13.76 after scaling.
113
+ 0, 0, 0, 1,
114
+ ]);
115
+ });
116
+ it('supports 3D affine transformation, for coordinate system without C axis', () => {
117
+ const coordinateTransformations = [
118
+ { type: 'scale', scale: [1, 1, 1] },
119
+ {
120
+ type: 'affine',
121
+ affine: [
122
+ [0, 0, 1, 0],
123
+ [0, 1, 0, 0],
124
+ [13.76, 0, 0, -13.76],
125
+ ],
126
+ input: {
127
+ name: 'zyx',
128
+ axes: [
129
+ { type: 'space', name: 'z' },
130
+ { type: 'space', name: 'y' },
131
+ { type: 'space', name: 'x' },
132
+ ],
133
+ },
134
+ output: {
135
+ name: 'global',
136
+ axes: [
137
+ // Notice: swap-axes (ZYX to XYZ).
138
+ { type: 'space', name: 'x' },
139
+ { type: 'space', name: 'y' },
140
+ { type: 'space', name: 'z' },
141
+ ],
142
+ },
143
+ },
144
+ ];
145
+ const axes = [
146
+ { type: 'space', name: 'z' },
147
+ { type: 'space', name: 'y' },
148
+ { type: 'space', name: 'x' },
149
+ ];
150
+ expect(coordinateTransformationsToMatrix(coordinateTransformations, axes)).toEqual([
151
+ 1, 0, 0, 0,
152
+ 0, 1, 0, 0,
153
+ 0, 0, 13.76, -13.76, // scale in Z by 13.76; translate in Z by -13.76 after scaling.
154
+ 0, 0, 0, 1,
155
+ ]);
156
+ });
38
157
  });
39
158
  describe('normalizeCoordinateTransformations', () => {
40
159
  it('does nothing for OME-NGFF v0.4 input', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/spatial-utils",
3
- "version": "3.6.7",
3
+ "version": "3.6.9",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -20,11 +20,12 @@
20
20
  "zarrita": "0.5.2",
21
21
  "math.gl": "^3.5.6",
22
22
  "mathjs": "^9.2.0",
23
- "@vitessce/gl": "3.6.7",
24
- "@vitessce/utils": "3.6.7",
25
- "@vitessce/zarr-utils": "3.6.7",
26
- "@vitessce/constants-internal": "3.6.7",
27
- "@vitessce/globals": "3.6.7"
23
+ "@vitessce/gl": "3.6.9",
24
+ "@vitessce/utils": "3.6.9",
25
+ "@vitessce/zarr-utils": "3.6.9",
26
+ "@vitessce/constants-internal": "3.6.9",
27
+ "@vitessce/globals": "3.6.9",
28
+ "@vitessce/error": "3.6.9"
28
29
  },
29
30
  "scripts": {
30
31
  "bundle": "pnpm exec vite build -c ../../../scripts/vite.config.js",
@@ -1,6 +1,7 @@
1
1
  import { viv } from '@vitessce/gl';
2
2
  import { open as zarrOpen } from 'zarrita';
3
3
  import { createZarrArrayAdapter } from '@vitessce/zarr-utils';
4
+ import { ZarrNodeNotFoundError } from '@vitessce/error';
4
5
 
5
6
  function prevPowerOf2(x) {
6
7
  return 2 ** Math.floor(Math.log2(x));
@@ -29,7 +30,16 @@ function isAxis(axisOrLabel) {
29
30
  }
30
31
 
31
32
  async function loadMultiscales(root) {
32
- const rootAttrs = (await zarrOpen(root)).attrs;
33
+ let rootAttrs;
34
+ try {
35
+ rootAttrs = (await zarrOpen(root)).attrs;
36
+ } catch (e) {
37
+ if (e.name === 'NodeNotFoundError') {
38
+ throw new ZarrNodeNotFoundError(root.path);
39
+ }
40
+ // Re-throw the error if it is not a NodeNotFoundError.
41
+ throw e;
42
+ }
33
43
 
34
44
  let paths = ['0'];
35
45
  // Default axes used for v0.1 and v0.2.
@@ -44,6 +54,9 @@ async function loadMultiscales(root) {
44
54
  labels = axes;
45
55
  }
46
56
  }
57
+ } else {
58
+ // TODO: Relax this after https://github.com/vitessce/vitessce/issues/2199
59
+ throw new Error('Expected the Zarr store to contain OME-NGFF multiscales metadata.');
47
60
  }
48
61
 
49
62
  const data = paths
package/src/spatial.js CHANGED
@@ -400,6 +400,42 @@ export function getNgffAxesForTiff(dimOrder) {
400
400
  }));
401
401
  }
402
402
 
403
+ /**
404
+ * Get a 4x4 matrix that swaps axes.
405
+ * TODO: add unit tests.
406
+ * @param {("x"|"y"|"z")[]} inputAxes
407
+ * @param {("x"|"y"|"z")[]} outputAxes
408
+ * @return {number[][]} 4x4 matrix that swaps axes.
409
+ */
410
+ export function getSwapAxesMatrix(inputAxes, outputAxes) {
411
+ const size = inputAxes.length;
412
+ if (!(size === 2 || size === 3)) {
413
+ throw new Error(`Expected input and output axes to have 2 or 3 elements, got ${size}.`);
414
+ }
415
+
416
+ // Initialize a 4x4 matrix.
417
+ const matrix = [
418
+ [0, 0, 0, 0],
419
+ [0, 0, 0, 0],
420
+ [0, 0, (size === 2 ? 1 : 0), 0], // In 2D, leave Z unchanged.
421
+ [0, 0, 0, 1],
422
+ ];
423
+
424
+ // Fill linear transformation (top-left 3x3)
425
+ // Iterate over output rows.
426
+ for (let outRow = 0; outRow < size; outRow++) {
427
+ // Get the axis name for the current output row.
428
+ const axis = outputAxes[outRow];
429
+ const inCol = inputAxes.indexOf(axis);
430
+ if (inCol === -1) {
431
+ throw new Error(`Axis "${axis}" not found in inputAxes`);
432
+ }
433
+ matrix[outRow][inCol] = 1;
434
+ }
435
+
436
+ return matrix;
437
+ }
438
+
403
439
  /**
404
440
  * Convert an array of coordinateTransformations objects to a 16-element
405
441
  * plain JS array using Matrix4 linear algebra transformation functions.
@@ -417,6 +453,64 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
417
453
  // Apply each transformation sequentially and in order according to the OME-NGFF v0.4 spec.
418
454
  // Reference: https://ngff.openmicroscopy.org/0.4/#trafo-md
419
455
  coordinateTransformations.forEach((transform) => {
456
+ if (transform.type === 'affine') {
457
+ const { affine, input, output } = transform;
458
+
459
+ // This is needed since axes can include "c" for example.
460
+ const excludeEntries = input.axes
461
+ .map((axis, i) => ([axis, i]))
462
+ // eslint-disable-next-line no-unused-vars
463
+ .filter(([axis, i]) => axis.type !== 'space')
464
+ // eslint-disable-next-line no-unused-vars
465
+ .map(([axis, i]) => i);
466
+
467
+ // We only want to keep the affine transformation entries for the spatial axes.
468
+ const filteredAffine = affine
469
+ .filter((_, i) => !excludeEntries.includes(i))
470
+ .map(row => row.filter((_, i) => !excludeEntries.includes(i)));
471
+
472
+ // TODO: if there was some affine transformation for the "c" axis, warn/error.
473
+
474
+ const spatialInputAxes = input.axes.filter(axis => axis.type === 'space');
475
+ const spatialOutputAxes = output.axes.filter(axis => axis.type === 'space');
476
+
477
+ const inputAxisNames = spatialInputAxes.map(axis => axis.name);
478
+ const outputAxisNames = spatialOutputAxes.map(axis => axis.name);
479
+
480
+ if (spatialInputAxes.length === 3) { // 3D case
481
+ const nextMat = (new Matrix4()).fromArray([
482
+ ...filteredAffine[0],
483
+ ...filteredAffine[1],
484
+ ...filteredAffine[2],
485
+ 0, 0, 0, 1,
486
+ ]);
487
+ mat = mat.multiplyLeft(nextMat);
488
+
489
+ if (!isEqual(inputAxisNames, outputAxisNames)) {
490
+ // Handle 3D axis swapping.
491
+ const swapMatNested = getSwapAxesMatrix(inputAxisNames, outputAxisNames);
492
+ const swapMat = (new Matrix4()).fromArray(swapMatNested.flat());
493
+ mat = mat.multiplyLeft(swapMat);
494
+ }
495
+ } else if (spatialOutputAxes.length === 2) { // 2D case
496
+ const nextMat = (new Matrix4()).fromArray([
497
+ ...filteredAffine[0],
498
+ ...filteredAffine[1],
499
+ 0, 0, 1, 0,
500
+ 0, 0, 0, 1,
501
+ ]);
502
+ mat = mat.multiplyLeft(nextMat);
503
+
504
+ if (!isEqual(inputAxisNames, outputAxisNames)) {
505
+ // Handle 2D axis swapping.
506
+ const swapMatNested = getSwapAxesMatrix(inputAxisNames, outputAxisNames);
507
+ const swapMat = (new Matrix4()).fromArray(swapMatNested.flat());
508
+ mat = mat.multiplyLeft(swapMat);
509
+ }
510
+ } else {
511
+ throw new Error('Affine transformation must have 2 or 3 rows.');
512
+ }
513
+ }
420
514
  if (transform.type === 'translation') {
421
515
  const { translation: axisOrderedTranslation } = transform;
422
516
  if (axisOrderedTranslation.length !== axes.length) {
@@ -431,6 +525,8 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
431
525
  ));
432
526
  const nextMat = (new Matrix4()).translate(xyzTranslation);
433
527
  mat = mat.multiplyLeft(nextMat);
528
+
529
+ // TODO: error if the user tries to use a translation on the "c" axis.
434
530
  }
435
531
  if (transform.type === 'scale') {
436
532
  const { scale: axisOrderedScale } = transform;
@@ -447,6 +543,8 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
447
543
  ));
448
544
  const nextMat = (new Matrix4()).scale(xyzScale);
449
545
  mat = mat.multiplyLeft(nextMat);
546
+
547
+ // TODO: error if the user tries to use a scale on the "c" axis.
450
548
  }
451
549
  });
452
550
  }
@@ -510,12 +608,23 @@ export function normalizeCoordinateTransformations(coordinateTransformations, da
510
608
  scale: transform.scale,
511
609
  };
512
610
  }
611
+ if (type === 'affine') {
612
+ return {
613
+ type,
614
+ affine: transform.affine,
615
+ // Affine transformations can potentially swap axes,
616
+ // so we need to include the input and output axes.
617
+ input: transform.input,
618
+ output: transform.output,
619
+ };
620
+ }
513
621
  if (type === 'identity') {
514
622
  return { type };
515
623
  }
516
624
  if (type === 'sequence') {
517
625
  return normalizeCoordinateTransformations(transform.transformations, datasets);
518
626
  }
627
+
519
628
  log.warn(`Coordinate transformation type "${type}" is not supported.`);
520
629
  }
521
630
  // Assume it was already an old-style (NGFF v0.4) coordinate transformation.
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
2
2
  import {
3
3
  coordinateTransformationsToMatrix,
4
4
  normalizeCoordinateTransformations,
5
+ getSwapAxesMatrix,
5
6
  } from './spatial.js';
6
7
 
7
8
  const defaultAxes = [
@@ -40,6 +41,127 @@ describe('Spatial.js', () => {
40
41
  0, 0, 0, 1,
41
42
  ]);
42
43
  });
44
+
45
+ it('swap axes works for 2D', () => {
46
+ const inputAxes = ['x', 'y'];
47
+ const outputAxes = ['y', 'x'];
48
+ const swapMatrix = getSwapAxesMatrix(inputAxes, outputAxes);
49
+ expect(swapMatrix.flat()).toEqual([
50
+ 0, 1, 0, 0,
51
+ 1, 0, 0, 0,
52
+ 0, 0, 1, 0,
53
+ 0, 0, 0, 1,
54
+ ]);
55
+ });
56
+ it('swap axes works for 3D', () => {
57
+ const inputAxes = ['x', 'y', 'z'];
58
+ const outputAxes = ['z', 'y', 'x'];
59
+ const swapMatrix = getSwapAxesMatrix(inputAxes, outputAxes);
60
+ expect(swapMatrix.flat()).toEqual([
61
+ 0, 0, 1, 0,
62
+ 0, 1, 0, 0,
63
+ 1, 0, 0, 0,
64
+ 0, 0, 0, 1,
65
+ ]);
66
+ });
67
+ it('swap axes works for 3D, different order', () => {
68
+ const inputAxes = ['x', 'z', 'y'];
69
+ const outputAxes = ['x', 'y', 'z'];
70
+ const swapMatrix = getSwapAxesMatrix(inputAxes, outputAxes);
71
+ expect(swapMatrix.flat()).toEqual([
72
+ 1, 0, 0, 0,
73
+ 0, 0, 1, 0,
74
+ 0, 1, 0, 0,
75
+ 0, 0, 0, 1,
76
+ ]);
77
+ });
78
+
79
+ it('supports 3D affine transformation, for coordinate system with C axis', () => {
80
+ const coordinateTransformations = [
81
+ { type: 'scale', scale: [1, 1, 1, 1] },
82
+ {
83
+ type: 'affine',
84
+ affine: [
85
+ [1, 0, 0, 0, 0],
86
+ [0, 0, 0, 1, 0],
87
+ [0, 0, 1, 0, 0],
88
+ [0, 13.76, 0, 0, -13.76],
89
+ ],
90
+ input: {
91
+ name: 'czyx',
92
+ axes: [
93
+ { type: 'channel', name: 'c' },
94
+ { type: 'space', name: 'x' },
95
+ { type: 'space', name: 'y' },
96
+ { type: 'space', name: 'z' },
97
+ ],
98
+ },
99
+ output: {
100
+ name: 'global',
101
+ axes: [
102
+ // Notice: swap-axes (CXYZ to CZYX).
103
+ { type: 'channel', name: 'c' },
104
+ { type: 'space', name: 'z' },
105
+ { type: 'space', name: 'y' },
106
+ { type: 'space', name: 'x' },
107
+ ],
108
+ },
109
+ },
110
+ ];
111
+ const axes = [
112
+ { type: 'channel', name: 'c' },
113
+ { type: 'space', name: 'x' },
114
+ { type: 'space', name: 'y' },
115
+ { type: 'space', name: 'z' },
116
+ ];
117
+ expect(coordinateTransformationsToMatrix(coordinateTransformations, axes)).toEqual([
118
+ 1, 0, 0, 0,
119
+ 0, 1, 0, 0,
120
+ 0, 0, 13.76, -13.76, // scale in Z by 13.76; translate in Z by -13.76 after scaling.
121
+ 0, 0, 0, 1,
122
+ ]);
123
+ });
124
+ it('supports 3D affine transformation, for coordinate system without C axis', () => {
125
+ const coordinateTransformations = [
126
+ { type: 'scale', scale: [1, 1, 1] },
127
+ {
128
+ type: 'affine',
129
+ affine: [
130
+ [0, 0, 1, 0],
131
+ [0, 1, 0, 0],
132
+ [13.76, 0, 0, -13.76],
133
+ ],
134
+ input: {
135
+ name: 'zyx',
136
+ axes: [
137
+ { type: 'space', name: 'z' },
138
+ { type: 'space', name: 'y' },
139
+ { type: 'space', name: 'x' },
140
+ ],
141
+ },
142
+ output: {
143
+ name: 'global',
144
+ axes: [
145
+ // Notice: swap-axes (ZYX to XYZ).
146
+ { type: 'space', name: 'x' },
147
+ { type: 'space', name: 'y' },
148
+ { type: 'space', name: 'z' },
149
+ ],
150
+ },
151
+ },
152
+ ];
153
+ const axes = [
154
+ { type: 'space', name: 'z' },
155
+ { type: 'space', name: 'y' },
156
+ { type: 'space', name: 'x' },
157
+ ];
158
+ expect(coordinateTransformationsToMatrix(coordinateTransformations, axes)).toEqual([
159
+ 1, 0, 0, 0,
160
+ 0, 1, 0, 0,
161
+ 0, 0, 13.76, -13.76, // scale in Z by 13.76; translate in Z by -13.76 after scaling.
162
+ 0, 0, 0, 1,
163
+ ]);
164
+ });
43
165
  });
44
166
  describe('normalizeCoordinateTransformations', () => {
45
167
  it('does nothing for OME-NGFF v0.4 input', () => {