@vitessce/spatial-utils 3.6.11 → 3.6.12

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.
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { DAG } from './dag.js';
3
+
4
+
5
+ describe('DAG', () => {
6
+ describe('findPath', () => {
7
+ it('Can find a path from start to end.', () => {
8
+ // Example usage:
9
+ const edges = [
10
+ { from: 'A', to: 'B', attributes: { some_key: 'a_to_b' } },
11
+ { from: 'B', to: 'C', attributes: { some_key: 'b_to_c', hello: 'world' } },
12
+ { from: 'A', to: 'D', attributes: { some_key: 'a_to_d' } },
13
+ { from: 'D', to: 'E', attributes: { some_key: 'd_to_e' } },
14
+ { from: 'E', to: 'C', attributes: { some_key: 'e_to_c' } },
15
+ ];
16
+
17
+ const dag = new DAG(edges);
18
+ const path = dag.findPath('A', 'C');
19
+ expect(path).toEqual([
20
+ { from: 'A', to: 'B', attributes: { some_key: 'a_to_b' } },
21
+ { from: 'B', to: 'C', attributes: { some_key: 'b_to_c', hello: 'world' } },
22
+ ]);
23
+
24
+ const path2 = dag.findPath('D', 'C');
25
+ expect(path2).toEqual([
26
+ { from: 'D', to: 'E', attributes: { some_key: 'd_to_e' } },
27
+ { from: 'E', to: 'C', attributes: { some_key: 'e_to_c' } },
28
+ ]);
29
+
30
+ const path3 = dag.findPath('D', 'B');
31
+ expect(path3).toEqual(null);
32
+ });
33
+ });
34
+ });
package/src/index.js CHANGED
@@ -16,6 +16,7 @@ export {
16
16
  getNgffAxes,
17
17
  getNgffAxesForTiff,
18
18
  coordinateTransformationsToMatrix,
19
+ coordinateTransformationsToMatrixForSpatialData,
19
20
  physicalSizeToMatrix,
20
21
  hexToRgb,
21
22
  getStatsForResolution,
package/src/spatial.js CHANGED
@@ -5,6 +5,8 @@ import { VIEWER_PALETTE } from '@vitessce/utils';
5
5
  import { GLOBAL_LABELS, DEFAULT_RASTER_LAYER_PROPS } from '@vitessce/constants-internal';
6
6
  import { log } from '@vitessce/globals';
7
7
  import { getMultiSelectionStats } from './layer-controller.js';
8
+ import { DAG } from './dag.js';
9
+
8
10
  /**
9
11
  * Get a representative PixelSource from a loader object returned from
10
12
  * the Vitessce imaging loaders
@@ -494,8 +496,8 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
494
496
  }
495
497
  } else if (spatialOutputAxes.length === 2) { // 2D case
496
498
  const nextMat = (new Matrix4()).fromArray([
497
- ...filteredAffine[0],
498
- ...filteredAffine[1],
499
+ filteredAffine[0][0], filteredAffine[0][1], 0, filteredAffine[0][2],
500
+ filteredAffine[1][0], filteredAffine[1][1], 0, filteredAffine[1][2],
499
501
  0, 0, 1, 0,
500
502
  0, 0, 0, 1,
501
503
  ]);
@@ -507,6 +509,10 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
507
509
  const swapMat = (new Matrix4()).fromArray(swapMatNested.flat());
508
510
  mat = mat.multiplyLeft(swapMat);
509
511
  }
512
+ // TODO: is the transpose needed? why?
513
+ // TODO: is transpose only needed when axis-swapping?
514
+ // TODO: is it also needed in the 3D case? Why was it not needed before?
515
+ mat = mat.transpose();
510
516
  } else {
511
517
  throw new Error('Affine transformation must have 2 or 3 rows.');
512
518
  }
@@ -548,6 +554,9 @@ export function coordinateTransformationsToMatrix(coordinateTransformations, axe
548
554
  }
549
555
  });
550
556
  }
557
+ if (mat.some(value => Number.isNaN(value))) {
558
+ throw new Error('Matrix contains NaN values');
559
+ }
551
560
  return mat;
552
561
  }
553
562
 
@@ -575,6 +584,14 @@ export function normalizeAxes(axes) {
575
584
  });
576
585
  }
577
586
 
587
+ export function getIntrinsicCoordinateSystem(normAxes) {
588
+ // Get the intrinsic coordinate system from the axes.
589
+ // TODO: is this correct?
590
+ // See https://github.com/scverse/spatialdata/pull/963
591
+ return normAxes.map(axis => axis.name).join('');
592
+ }
593
+
594
+
578
595
  /**
579
596
  * Normalize coordinate transformations to the OME-NGFF v0.4 format,
580
597
  * despite potentially being in the new format proposed in
@@ -596,36 +613,17 @@ export function normalizeCoordinateTransformations(coordinateTransformations, da
596
613
  // This is a new-style coordinate transformation.
597
614
  // (As proposed in https://github.com/ome/ngff/pull/138)
598
615
  const { type } = transform;
599
- if (type === 'translation') {
600
- return {
601
- type,
602
- translation: transform.translation,
603
- };
604
- }
605
- if (type === 'scale') {
606
- return {
607
- type,
608
- scale: transform.scale,
609
- };
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
- }
621
- if (type === 'identity') {
622
- return { type };
623
- }
624
616
  if (type === 'sequence') {
625
- return normalizeCoordinateTransformations(transform.transformations, datasets);
617
+ // Recursion to flatten the sequence of transformations.
618
+ return normalizeCoordinateTransformations(transform.transformations, null);
619
+ } if (type === 'affine' || type === 'translation' || type === 'scale' || type === 'identity') {
620
+ // TODO: normalize the transform.input/transform.output if they are missing?
621
+ // Old transformations did not specify them for translation/scale/identity.
622
+ // But we are currently only using them for affine.
623
+ return transform;
626
624
  }
627
-
628
- log.warn(`Coordinate transformation type "${type}" is not supported.`);
625
+ // If the type is not recognized, log an error.
626
+ log.error(`Coordinate transformation type "${type}" is not supported.`);
629
627
  }
630
628
  // Assume it was already an old-style (NGFF v0.4) coordinate transformation.
631
629
  return transform;
@@ -647,3 +645,60 @@ export function normalizeCoordinateTransformations(coordinateTransformations, da
647
645
  }
648
646
  return result;
649
647
  }
648
+
649
+ /**
650
+ * Convert coordinate transformations to a 4x4 matrix for SpatialData
651
+ * spatial elements. This function is intended to be used with SpatialData
652
+ * objects which have named coordinate systems. This function should
653
+ * be compatible with any SpatialElement (shapes, points, labels, images).
654
+ * @param {{
655
+ * axe?: (string[]|{ name: string, type?: string, unit?: string }[]),
656
+ * coordinateTransformations?: object[],
657
+ * datasets?: object[]
658
+ * }} ngffMetadata For images/labels, this is multiscales[0] from .zattrs.
659
+ * For shapes/points, this is { axes, coordinateTransformations } from .zattrs.
660
+ * @param {string} targetCoordinateSystem A target coordinate system name.
661
+ * @returns {Matrix4} A 4x4 transformation matrix.
662
+ * This can later be multiplied with other matrices if needed.
663
+ */
664
+ export function coordinateTransformationsToMatrixForSpatialData(
665
+ ngffMetadata, targetCoordinateSystem,
666
+ ) {
667
+ const {
668
+ datasets,
669
+ coordinateTransformations,
670
+ axes,
671
+ } = ngffMetadata; // In the image/labels case, this is multiscales[0].
672
+
673
+ // In case axes is an array of strings like ['x', 'y', 'z'],
674
+ // we convert them to a consistent object format.
675
+ const normAxes = normalizeAxes(axes);
676
+ // We know the target coordinate system, but not yet the starting (intrinsic) one.
677
+ const intrinsicCoordinateSystem = getIntrinsicCoordinateSystem(normAxes);
678
+ // Create a DAG of coordinate transformations.
679
+ const edges = coordinateTransformations.map(ct => ({
680
+ from: ct.input.name,
681
+ to: ct.output.name,
682
+ attributes: ct,
683
+ }));
684
+ const dag = new DAG(edges);
685
+ const ctPath = dag.findPath(intrinsicCoordinateSystem, targetCoordinateSystem);
686
+ if (!ctPath) {
687
+ // For example, this can be the case if the targetCoordinateSystem
688
+ // is "global" but the set of coordinate transformations does not
689
+ // include one with output.name "global".
690
+ if (!coordinateTransformations.find(
691
+ ct => ct.output.name === targetCoordinateSystem,
692
+ )) {
693
+ throw new Error(`No coordinate transformation found with output.name "${targetCoordinateSystem}".`);
694
+ }
695
+ throw new Error(`No path found from "${intrinsicCoordinateSystem}" to "${targetCoordinateSystem}".`);
696
+ }
697
+ const filteredTransformations = ctPath.map(ctEdge => ctEdge.attributes);
698
+ // Normalize the coordinate transformations to OME-NGFF v0.4 format.
699
+ const normalizedTransformations = normalizeCoordinateTransformations(
700
+ filteredTransformations, datasets,
701
+ );
702
+ // Convert the coordinate transformations to a 4x4 matrix.
703
+ return coordinateTransformationsToMatrix(normalizedTransformations, normAxes);
704
+ }