All files / platform/core/src/services/DisplaySetService DisplaySetService.ts

60.36% Statements 99/164
42.22% Branches 19/45
66.66% Functions 26/39
59.49% Lines 94/158

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 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448          34x           34x       133x 133x 133x 133x 2x   8889x       8891x     34x         34x       34x 34x 34x         34x 34x     34x 34x         34x 34x 34x 34x       134x 134x         134x   134x 134x 134x 134x 134x                                     1x 1x       1x 1x       1x       34x               319x       278x 787x                 73x       73x 386x     73x                                                                               34x         8871x           8871x     34x             133x       133x         133x   133x               133x   133x     133x   133x       133x 133x 133x   133x     133x         133x                                                         133x 8891x 133x   8891x 8891x         133x 133x 133x 133x       133x   133x                                           133x 133x   133x   133x 133x     133x 516x 516x   516x   133x       133x                                                             133x   133x         133x 133x         133x 133x         133x   133x       133x                                 133x                                                                                         34x  
import { ExtensionManager } from '../../extensions';
import { DisplaySet, InstanceMetadata } from '../../types';
import { PubSubService } from '../_shared/pubSubServiceInterface';
import EVENTS from './EVENTS';
 
const displaySetCache = new Map<string, DisplaySet>();
 
/**
 * Filters the instances set by instances not in
 * display sets.  Done in O(n) time.
 */
const filterInstances = (
  instances: InstanceMetadata[],
  displaySets: DisplaySet[]
): InstanceMetadata[] => {
  const dsInstancesSOP = new Set();
  displaySets.forEach(ds => {
    const dsInstances = ds.instances;
    if (!dsInstances) {
      console.warn('No instances in', ds);
    } else {
      dsInstances.forEach(instance => dsInstancesSOP.add(instance.SOPInstanceUID));
    }
  });
 
  return instances.filter(instance => !dsInstancesSOP.has(instance.SOPInstanceUID));
};
 
export default class DisplaySetService extends PubSubService {
  public static REGISTRATION = {
    altName: 'DisplaySetService',
    name: 'displaySetService',
    create: ({ configuration = {} }) => {
      return new DisplaySetService();
    },
  };
 
  public activeDisplaySets = [];
  public unsupportedSOPClassHandler;
  extensionManager: ExtensionManager;
 
  protected activeDisplaySetsMap = new Map<string, DisplaySet>();
 
  // Record if the active display sets changed - used to group change events so
  // that fewer events need to be fired when creating multiple display sets
  protected activeDisplaySetsChanged = false;
 
  constructor() {
    super(EVENTS);
    this.unsupportedSOPClassHandler =
      '@ohif/extension-default.sopClassHandlerModule.not-supported-display-sets-handler';
  }
 
  public init(extensionManager, SOPClassHandlerIds): void {
    this.extensionManager = extensionManager;
    this.SOPClassHandlerIds = SOPClassHandlerIds;
    this.activeDisplaySets = [];
    this.activeDisplaySetsMap.clear();
  }
 
  _addDisplaySetsToCache(displaySets: DisplaySet[]) {
    displaySets.forEach(displaySet => {
      displaySetCache.set(displaySet.displaySetInstanceUID, displaySet);
    });
  }
 
  _addActiveDisplaySets(displaySets: DisplaySet[]) {
    const { activeDisplaySets, activeDisplaySetsMap } = this;
 
    displaySets.forEach(displaySet => {
      if (!activeDisplaySetsMap.has(displaySet.displaySetInstanceUID)) {
        this.activeDisplaySetsChanged = true;
        activeDisplaySets.push(displaySet);
        activeDisplaySetsMap.set(displaySet.displaySetInstanceUID, displaySet);
      }
    });
  }
 
  /**
   * Sets the handler for unsupported sop classes
   * @param sopClassHandlerUID
   */
  public setUnsuportedSOPClassHandler(sopClassHandler) {
    this.unsupportedSOPClassHandler = sopClassHandler;
  }
 
