All files / extensions/default/src/displaySetSplitting makeImageSetDisplaySet.ts

90.76% Statements 59/65
79.31% Branches 23/29
90.9% Functions 10/11
90.47% Lines 57/63

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 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277            248x 248x   248x 248x                     248x 248x                       248x 941x       941x   941x       941x       941x       941x   941x                 941x 941x     941x   941x 7x 7x       7x                         373x 139x   7x   934x     941x                                                       941x 941x 941x       941x                           941x                         941x 941x                 941x 941x           941x 941x                                   941x 941x 941x                                               941x 941x           941x         941x 941x 941x   941x                                 941x                 941x 941x 941x 941x 7x 7x 7x 7x     941x        
import { utils, classes } from '@ohif/core';
import i18n from '@ohif/i18n';
import { metaData } from '@cornerstonejs/core';
import { id } from '../id';
import getDisplaySetMessages from '../getDisplaySetMessages';
 
const { sortStudyInstances, isDisplaySetReconstructable } = utils;
const { ImageSet } = classes;
 
const DEFAULT_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
const DYNAMIC_VOLUME_LOADER_SCHEME = 'cornerstoneStreamingDynamicImageVolume';
 
/**
 * The registered module name of the stack SOP class handler, and the resulting
 * fully-qualified `SOPClassHandlerId`.
 *
 * Both live here rather than in `getSopClassHandlerModule` because that module
 * imports this one (the reverse would be a cycle).  The handler imports the
 * name back for its registration so the id and the registration can never
 * disagree about what the handler is called.
 */
export const STACK_SOP_CLASS_HANDLER_NAME = 'stack';
export const STACK_SOP_CLASS_HANDLER_ID = `${id}.sopClassHandlerModule.${STACK_SOP_CLASS_HANDLER_NAME}`;
 
/**
 * The application context required to build an ImageSet display set.  This is
 * the same shape as the `appContext` handed to `getSopClassHandlerModule`.
 */
export type ImageSetFactoryContext = {
  servicesManager: AppTypes.ServicesManager;
  extensionManager: AppTypes.ExtensionManager;
  appConfig?: AppTypes.Config;
};
 
const isMultiFrame = instance => {
  return instance.NumberOfFrames > 1;
};
 
function getDynamicVolumeInfo(imageIds, context: ImageSetFactoryContext) {
  const { extensionManager } = context;
 
  Iif (!extensionManager) {
    throw new Error('extensionManager is not available');
  }
 
  const volumeLoaderUtility = extensionManager.getModuleEntry(
    '@ohif/extension-cornerstone.utilityModule.volumeLoader'
  );
 
  Iif (!volumeLoaderUtility?.exports) {
    throw new Error('The @ohif/extension-cornerstone volumeLoader utility module is not available');
  }
 
  const { getDynamicVolumeInfo: csGetDynamicVolumeInfo } = volumeLoaderUtility.exports;
 
  return csGetDynamicVolumeInfo(imageIds);
}
 
/**
 * Computes reconstructability / dynamic-volume information for a set of
 * instances.  Callers go through {@link applyImageListAttributes} so creation
 * and incremental updates share one computation.
 */
function getDisplaySetInfo(instances, imageIds, context: ImageSetFactoryContext) {
  const dynamicVolumeInfo = getDynamicVolumeInfo(imageIds, context);
  const { isDynamicVolume, timePoints } = dynamicVolumeInfo;
  let displaySetInfo;
 
  const { appConfig } = context;
 
  if (isDynamicVolume) {
    const timePoint = timePoints[0];
    const instancesMap = new Map();
 
    let firstTimePointInstances;
 
    Iif (instances[0].NumberOfFrames > 1 && timePoints.length > 1) {
      // Handle multiframe dynamic volumes. Local file frame imageIds do not
      // always resolve to a frame-level instance object, so keep resolved
      // entries and fall back to the source multiframe instance when needed.
      firstTimePointInstances = timePoints[0]
        .map(imageId => metaData.get('instance', imageId))
        .filter(Boolean);
 
      Iif (!firstTimePointInstances.length) {
        firstTimePointInstances = [instances[0]];
      }
    } else {
      // O(n) to convert it into a map and O(1) to find each instance
      instances.forEach(instance => instancesMap.set(instance.imageId, instance));
      firstTimePointInstances = timePoint.map(imageId => instancesMap.get(imageId)).filter(Boolean);
    }
    displaySetInfo = isDisplaySetReconstructable(firstTimePointInstances, appConfig);
  } else {
    displaySetInfo = isDisplaySetReconstructable(instances, appConfig);
  }
 
  return {
    isDynamicVolume,
    ...displaySetInfo,
    dynamicVolumeInfo,
  };
}
 
