src/loaders/loaders.volume.js

Summary

Maintainability
C
1 day
Test Coverage
/** * Imports ***/
const PAKO = require('pako');

import LoadersBase from './loaders.base';
import CoreUtils from '../core/core.utils';
import ModelsSeries from '../models/models.series';
import ModelsStack from '../models/models.stack';
import ModelsFrame from '../models/models.frame';
import ParsersDicom from '../parsers/parsers.dicom';
import ParsersMhd from '../parsers/parsers.mhd';
import ParsersNifti from '../parsers/parsers.nifti';
import ParsersNrrd from '../parsers/parsers.nrrd';
import ParsersMgh from '../parsers/parsers.mgh';

/**
 *
 * It is typically used to load a DICOM image. Use loading manager for
 * advanced usage, such as multiple files handling.
 *
 * Demo: {@link https://fnndsc.github.io/vjs#loader_dicom}
 *
 * @module loaders/volumes
 * @example
 * var files = ['/data/dcm/fruit'];
 *
 * // Instantiate a dicom loader
 * var lDicomoader = new dicom();
 *
 * // load a resource
 * loader.load(
 *   // resource URL
 *   files[0],
 *   // Function when resource is loaded
 *   function(object) {
 *     //scene.add( object );
 *     console.log(object);
 *   }
 * );
 */