  /**
   * Adds new display sets directly, as specified.
   * Use this function when the display sets are created externally directly
   * rather than using the default sop class handlers to create display sets.
   */
  public addDisplaySets(...displaySets: DisplaySet[]): string[] {
    this._addDisplaySetsToCache(displaySets);
    this._addActiveDisplaySets(displaySets);
 
    // The activeDisplaySetsChanged flag is only seen if we add display sets
    // so, don't broadcast the change if all the display sets were pre-existing.
    this.activeDisplaySetsChanged = false;
    this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, {
      displaySetsAdded: displaySets,
      options: { madeInClient: displaySets[0].madeInClient },
    });
    return displaySets;
  }
 
  public getDisplaySetCache(): Map<string, DisplaySet> {
    return displaySetCache;
  }
 
  public getMostRecentDisplaySet(): DisplaySet {
    return this.activeDisplaySets[this.activeDisplaySets.length - 1];
  }
 
  public getActiveDisplaySets(): DisplaySet[] {
    return this.activeDisplaySets;
  }
 
  public getDisplaySetsForSeries = (seriesInstanceUID: string): DisplaySet[] => {
    return [...displaySetCache.values()].filter(
      displaySet => displaySet.SeriesInstanceUID === seriesInstanceUID
    );
  };
 
  public getDisplaySetForSOPInstanceUID(
    sopInstanceUID: string,
    seriesInstanceUID: string,
    frameNumber?: number
  ): DisplaySet {
    const displaySets = seriesInstanceUID
      ? this.getDisplaySetsForSeries(seriesInstanceUID)
      : [...this.getDisplaySetCache().values()];
 
    const displaySet = displaySets.find(ds => {
      return ds.instances?.some(i => i.SOPInstanceUID === sopInstanceUID);
    });
 
    return displaySet;
  }
 
  public setDisplaySetMetadataInvalidated(
    displaySetInstanceUID: string,
    invalidateData = true
  ): void {
    const displaySet = this.getDisplaySetByUID(displaySetInstanceUID);
 
    Iif (!displaySet) {
      return;
    }
 
    // broadcast event to update listeners with the new displaySets
    this._broadcastEvent(EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, {
      displaySetInstanceUID,
      invalidateData,
    });
  }
 
  public deleteDisplaySet(displaySetInstanceUID) {
    Iif (!displaySetInstanceUID) {
      return;
    }
    const { activeDisplaySets, activeDisplaySetsMap } = this;
 
    const activeDisplaySetsIndex = activeDisplaySets.findIndex(
      ds => ds.displaySetInstanceUID === displaySetInstanceUID
    );
 
    displaySetCache.delete(displaySetInstanceUID);
    activeDisplaySets.splice(activeDisplaySetsIndex, 1);
    activeDisplaySetsMap.delete(displaySetInstanceUID);
 
    this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets);
    this._broadcastEvent(EVENTS.DISPLAY_SETS_REMOVED, {
      displaySetInstanceUIDs: [displaySetInstanceUID],
    });
  }
 
  /**
   * @param {string} displaySetInstanceUID
   * @returns {object} displaySet
   */
  public getDisplaySetByUID = (displaySetInstanceUid: string): DisplaySet => {
    Iif (typeof displaySetInstanceUid !== 'string') {
      throw new Error(
        `getDisplaySetByUID: displaySetInstanceUid must be a string, you passed ${displaySetInstanceUid}`
      );
    }
 
    return displaySetCache.get(displaySetInstanceUid);
  };
 
  /**
   *
   * @param {*} input
   * @param {*} param1: settings: initialViewportSettings by HP or callbacks after rendering
   * @returns {string[]} - added displaySetInstanceUIDs
   */
  makeDisplaySets = (input, { batch = false, madeInClient = false, settings = {} } = {}) => {
    Iif (!input || !input.length) {
      throw new Error('No instances were provided.');
    }
 
    Iif (batch && !input[0].length) {
      throw new Error('Batch displaySet creation does not contain array of array of instances.');
    }
 
    // If array of instances => One instance.
    const displaySetsAdded = new Array<DisplaySet>();
 
    Iif (batch) {
      for (let i = 0; i < input.length; i++) {
        const instances = input[i];
        const displaySets = this.makeDisplaySetForInstances(instances, settings);
 
        displaySetsAdded.push(...displaySets);
      }
    } else {
      const displaySets = this.makeDisplaySetForInstances(input, settings);
 
      displaySetsAdded.push(...displaySets);
    }
 
    const options = {};
 
    Iif (madeInClient) {
      options.madeInClient = true;
    }
 
    if (this.activeDisplaySetsChanged) {
      this.activeDisplaySetsChanged = false;
      this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets);
    }
    if (displaySetsAdded?.length) {
      // The response from displaySetsAdded will only contain newly added
      // display sets.
      this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, {
        displaySetsAdded,
        options,
      });
 
      return displaySetsAdded;
    }
  };
 
  /**
   * The onModeExit returns the display set service to the initial state,
   * that is without any display sets.  To avoid recreating display sets,
   * the mode specific onModeExit is called before this method and should
   * store the active display sets and the cached data.
   */
  public onModeExit(): void {
    this.getDisplaySetCache().clear();
    this.activeDisplaySets.length = 0;
    this.activeDisplaySetsMap.clear();
  }
 
  /**
   * This function hides the old makeDisplaySetForInstances function to first
   * separate the instances by sopClassUID so each call have only instances
   * with the same sopClassUID, to avoid a series composed by different
   * sopClassUIDs be filtered inside one of the SOPClassHandler functions and
   * didn't appear in the series list.
   * @param instancesSrc
   * @param settings
   * @returns
   */
  public makeDisplaySetForInstances(instancesSrc: InstanceMetadata[], settings): DisplaySet[] {
    // creating a sopClassUID list and for each sopClass associate its respective
    // instance list
    const instancesForSetSOPClasses = instancesSrc.reduce((sopClassList, instance) => {
      if (!(instance.SOPClassUID in sopClassList)) {
        sopClassList[instance.SOPClassUID] = [];
      }
      sopClassList[instance.SOPClassUID].push(instance);
      return sopClassList;
    }, {});
    // for each sopClassUID, call the old makeDisplaySetForInstances with a
    // instance list composed only by instances with the same sopClassUID and
    // accumulate the displaySets in the variable allDisplaySets
    const sopClasses = Object.keys(instancesForSetSOPClasses);
    let allDisplaySets = [];
    sopClasses.forEach(sopClass => {
      const displaySets = this._makeDisplaySetForInstances(
        instancesForSetSOPClasses[sopClass],
        settings
      );
      allDisplaySets = [...allDisplaySets, ...displaySets];
    });
    return allDisplaySets;
  }
 
  /**
   * Creates new display sets for the instances contained in instancesSrc
   * according to the sop class handlers registered.
   * This is idempotent in that calling it a second time with the
   * same set of instances will not result in new display sets added.
   * However, the response for the subsequent call will be empty as the data
   * is already present.
   * Calling it with some new instances and some existing instances will
   * result in the new instances being added to existing display sets if
   * they support the addInstances call, OR to new instances otherwise.
   * Only the new instances are returned - the others are updated.
   *
   * @param instancesSrc are instances to add
   * @param settings are settings to add
   * @returns Array of the display sets added.
   */
  private _makeDisplaySetForInstances(instancesSrc: InstanceMetadata[], settings): DisplaySet[] {
    // Some of the sop class handlers take a direct reference to instances
    // so make sure it gets copied here so that they have their own ref
    let instances = [...instancesSrc];
    const instance = instances[0];
 
    const existingDisplaySets = this.getDisplaySetsForSeries(instance.SeriesInstanceUID) || [];
 
    const SOPClassHandlerIds = this.SOPClassHandlerIds;
    const allDisplaySets = [];
 
    // Iterate over the sop class handlers while there are still instances to add
    for (let i = 0; i < SOPClassHandlerIds.length && instances.length; i++) {
      const SOPClassHandlerId = SOPClassHandlerIds[i];
      const handler = this.extensionManager.getModuleEntry(SOPClassHandlerId);
 
      if (handler.sopClassUids.includes(instance.SOPClassUID)) {
        // Check if displaySets are already created using this SeriesInstanceUID/SOPClassHandler pair.
        let displaySets = existingDisplaySets.filter(
          displaySet => displaySet.SOPClassHandlerId === SOPClassHandlerId
        );
 
        Iif (displaySets.length) {
          // This case occurs when there are already display sets, so remove
          // any instances in existing display sets.
          instances = filterInstances(instances, displaySets);
          // See if an existing display set can add this instance to it,
          // for example, if it is a new image to be added to the existing set
          for (const ds of displaySets) {
            const addedDs = ds.addInstances?.(instances, this);
            Iif (addedDs) {
              this.activeDisplaySetsChanged = true;
              instances = filterInstances(instances, [addedDs]);
              this._addActiveDisplaySets([addedDs]);
              this.setDisplaySetMetadataInvalidated(addedDs.displaySetInstanceUID);
            }
            // This means that all instances already existed or got added to
            // existing display sets, and had an invalidated event fired
            Iif (!instances.length) {
              return allDisplaySets;
            }
          }
 
          Iif (!instances.length) {
            // Everything is already added - this is just an update caused
            // by something else
            this._addActiveDisplaySets(displaySets);
            return allDisplaySets;
          }
        }
 
        // The instances array still contains some instances, so try
        // creating additional display sets using the sop class handler
        displaySets = handler.getDisplaySetsFromSeries(instances);
 
        Iif (!displaySets || !displaySets.length) {
          continue;
        }
 
        // applying hp-defined viewport settings to the displaysets
        displaySets.forEach(ds => {
          Object.keys(settings).forEach(key => {
            ds[key] = settings[key];
          });
        });
 
        this._addDisplaySetsToCache(displaySets);
        this._addActiveDisplaySets(displaySets);
 
        // It is possible that this SOP class handler handled some instances
        // but there may need to be other instances handled by other handlers,
        // so remove the handled instances
        instances = filterInstances(instances, displaySets);
 
        allDisplaySets.push(...displaySets);
      }
    }
    // applying the default sopClassUID handler
    Iif (allDisplaySets.length === 0) {
      // applying hp-defined viewport settings to the displaysets
      const handler = this.extensionManager.getModuleEntry(this.unsupportedSOPClassHandler);
      const displaySets = handler.getDisplaySetsFromSeries(instances);
      Iif (displaySets?.length) {
        displaySets.forEach(ds => {
          Object.keys(settings).forEach(key => {
            ds[key] = settings[key];
          });
        });
 
        this._addDisplaySetsToCache(displaySets);
        this._addActiveDisplaySets(displaySets);
 
        allDisplaySets.push(...displaySets);
      }
    }
    return allDisplaySets;
  }
 
  /**
   * Iterates over displaysets and invokes comparator for each element.
   * It returns a list of items that has being succeed by comparator method.
   *
   * @param comparator - method to be used on the validation
   * @returns list of displaysets
   */
  public getDisplaySetsBy(comparator: (DisplaySet) => boolean): DisplaySet[] {
    const result = [];
 
    Iif (typeof comparator !== 'function') {
      throw new Error(`The comparator ${comparator} was not a function`);
    }
 
    this.getActiveDisplaySets().forEach(displaySet => {
      Iif (comparator(displaySet)) {
        result.push(displaySet);
      }
    });
 
    return result;
  }
 
  /**
   *
   * @param sortFn function to sort the display sets
   * @param direction direction to sort the display sets
   * @returns void
   */
  public sortDisplaySets(
    sortFn: (a: DisplaySet, b: DisplaySet) => number,
    direction: string,
    suppressEvent = false
  ): void {
    this.activeDisplaySets.sort(sortFn);
    Iif (direction === 'descending') {
      this.activeDisplaySets.reverse();
    }
    Iif (!suppressEvent) {
      this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets);
    }
  }
}