export type MakeImageSetDisplaySetOptions = {
  /**
   * Leave the instance order alone. Set by the split-rule path, which applies
   * OHIF's default order and the matched rule's comparator together, once.
   */
  skipSort?: boolean;
  /**
   * The `isReconstructable` that a split rule's attributes set. It replaces
   * the OHIF check, and the messages and the icon follow it.
   */
  isReconstructable?: boolean;
};
 
/**
 * The generic part of an OHIF display set: an ImageSet over the instances,
 * with the series identity (`displaySetInstanceUID`, the series UIDs, date,
 * number, description, `Modality`, `label`). Every display set of a split
 * rule starts here, whatever its handler.
 */
export function makeSeriesDisplaySet(instances) {
  // Need to sort the instances in order to get a consistent instance/thumbnail
  sortStudyInstances(instances);
  const instance = instances[0];
  const imageSet = new ImageSet(instances);
 
  // Series-level identity: fixed for the lifetime of the display set, because
  // adding more instances of the SAME series cannot change any of it.
  imageSet.setAttributes({
    displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
    SeriesDate: instance.SeriesDate,
    SeriesTime: instance.SeriesTime,
    SeriesInstanceUID: instance.SeriesInstanceUID,
    StudyInstanceUID: instance.StudyInstanceUID,
    SeriesNumber: instance.SeriesNumber || 0,
    SOPClassUID: instance.SOPClassUID,
    SeriesDescription: instance.SeriesDescription || '',
    Modality: instance.Modality,
    label:
      instance.SeriesDescription ||
      `${i18n.t('Series')} ${instance.SeriesNumber} - ${i18n.t(instance.Modality)}`,
  });
  return imageSet;
}
 
/**
 * The image part of a display set: the stack SOP class handler id, the
 * image-list attributes (`isReconstructable`, `messages`, ...), the thumbnail
 * and, unless `skipSort`, OHIF's default instance order.
 */
export function applyImageAttributes(
  imageSet,
  context: ImageSetFactoryContext,
  options: MakeImageSetDisplaySetOptions = {}
) {
  const instance = imageSet.images[0];
  imageSet.setAttributes({
    FrameRate: instance.FrameTime,
    SOPClassHandlerId: STACK_SOP_CLASS_HANDLER_ID,
    supportsWindowLevel: true,
    FrameOfReferenceUID: instance.FrameOfReferenceUID,
  });
 
  // Everything derived from the image list. `imageSet.sort` reads
  // `isReconstructable`, so this must run before it.
  const derived = applyImageListAttributes(imageSet, context, options.isReconstructable);
  applyThumbnailSrc(imageSet, context, derived);
 
  // The legacy SOP class handler path has no rule to consult, so OHIF's default
  // order is the whole answer and is applied here. The split-rule path passes
  // `skipSort` and orders once itself, folding the matched rule's comparator in
  // over this same default - see `applyInstanceOrder`.
  if (!options.skipSort) {
    imageSet.sort(context.servicesManager.services.customizationService);
  }
}
 
