imageLoader/wadouri/dataSetCacheManager.js

import external from '../../externalModules.js';
import { xhrRequest } from '../internal/index.js';
import { combineFrameInstanceDataset } from './combineFrameInstanceDataset.js';
import multiframeDataset from './retrieveMultiframeDataset.js';
import dataSetFromPartialContent from './dataset-from-partial-content.js';

/**
 * This object supports loading of DICOM P10 dataset from a uri and caching it so it can be accessed
 * by the caller.  This allows a caller to access the datasets without having to go through cornerstone's
 * image loader mechanism.  One reason a caller may need to do this is to determine the number of frames
 * in a multiframe sop instance so it can create the imageId's correctly.
 */
let cacheSizeInBytes = 0;

let loadedDataSets = {};

let promises = {};

// returns true if the wadouri for the specified index has been loaded
function isLoaded(uri) {
  return loadedDataSets[uri] !== undefined;
}

function get(uri) {
  let dataSet;

  if (uri.includes('&frame=')) {
    const { frame, dataSet: multiframeDataSet } =
      multiframeDataset.retrieveMultiframeDataset(uri);

    dataSet = combineFrameInstanceDataset(frame, multiframeDataSet);
  } else if (loadedDataSets[uri]) {
    dataSet = loadedDataSets[uri].dataSet;
  }

  return dataSet;
}

function update(uri, dataSet) {
  const loadedDataSet = loadedDataSets[uri];

  if (!loadedDataSet) {
    console.error(`No loaded dataSet for uri ${uri}`);

    return;
  }
  // Update dataset
  cacheSizeInBytes -= loadedDataSet.dataSet.byteArray.length;
  loadedDataSet.dataSet = dataSet;
  cacheSizeInBytes += dataSet.byteArray.length;

  external.cornerstone.triggerEvent(
    external.cornerstone.events,
    'datasetscachechanged',
    {
      uri,
      action: 'updated',
      cacheInfo: getInfo(),
    }
  );
}

// loads the dicom dataset from the wadouri sp
function load(uri, loadRequest = xhrRequest, imageId) {
  const { cornerstone, dicomParser } = external;

  // if already loaded return it right away
  if (loadedDataSets[uri]) {
    // console.log('using loaded dataset ' + uri);
    return new Promise((resolve) => {
      loadedDataSets[uri].cacheCount++;
      resolve(loadedDataSets[uri].dataSet);
    });
  }

  // if we are currently loading this uri, increment the cacheCount and return its promise
  if (promises[uri]) {
    // console.log('returning existing load promise for ' + uri);
    promises[uri].cacheCount++;

    return promises[uri];
  }

  // This uri is not loaded or being loaded, load it via an xhrRequest
  const loadDICOMPromise = loadRequest(uri, imageId);

  // handle success and failure of the XHR request load
  const promise = new Promise((resolve, reject) => {
    loadDICOMPromise
      .then(async function (dicomPart10AsArrayBuffer) {
        const partialContent = {
          isPartialContent: false,
          fileTotalLength: null,
        };

        // Allow passing extra data with the loader promise so as not to change
        // the API
        if (!(dicomPart10AsArrayBuffer instanceof ArrayBuffer)) {
          if (!dicomPart10AsArrayBuffer.arrayBuffer) {
            return reject(
              new Error(
                'If not returning ArrayBuffer, must return object with `arrayBuffer` parameter'
              )
            );
          }
          partialContent.isPartialContent =
            dicomPart10AsArrayBuffer.flags.isPartialContent;
          partialContent.fileTotalLength =
            dicomPart10AsArrayBuffer.flags.fileTotalLength;
          dicomPart10AsArrayBuffer = dicomPart10AsArrayBuffer.arrayBuffer;
        }

        const byteArray = new Uint8Array(dicomPart10AsArrayBuffer);

        // Reject the promise if parsing the dicom file fails
        let dataSet;

        try {
          if (partialContent.isPartialContent) {
            // This dataSet object will include a fetchMore function,
            dataSet = await dataSetFromPartialContent(byteArray, loadRequest, {
              uri,
              imageId,
              fileTotalLength: partialContent.fileTotalLength,
            });
          } else {
            dataSet = dicomParser.parseDicom(byteArray);
          }
        } catch (error) {
          return reject(error);
        }

        loadedDataSets[uri] = {
          dataSet,
          cacheCount: promise.cacheCount,
        };
        cacheSizeInBytes += dataSet.byteArray.length;
        resolve(dataSet);

        cornerstone.triggerEvent(cornerstone.events, 'datasetscachechanged', {
          uri,
          action: 'loaded',
          cacheInfo: getInfo(),
        });
      }, reject)
      .then(
        () => {
          // Remove the promise if success
          delete promises[uri];
        },
        () => {
          // Remove the promise if failure
          delete promises[uri];
        }
      );
  });

  promise.cacheCount = 1;

  promises[uri] = promise;

  return promise;
}

// remove the cached/loaded dicom dataset for the specified wadouri to free up memory
function unload(uri) {
  const { cornerstone } = external;

  // console.log('unload for ' + uri);
  if (loadedDataSets[uri]) {
    loadedDataSets[uri].cacheCount--;
    if (loadedDataSets[uri].cacheCount === 0) {
      // console.log('removing loaded dataset for ' + uri);
      cacheSizeInBytes -= loadedDataSets[uri].dataSet.byteArray.length;
      delete loadedDataSets[uri];

      cornerstone.triggerEvent(cornerstone.events, 'datasetscachechanged', {
        uri,
        action: 'unloaded',
        cacheInfo: getInfo(),
      });
    }
  }
}

function getInfo() {
  return {
    cacheSizeInBytes,
    numberOfDataSetsCached: Object.keys(loadedDataSets).length,
  };
}

// removes all cached datasets from memory
function purge() {
  loadedDataSets = {};
  promises = {};
  cacheSizeInBytes = 0;
}

export { loadedDataSets };

export default {
  isLoaded,
  load,
  unload,
  getInfo,
  purge,
  get,
  update,
};