@vitessce/spatial-utils 2.0.3 → 3.0.1

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/src/spatial.js CHANGED
@@ -1,9 +1,9 @@
1
- import isEqual from 'lodash/isEqual';
1
+ import { isEqual } from 'lodash-es';
2
2
  import { Matrix4 } from 'math.gl';
3
3
  import { divide, compare, unit } from 'mathjs';
4
4
  import { VIEWER_PALETTE } from '@vitessce/utils';
5
5
  import { viv } from '@vitessce/gl';
6
- import { getMultiSelectionStats } from './layer-controller';
6
+ import { getMultiSelectionStats } from './layer-controller.js';
7
7
 
8
8
  export function square(x, y, r) {
9
9
  return [[x, y + r], [x + r, y], [x, y - r], [x - r, y]];
@@ -117,6 +117,11 @@ function getMetaWithTransformMatrices(imageMeta, imageLoaders) {
117
117
  }
118
118
  // Find the ratio of the sizes to get the scaling factor.
119
119
  const scale = sizes.map((i, k) => divide(i, minPhysicalSize[k]));
120
+ // sizes are special objects with own equals method - see `unit` in declaration
121
+ if (!sizes[0].equals(sizes[1])) {
122
+ // Handle scaling in the Y direction for non-square pixels
123
+ scale[1] = divide(sizes[1], sizes[0]);
124
+ }
120
125
  // Add in z dimension needed for Matrix4 scale API.
121
126
  if (!scale[2]) {
122
127
  scale[2] = 1;
@@ -316,3 +321,98 @@ export async function initializeRasterLayersAndChannels(
316
321
  const autoImageLayerDefs = await Promise.all(autoImageLayerDefPromises);
317
322
  return [autoImageLayerDefs, nextImageLoaders, nextImageMetaAndLayers];
318
323
  }
324
+
325
+ function getDefaultAxisType(name) {
326
+ if (name === 't') return 'time';
327
+ if (name === 'c') return 'channel';
328
+ return 'space';
329
+ }
330
+
331
+ // Reference: https://github.com/hms-dbmi/vizarr/blob/ade9d5d71bbedc8c20357c5310557ff9a0c59ac5/src/utils.ts#LL114-L142C2
332
+ export function getNgffAxes(firstMultiscalesAxes) {
333
+ // Returns axes in the latest v0.4+ format.
334
+ // defaults for v0.1 & v0.2
335
+ const defaultAxes = [
336
+ { type: 'time', name: 't' },
337
+ { type: 'channel', name: 'c' },
338
+ { type: 'space', name: 'z' },
339
+ { type: 'space', name: 'y' },
340
+ { type: 'space', name: 'x' },
341
+ ];
342
+ let axes = defaultAxes;
343
+ // v0.3 & v0.4+
344
+ if (firstMultiscalesAxes) {
345
+ axes = firstMultiscalesAxes.map((axis) => {
346
+ // axis may be string 'x' (v0.3) or object
347
+ if (typeof axis === 'string') {
348
+ return { name: axis, type: getDefaultAxisType(axis) };
349
+ }
350
+ const { name, type } = axis;
351
+ return { name, type: type ?? getDefaultAxisType(name) };
352
+ });
353
+ }
354
+ return axes;
355
+ }
356
+
357
+ export function getNgffAxesForTiff(dimOrder) {
358
+ return dimOrder
359
+ .toLowerCase()
360
+ .split('')
361
+ .map(name => ({
362
+ name,
363
+ type: getDefaultAxisType(name),
364
+ }));
365
+ }
366
+
367
+ /**
368
+ * Convert an array of coordinateTransformations objects to a 16-element
369
+ * plain JS array using Matrix4 linear algebra transformation functions.
370
+ * @param {object[]} coordinateTransformations List of objects matching the
371
+ * OME-NGFF v0.4 coordinateTransformations spec.
372
+ * @param {object[]} axes Axes in OME-NGFF v0.4 format, objects
373
+ * with { type, name }.
374
+ * @returns {Array<number>} Array of 16 numbers representing the Matrix4.
375
+ */
376
+ export function coordinateTransformationsToMatrix(coordinateTransformations, axes) {
377
+ let mat = (new Matrix4()).identity();
378
+ if (coordinateTransformations && axes) {
379
+ // Get the indices of the objects corresponding to X, Y, and Z from `axes`.
380
+ const xyzIndices = ['x', 'y', 'z'].map(name => axes.findIndex(axisObj => axisObj.type === 'space' && axisObj.name === name));
381
+ // Apply each transformation sequentially and in order according to the OME-NGFF v0.4 spec.
382
+ // Reference: https://ngff.openmicroscopy.org/0.4/#trafo-md
383
+ coordinateTransformations.forEach((transform) => {
384
+ if (transform.type === 'translation') {
385
+ const { translation: axisOrderedTranslation } = transform;
386
+ if (axisOrderedTranslation.length !== axes.length) {
387
+ throw new Error('Length of translation array was expected to match length of axes.');
388
+ }
389
+ const defaultValue = 0;
390
+ // Get the translation values for [x, y, z].
391
+ const xyzTranslation = xyzIndices.map(axisIndex => (
392
+ axisIndex >= 0
393
+ ? axisOrderedTranslation[axisIndex]
394
+ : defaultValue
395
+ ));
396
+ const nextMat = (new Matrix4()).translate(xyzTranslation);
397
+ mat = mat.multiplyLeft(nextMat);
398
+ }
399
+ if (transform.type === 'scale') {
400
+ const { scale: axisOrderedScale } = transform;
401
+ // Add in z dimension needed for Matrix4 scale API.
402
+ if (axisOrderedScale.length !== axes.length) {
403
+ throw new Error('Length of scale array was expected to match length of axes.');
404
+ }
405
+ const defaultValue = 1;
406
+ // Get the scale values for [x, y, z].
407
+ const xyzScale = xyzIndices.map(axisIndex => (
408
+ axisIndex >= 0
409
+ ? axisOrderedScale[axisIndex]
410
+ : defaultValue
411
+ ));
412
+ const nextMat = (new Matrix4()).scale(xyzScale);
413
+ mat = mat.multiplyLeft(nextMat);
414
+ }
415
+ });
416
+ }
417
+ return mat;
418
+ }
@@ -1,4 +1,13 @@
1
- import { square } from './spatial';
1
+ import { describe, it, expect } from 'vitest';
2
+ import { square, coordinateTransformationsToMatrix } from './spatial.js';
3
+
4
+ const defaultAxes = [
5
+ { type: 'time', name: 't' },
6
+ { type: 'channel', name: 'c' },
7
+ { type: 'space', name: 'z' },
8
+ { type: 'space', name: 'y' },
9
+ { type: 'space', name: 'x' },
10
+ ];
2
11
 
3
12
  describe('Spatial.js', () => {
4
13
  describe('square()', () => {
@@ -6,4 +15,32 @@ describe('Spatial.js', () => {
6
15
  expect(square(0, 0, 50)).toEqual([[0, 50], [50, 0], [0, -50], [-50, 0]]);
7
16
  });
8
17
  });
18
+ describe('coordinateTransformationsToMatrix', () => {
19
+ it('returns an Array instance', () => {
20
+ const transformations = [
21
+ {
22
+ type: 'translation',
23
+ translation: [0, 0, 0, 1, 1],
24
+ },
25
+ {
26
+ type: 'scale',
27
+ scale: [1, 1, 0.5, 0.5, 0.5],
28
+ },
29
+ ];
30
+ expect(coordinateTransformationsToMatrix(transformations, defaultAxes)).toEqual([
31
+ 0.5, 0, 0, 0,
32
+ 0, 0.5, 0, 0,
33
+ 0, 0, 0.5, 0,
34
+ 0.5, 0.5, 0, 1,
35
+ ]);
36
+ });
37
+ it('returns Identity matrix when coordinateTransformations is null', () => {
38
+ expect(coordinateTransformationsToMatrix(null, defaultAxes)).toEqual([
39
+ 1, 0, 0, 0,
40
+ 0, 1, 0, 0,
41
+ 0, 0, 1, 0,
42
+ 0, 0, 0, 1,
43
+ ]);
44
+ });
45
+ });
9
46
  });