/**
 * Builds a stack/volume ImageSet display set carrying the full OHIF attribute
 * set (`label`, `supportsWindowLevel`, `FrameOfReferenceUID`,
 * `SOPClassHandlerId`, `isReconstructable`, `messages`, ...): the generic part
 * and the image part.
 *
 * The legacy stack SOP class handler and the split-rule path share these two
 * parts, so both paths produce identical image display sets.
 */
export function makeImageSetDisplaySet(
  instances,
  context: ImageSetFactoryContext,
  options: MakeImageSetDisplaySetOptions = {}
) {
  const imageSet = makeSeriesDisplaySet(instances);
  applyImageAttributes(imageSet, context, options);
  return imageSet;
}
 
/**
 * Applies every attribute derived from an ImageSet's current image list.
 *
 * Shared by the initial build and the split-rule `extendInstances` growth hook
 * so the two cannot drift: whatever is recomputed here is, by construction,
 * recomputed identically when instances arrive later.
 *
 * Series-level identity (`label`, `SeriesDescription`, `Modality`, ...) is
 * deliberately NOT touched — it cannot change by adding instances to the same
 * series, and a split rule's `customAttributes` may have overridden it (e.g.
 * the SCOUT example relabels `SeriesDescription`).
 *
 * @param ruleIsReconstructable - a value that a split rule's attributes set;
 *   it replaces the OHIF check.
 * @returns the intermediate values a caller needs for the thumbnail.
 */
export function applyImageListAttributes(
  imageSet,
  context: ImageSetFactoryContext,
  ruleIsReconstructable?: boolean
) {
  const dataSource = context.extensionManager.getActiveDataSource()[0];
  const imageIds = dataSource.getImageIdsForDisplaySet(imageSet);
  const {
    isDynamicVolume,
    value: checkedIsReconstructable,
    averageSpacingBetweenFrames,
    dynamicVolumeInfo,
  } = getDisplaySetInfo(imageSet.images, imageIds, context);
 
  // `ImageSet` snapshots `instance = images[0]` in its constructor and `sort()`
  // does not refresh it, so an instance that sorts to the front would otherwise
  // leave `instance` pointing at the wrong image.
  const instance = imageSet.images[0];
  const multiFrame = isMultiFrame(instance);
  const isReconstructable = ruleIsReconstructable ?? checkedIsReconstructable;
 
  imageSet.setAttributes({
    instance,
    isMultiFrame: multiFrame,
    volumeLoaderSchema: isDynamicVolume
      ? DYNAMIC_VOLUME_LOADER_SCHEME
      : DEFAULT_VOLUME_LOADER_SCHEME,
    // A multiframe display set holds one instance whose frames are the content;
    // a stack counts images.
    numImageFrames: multiFrame ? Number(instance.NumberOfFrames) : imageSet.images.length,
    countIcon: isReconstructable ? 'icon-mpr' : undefined,
    isReconstructable,
    messages: getDisplaySetMessages(imageSet.images, isReconstructable, isDynamicVolume),
    averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
    isDynamicVolume,
    dynamicVolumeInfo,
  });
 
  return { imageIds, isDynamicVolume, dynamicVolumeInfo };
}
 
/** Picks the middle image (middle time point when dynamic) as the thumbnail. */
export function applyThumbnailSrc(
  imageSet,
  context: ImageSetFactoryContext,
  { imageIds, isDynamicVolume, dynamicVolumeInfo }
) {
  const dataSource = context.extensionManager.getActiveDataSource()[0];
  let imageId = imageIds[Math.floor(imageIds.length / 2)];
  const thumbnailInstance = imageSet.images[Math.floor(imageSet.images.length / 2)];
  if (isDynamicVolume) {
    const timePoints = dynamicVolumeInfo.timePoints;
    const middleIndex = Math.floor(timePoints.length / 2);
    const middleTimePointImageIds = timePoints[middleIndex];
    imageId = middleTimePointImageIds[Math.floor(middleTimePointImageIds.length / 2)];
  }
 
  imageSet.setAttributes({
    getThumbnailSrc: dataSource.retrieve.getGetThumbnailSrc?.(thumbnailInstance, imageId),
  });
}