export default class LoadersVolumes extends LoadersBase {
  /**
   * Parse response.
   * response is formated as:
   *    {
   *      url: 'resource url',
   *      buffer: xmlresponse,
   *    }
   * @param {object} response - response
   * @return {promise} promise
   */
  parse(response) {
    // emit 'parse-start' event
    this.emit('parse-start', {
      file: response.url,
      time: new Date(),
    });

    // give a chance to the UI to update because
    // after the rendering will be blocked with intensive JS
    // will be removed after eventer set up
    if (this._progressBar) {
      this._progressBar.update(0, 100, 'parse', response.url);
    }

    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(
          new Promise((resolve, reject) => {
            let data = response;

            if (!Array.isArray(data)) {
              data = [data];
            }

            data.forEach(dataset => {
              this._preprocess(dataset);
            });

            if (data.length === 1) {
              data = data[0];
            } else {
              // if raw/mhd pair
              let mhdFile = data.filter(this._filterByExtension.bind(null, 'MHD'));
              let rawFile = data.filter(this._filterByExtension.bind(null, 'RAW'));
              if (data.length === 2 && mhdFile.length === 1 && rawFile.length === 1) {
                data.url = mhdFile[0].url;
                data.extension = mhdFile[0].extension;
                data.mhdBuffer = mhdFile[0].buffer;
                data.rawBuffer = rawFile[0].buffer;
              }
            }

            let Parser = this._parser(data.extension);
            if (!Parser) {
              // emit 'parse-error' event
              this.emit('parse-error', {
                file: response.url,
                time: new Date(),
                error: data.filename + 'can not be parsed.',
              });
              reject(data.filename + ' can not be parsed.');
            }

            // check extension
            let volumeParser = null;
            try {
              volumeParser = new Parser(data, 0);
            } catch (e) {
              console.warn(e);
              // emit 'parse-error' event
              this.emit('parse-error', {
                file: response.url,
                time: new Date(),
                error: e,
              });
              reject(e);
            }

            // create a series
            let series = new ModelsSeries();
            series.rawHeader = volumeParser.rawHeader();
            // global information
            series.seriesInstanceUID = volumeParser.seriesInstanceUID();
            series.transferSyntaxUID = volumeParser.transferSyntaxUID();
            series.seriesDate = volumeParser.seriesDate();
            series.seriesDescription = volumeParser.seriesDescription();
            series.studyDate = volumeParser.studyDate();
            series.studyDescription = volumeParser.studyDescription();
            series.numberOfFrames = volumeParser.numberOfFrames();
            if (!series.numberOfFrames) {
              series.numberOfFrames = 1;
            }
            series.numberOfChannels = volumeParser.numberOfChannels();
            series.modality = volumeParser.modality();
            // if it is a segmentation, attach extra information
            if (series.modality === 'SEG') {
              // colors
              // labels
              // etc.
              series.segmentationType = volumeParser.segmentationType();
              series.segmentationSegments = volumeParser.segmentationSegments();
            }
            // patient information
            series.patientID = volumeParser.patientID();
            series.patientName = volumeParser.patientName();
            series.patientAge = volumeParser.patientAge();
            series.patientBirthdate = volumeParser.patientBirthdate();
            series.patientSex = volumeParser.patientSex();

            // just create 1 dummy stack for now
            let stack = new ModelsStack();
            stack.numberOfChannels = volumeParser.numberOfChannels();
            stack.pixelRepresentation = volumeParser.pixelRepresentation();
            stack.pixelType = volumeParser.pixelType();
            stack.invert = volumeParser.invert();
            stack.spacingBetweenSlices = volumeParser.spacingBetweenSlices();
            stack.modality = series.modality;
            // if it is a segmentation, attach extra information
            if (stack.modality === 'SEG') {
              // colors
              // labels
              // etc.
              stack.segmentationType = series.segmentationType;
              stack.segmentationSegments = series.segmentationSegments;
            }
            series.stack.push(stack);
            // recursive call for each frame
            // better than for loop to be able
            // to update dom with "progress" callback
            setTimeout(
              this.parseFrameClosure(series, stack, response.url, 0, volumeParser, resolve, reject),
              0
            );
          })
        );
      }, 10);
    });
  }

  parseFrameClosure(series, stack, url, i, dataParser, resolve, reject) {
    return () => {
      this.parseFrame(series, stack, url, i, dataParser, resolve, reject);
    };
  }

  /**
   * recursive parse frame
   * @param {ModelsSeries} series - data series
   * @param {ModelsStack} stack - data stack
   * @param {string} url - resource url
   * @param {number} i - frame index
   * @param {parser} dataParser - selected parser
   * @param {promise.resolve} resolve - promise resolve args
   * @param {promise.reject} reject - promise reject args
   */
  parseFrame(series, stack, url, i, dataParser, resolve, reject) {
    let frame = new ModelsFrame();
    frame.sopInstanceUID = dataParser.sopInstanceUID(i);
    frame.url = url;
    frame.index = i;
    frame.invert = stack.invert;
    frame.frameTime = dataParser.frameTime(i);
    frame.ultrasoundRegions = dataParser.ultrasoundRegions(i);
    frame.rows = dataParser.rows(i);
    frame.columns = dataParser.columns(i);
    frame.numberOfChannels = stack.numberOfChannels;
    frame.pixelPaddingValue = dataParser.pixelPaddingValue(i);
    frame.pixelRepresentation = stack.pixelRepresentation;
    frame.pixelType = stack.pixelType;
    frame.pixelData = dataParser.extractPixelData(i);
    frame.pixelSpacing = dataParser.pixelSpacing(i);
    frame.spacingBetweenSlices = dataParser.spacingBetweenSlices(i);
    frame.sliceThickness = dataParser.sliceThickness(i);
    frame.imageOrientation = dataParser.imageOrientation(i);
    frame.rightHanded = dataParser.rightHanded();
    stack.rightHanded = frame.rightHanded;
    if (frame.imageOrientation === null) {
      frame.imageOrientation = [1, 0, 0, 0, 1, 0];
    }
    frame.imagePosition = dataParser.imagePosition(i);
    /*
    null ImagePosition should not be handle here
    if (frame.imagePosition === null) {
      frame.imagePosition = [0, 0, i];
    }*/
    frame.dimensionIndexValues = dataParser.dimensionIndexValues(i);
    frame.bitsAllocated = dataParser.bitsAllocated(i);
    frame.instanceNumber = dataParser.instanceNumber(i);
    frame.windowCenter = dataParser.windowCenter(i);
    frame.windowWidth = dataParser.windowWidth(i);
    frame.rescaleSlope = dataParser.rescaleSlope(i);
    frame.rescaleIntercept = dataParser.rescaleIntercept(i);
    // should pass frame index for consistency...
    frame.minMax = dataParser.minMaxPixelData(frame.pixelData);

    // if series.mo
    if (series.modality === 'SEG') {
      frame.referencedSegmentNumber = dataParser.referencedSegmentNumber(i);
    }

    stack.frame.push(frame);

    // update status
    this._parsed = i + 1;
    this._totalParsed = series.numberOfFrames;

    // will be removed after eventer set up
    if (this._progressBar) {
      this._progressBar.update(this._parsed, this._totalParsed, 'parse', url);
    }

    // emit 'parsing' event
    this.emit('parsing', {
      file: url,
      total: this._totalParsed,
      parsed: this._parsed,
      time: new Date(),
    });

    if (this._parsed === this._totalParsed) {
      // emit 'parse-success' event
      this.emit('parse-success', {
        file: url,
        total: this._totalParsed,
        parsed: this._parsed,
        time: new Date(),
      });

      resolve(series);
    } else {
      setTimeout(
        this.parseFrameClosure(series, stack, url, this._parsed, dataParser, resolve, reject),
        0
      );
    }
  }

  /**
   * Return parser given an extension
   * @param {string} extension - extension
   * @return {parser} selected parser
   */
  _parser(extension) {
    let Parser = null;

    switch (extension.toUpperCase()) {
      case 'NII':
      case 'NII_':
        Parser = ParsersNifti;
        break;
      case 'DCM':
      case 'DIC':
      case 'DICOM':
      case 'IMA':
      case '':
        Parser = ParsersDicom;
        break;
      case 'MHD':
        Parser = ParsersMhd;
        break;
      case 'NRRD':
        Parser = ParsersNrrd;
        break;
      case 'MGH':
      case 'MGZ':
        Parser = ParsersMgh;
        break;
      default:
        console.warn('unsupported extension: ' + extension);
        return false;
    }
    return Parser;
  }

  /**
   * Pre-process data to be parsed (find data type and de-compress)
   * @param {*} data
   */
  _preprocess(data) {
    const parsedUrl = CoreUtils.parseUrl(data.url);
    // update data
    data.filename = parsedUrl.filename;
    data.extension = parsedUrl.extension;
    data.pathname = parsedUrl.pathname;
    data.query = parsedUrl.query;

    // unzip if extension is '.gz'
    if (data.extension === 'gz') {
      data.gzcompressed = true;
      data.extension = data.filename
        .split('.gz')
        .shift()
        .split('.')
        .pop();
    } else if (data.extension === 'mgz') {
      data.gzcompressed = true;
      data.extension = 'mgh';
    } else if (data.extension === 'zraw') {
      data.gzcompressed = true;
      data.extension = 'raw';
    } else {
      data.gzcompressed = false;
    }

    if (data.gzcompressed) {
      let decompressedData = PAKO.inflate(data.buffer);
      data.buffer = decompressedData.buffer;
    }
  }

  /**
   * Filter data by extension
   * @param {*} extension
   * @param {*} item
   * @returns Boolean
   */
  _filterByExtension(extension, item) {
    if (item.extension.toUpperCase() === extension.toUpperCase()) {
      return true;
    }
    return false;
  }
}