All files / metadata/src/displayset defaultDisplaySetSplitRules.ts

7.69% Statements 2/26
0% Branches 0/29
0% Functions 0/16
8% Lines 2/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148            128x               128x                                                                                                                                                                                                                                                                          
import { isEcgInstance } from './isEcgInstance';
import { isImageInstance } from './isImageInstance';
import { isVideoInstance } from './isVideoInstance';
import { isWsiInstance } from './isWsiInstance';
import type { SplitRule } from './types';
 
const VOLUME_MODALITIES = new Set(['CT', 'MR', 'PT', 'NM']);
 
/**
 * Default display-set split rules (OHIF PR parity + video, ECG, volume3d).
 * Rules are evaluated in order; the first match wins. Each rule's `viewportTypes`
 * (index 0 = preferred) is applied to the resulting display set, so only rules
 * that add *other* attributes need a `customAttributes` callback.
 */
export const defaultDisplaySetSplitRules: SplitRule[] = [
  {
    id: 'video',
    viewportTypes: ['video'],
    matches: (instance) => isVideoInstance(instance),
    groupBy: ['SOPInstanceUID'],
  },
 
  {
    id: 'ecg',
    viewportTypes: ['ecg'],
    matches: (instance) => isEcgInstance(instance),
    groupBy: ['SOPInstanceUID'],
  },
 
  {
    id: 'wholeslide',
    viewportTypes: ['wholeslide'],
    // All microscopy levels of a series form a single whole-slide display set.
    matches: (instance) => isWsiInstance(instance),
    groupBy: ['SeriesInstanceUID'],
  },
 
  {
    id: 'singleImageModality',
    viewportTypes: ['stack'],
    matches: (instance) =>
      ['CR', 'DX', 'MG'].includes(instance.Modality ?? '') &&
      isImageInstance(instance) &&
      !!instance.Rows,
    // Split within the series by a coarse size bucket so differently-sized
    // images (e.g. MG views) become separate stacks. `SeriesInstanceUID` keeps
    // the bucket series-scoped (the entry point is per-series, but this stays
    // correct if ever fed multiple series). The `/64` rounding is a deliberately
    // fuzzy bucket and can straddle a boundary (480 -> 8, 544 -> 9).
    groupBy: [
      'SeriesInstanceUID',
      (instance) =>
        `rows=${Math.round(Number(instance.Rows) / 64)}&cols=${Math.round(Number(instance.Columns) / 64)}`,
    ],
  },
 
  {
    id: 'multiFrame',
    viewportTypes: ['stack'],
    // Assumes a homogeneous series: samples instances[0] for NumberOfFrames /
    // SliceLocation. The `SliceLocation !== undefined` guard mirrors OHIF - a
    // multi-frame object without a slice location is not treated as a clip here
    // and falls through to the volume/stack rules below.
    series: ({ instances }) => {
      const first = instances[0];
      return {
        isMultiFrame:
          Number(first?.NumberOfFrames) > 1 &&
          first?.SliceLocation !== undefined,
      };
    },
    matches: (instance, { series }) =>
      !!series.isMultiFrame && isImageInstance(instance) && !!instance.Rows,
    groupBy: ['SeriesInstanceUID', 'InstanceNumber'],
    customAttributes: ({ isMultiFrame }, options) => {
      // NumberOfFrames is frequently naturalized as a string (e.g. '30'); coerce
      // it so numImageFrames matches its declared `number` type.
      const numberOfFrames = options.instances[0]?.NumberOfFrames;
      return {
        isClip: true,
        numImageFrames:
          numberOfFrames === undefined ? undefined : Number(numberOfFrames),
        splitNumber: options.splitNumber,
        isMultiFrame,
      };
    },
  },
 
  /**
   * This rule splits off images containing an undefined bValue from the
   * 4d b-value containing images, since the undefined versions are not
   * part of the 4d data set.  That prevents applying incorrect 4d rendering
   * to the 3d portion.
   */
  {
    id: 'mixedDimensionalityBValue',
    // Both subgroups are multi-slice MR; default them to MPR (volume) like any
    // volumetric MR series. This rule matches before `volume3d`, so listing
    // stack first here would regress the defined-b-value subgroup to a stack.
    viewportTypes: ['volume', 'volume3d', 'stack'],
    // Gates on instances[0].Modality (assumes a homogeneous-modality series),
    // then scans all instances for the mix of defined/undefined b-values.
    series: ({ instances }) => {
      const [instance] = instances;
      if (!instance || instance.Modality !== 'MR') {
        return { mixedBValue: false };
      }
      const hasBValue = instances.some((i) => i.DiffusionBValue !== undefined);
      const missingBValue = instances.some(
        (i) => i.DiffusionBValue === undefined
      );
      return { mixedBValue: hasBValue && missingBValue };
    },
    matches: (instance, { series }) =>
      !!series.mixedBValue && isImageInstance(instance) && !!instance.Rows,
    groupBy: [
      'SeriesInstanceUID',
      (instance) => instance.DiffusionBValue === undefined,
    ],
  },
 
  {
    id: 'volume3d',
    // Default volumetric series to MPR (volume); 3D is an extra allowed type.
    viewportTypes: ['volume', 'volume3d', 'stack'],
    // Assumes a homogeneous series: samples instances[0].Modality. A
    // heterogeneous series (e.g. a localizer first, then a volume) can be
    // misflagged - add a dedicated split rule (as `mixedDimensionalityBValue`
    // does for DWI) when a specific mix must be separated.
    series: ({ instances }) => {
      const modality = instances[0]?.Modality;
      return {
        supportsVolume3d:
          !!modality && VOLUME_MODALITIES.has(modality) && instances.length > 1,
      };
    },
    matches: (instance, { series }) =>
      !!series.supportsVolume3d && isImageInstance(instance) && !!instance.Rows,
    groupBy: ['SeriesInstanceUID'],
  },
 
  {
    id: 'defaultImageRule',
    viewportTypes: ['stack', 'volume', 'volume3d'],
    matches: (instance) => isImageInstance(instance) && !!instance.Rows,
  },
];