tests_pacs_viewer.js

var dwv = dwv || {};
dwv.test = dwv.test || {};

// Image decoders (for web workers)
dwv.image.decoderScripts = {
  jpeg2000: '../../decoders/pdfjs/decode-jpeg2000.js',
  'jpeg-lossless': '../../decoders/rii-mango/decode-jpegloss.js',
  'jpeg-baseline': '../../decoders/pdfjs/decode-jpegbaseline.js',
  rle: '../../decoders/dwv/decode-rle.js'
};
// logger level (optional)
dwv.logger.level = dwv.utils.logger.levels.DEBUG;

// check environment support
dwv.env.check();

var _app = null;
var _tools = null;

// viewer options
var _mode = 0;
var _dicomWeb = false;

// example private logic for time value retrieval
// dwv.dicom.DicomElementsWrapper.prototype.getTime = function () {
//   var value;
//   var time = this.getFromKey('xABCD0123');
//   if (typeof time !== 'undefined') {
//     value = parseInt(time, 10);
//   }
//   return value;
// };

/**
 * Setup simple dwv app.
 */
dwv.test.viewerSetup = function () {
  // stage options
  var dataViewConfigs;
  var viewOnFirstLoadItem = true;

  // use for concurrent load
  var numberOfDataToLoad = 1;

  if (_mode === 0) {
    // simplest: one layer group
    dataViewConfigs = prepareAndGetSimpleDataViewConfig();
  } else if (_mode === 1) {
    // MPR
    viewOnFirstLoadItem = false;
    dataViewConfigs = prepareAndGetMPRDataViewConfig();
  } else if (_mode === 2) {
    // simple side by side
    addLayerGroup('layerGroupA');
    addLayerGroup('layerGroupB');
    dataViewConfigs = {
      0: [
        {
          divId: 'layerGroupA'
        }
      ],
      1: [
        {
          divId: 'layerGroupB'
        }
      ]
    };
  } else if (_mode === 3) {
    // multiple data, multiple layer group
    addLayerGroup('layerGroupA');
    addLayerGroup('layerGroupB');
    dataViewConfigs = {
      0: [
        {
          divId: 'layerGroupA'
        },
        {
          divId: 'layerGroupB'
        }
      ],
      1: [
        {
          divId: 'layerGroupA'
        }
      ],
      2: [
        {
          divId: 'layerGroupB'
        }
      ],
      3: [
        {
          divId: 'layerGroupB'
        }
      ]
    };
  }

  // tools
  _tools = {
    Scroll: {},
    WindowLevel: {},
    ZoomAndPan: {},
    Opacity: {},
    Draw: {options: ['Rectangle']}
  };

  // app config
  var config = {
    viewOnFirstLoadItem: viewOnFirstLoadItem,
    dataViewConfigs: dataViewConfigs,
    tools: _tools
  };
  // app
  _app = new dwv.App();
  _app.init(config);

  // bind events
  _app.addEventListener('loaderror', function (event) {
    console.error('load error', event);
  });
  _app.addEventListener('loadstart', function (event) {
    console.time('load-data-' + event.loadid);
  });
  var dataLoadProgress = new Array(numberOfDataToLoad);
  var sumReducer = function (sum, value) {
    return sum + value;
  };
  _app.addEventListener('loadprogress', function (event) {
    if (typeof event.lengthComputable !== 'undefined' &&
      event.lengthComputable) {
      dataLoadProgress[event.loadid] =
        Math.ceil((event.loaded / event.total) * 100);
      document.getElementById('loadprogress').value =
        dataLoadProgress.reduce(sumReducer) / numberOfDataToLoad;
    }
  });
  _app.addEventListener('load', function (event) {
    if (!viewOnFirstLoadItem) {
      _app.render(event.loadid);
    }
  });
  _app.addEventListener('loadend', function (event) {
    console.timeEnd('load-data-' + event.loadid);
  });

  var dataLoad = 0;
  var firstRender = [];
  _app.addEventListener('loadend', function (event) {
    // update UI at first render
    if (!firstRender.includes(event.loadid)) {
      // store data id
      firstRender.push(event.dataid);
      // log meta data
      if (event.loadtype === 'image') {
        console.log('metadata', _app.getMetaData(event.loadid));
        // add data row
        addDataRow(event.loadid);
        ++dataLoad;
        // init gui
        if (dataLoad === numberOfDataToLoad) {
          // select tool
          _app.setTool(getSelectedTool());

          var changeLayoutSelect = document.getElementById('changelayout');
          changeLayoutSelect.disabled = false;
          var resetLayoutButton = document.getElementById('resetlayout');
          resetLayoutButton.disabled = false;
        }
      }
    }

    if (event.loadtype === 'image' &&
      typeof _app.getMetaData(event.loadid).Modality !== 'undefined' &&
      _app.getMetaData(event.loadid).Modality.value === 'SEG') {
      // log SEG details
      logFramePosPats(_app.getMetaData(event.loadid));

      // example usage of a dicom SEG as data mask
      var useSegAsMask = false;
      if (useSegAsMask) {
        // image to filter
        var imgDataIndex = 0;
        var vls = _app.getViewLayersByDataIndex(imgDataIndex);
        var vc = vls[0].getViewController();
        var img = _app.getImage(imgDataIndex);
        var imgGeometry = img.getGeometry();
        var sliceSize = imgGeometry.getSize().getDimSize(2);
        // SEG image
        var segImage = _app.getImage(event.loadid);
        // calculate slice difference
        var segOrigin0 = segImage.getGeometry().getOrigins()[0];
        var segOrigin0Point = new dwv.math.Point([
          segOrigin0.getX(), segOrigin0.getY(), segOrigin0.getZ()
        ]);
        var segOriginIndex = imgGeometry.worldToIndex(segOrigin0Point);
        var indexOffset = segOriginIndex.get(2) * sliceSize;
        // set alpha function
        vc.setViewAlphaFunction(function (value, index) {
          // multiply by 3 since SEG is RGB
          var segIndex = 3 * (index - indexOffset);
          if (segIndex >= 0 &&
            segImage.getValueAtOffset(segIndex) === 0 &&
            segImage.getValueAtOffset(segIndex + 1) === 0 &&
            segImage.getValueAtOffset(segIndex + 2) === 0) {
            return 0;
          } else {
            return 0xff;
          }
        });
      }
    }
  });

  _app.addEventListener('positionchange', function (event) {
    var input = document.getElementById('position');
    var values = event.value[1];
    var text = '(index: ' + event.value[0] + ')';
    if (event.value.length > 2) {
      text += ' value: ' + event.value[2];
    }
    input.value = values.map(getPrecisionRound(2));
    // index as small text
    var span = document.getElementById('positionspan');
    span.innerHTML = text;
  });

  // default keyboard shortcuts
  window.addEventListener('keydown', function (event) {
    _app.defaultOnKeydown(event);
    // mask segment related
    if (!isNaN(parseInt(event.key, 10))) {
      var vc =
        _app.getActiveLayerGroup().getActiveViewLayer().getViewController();
      if (!vc.isMask()) {
        return;
      }
      var number = parseInt(event.key, 10);
      var segHelper = vc.getMaskSegmentHelper();
      if (segHelper.hasSegment(number)) {
        var segment = segHelper.getSegment(number);
        if (event.ctrlKey) {
          if (event.altKey) {
            dwv.logger.debug('Delete segment: ' + segment.label);
            // delete
            vc.deleteSegment(number, _app.addToUndoStack);
          } else {
            dwv.logger.debug('Show/hide segment: ' + segment.label);
            // show/hide the selected segment
            if (segHelper.isHidden(number)) {
              segHelper.removeFromHidden(number);
            } else {
              segHelper.addToHidden(number);
            }
            vc.applyHiddenSegments();
          }
        }
      }
    }
  });
  // default on resize
  window.addEventListener('resize', function () {
    _app.onResize();
  });

  var options = {};
  // special dicom web request header
  if (_dicomWeb) {
    options.requestHeaders = [{
      name: 'Accept',
      value: 'multipart/related; type="application/dicom"; transfer-syntax=*'
    }];
  }
  // load from window location
  dwv.utils.loadFromUri(window.location.href, _app, options);
};

/**
 * Last minute.
 */
dwv.test.onDOMContentLoadedViewer = function () {
  // setup
  dwv.test.viewerSetup();

  var positionInput = document.getElementById('position');
  positionInput.addEventListener('change', function () {
    var vls = _app.getViewLayersByDataIndex(0);
    var vc = vls[0].getViewController();
    var values = this.value.split(',');
    vc.setCurrentPosition(new dwv.math.Point3D(
      parseFloat(values[0]), parseFloat(values[1]), parseFloat(values[2]))
    );
  });

  var resetLayoutButton = document.getElementById('resetlayout');
  resetLayoutButton.addEventListener('click', function () {
    _app.resetLayout();
  });

  var changeLayoutSelect = document.getElementById('changelayout');
  changeLayoutSelect.addEventListener('change', function (event) {
    var configs;
    var value = event.target.value;
    if (value === 'mpr') {
      configs = prepareAndGetMPRDataViewConfig();
    } else {
      configs = prepareAndGetSimpleDataViewConfig();
    }

    // unbind app to controls
    unbindAppToControls();

    // set config
    _app.setDataViewConfig(configs);

    clearDataTable();
    for (var i = 0; i < _app.getNumberOfLoadedData(); ++i) {
      _app.render(i);
      // add data row (will bind controls)
      addDataRow(i);
    }

    // need to set tool after config change
    _app.setTool(getSelectedTool());
  });

  setupBindersCheckboxes();

  setupToolsCheckboxes();

  // bind app to input files
  var fileinput = document.getElementById('fileinput');
  fileinput.addEventListener('change', function (event) {
    console.log('%c ----------------', 'color: teal;');
    console.log(event.target.files);
    _app.loadFiles(event.target.files);
  });
};

/**
 * Append a layer div in the root 'dwv' one.
 *
 * @param {string} id The id of the layer.
 */
function addLayerGroup(id) {
  var layerDiv = document.createElement('div');
  layerDiv.id = id;
  layerDiv.className = 'layerGroup';
  var root = document.getElementById('dwv');
  root.appendChild(layerDiv);
}

/**
 * Create simple view config(s).
 *
 * @returns {object} The view config.
 */
function prepareAndGetSimpleDataViewConfig() {
  // clean up
  var dwvDiv = document.getElementById('dwv');
  dwvDiv.innerHTML = '';
  // add div
  addLayerGroup('layerGroupA');
  return {'*': [{divId: 'layerGroupA'}]};
}

/**
 * Create MPR view config(s).
 *
 * @returns {object} The view config.
 */
function prepareAndGetMPRDataViewConfig() {
  // clean up
  var dwvDiv = document.getElementById('dwv');
  dwvDiv.innerHTML = '';
  // add divs
  addLayerGroup('layerGroupA');
  addLayerGroup('layerGroupC');
  addLayerGroup('layerGroupS');
  return {
    '*': [
      {
        divId: 'layerGroupA',
        orientation: 'axial'
      },
      {
        divId: 'layerGroupC',
        orientation: 'coronal'
      },
      {
        divId: 'layerGroupS',
        orientation: 'sagittal'
      }
    ]
  };
}

/**
 * Get the layer groups div ids from the data view configs.
 *
 * @param {object} dataViewConfigs The configs.
 * @returns {Array} The list of ids.
 */
function getLayerGroupDivIds(dataViewConfigs) {
  var divIds = [];
  var keys = Object.keys(dataViewConfigs);
  for (var i = 0; i < keys.length; ++i) {
    var dataViewConfig = dataViewConfigs[keys[i]];
    for (var j = 0; j < dataViewConfig.length; ++j) {
      var divId = dataViewConfig[j].divId;
      if (!divIds.includes(divId)) {
        divIds.push(divId);
      }
    }
  }
  return divIds;
}

/**
 * Get the layer group ids associated to a data.
 *
 * @param {Array} dataViewConfig The data view config.
 * @returns {Array} The list of ids.
 */
function getDataLayerGroupIds(dataViewConfig) {
  var divIds = [];
  for (var j = 0; j < dataViewConfig.length; ++j) {
    divIds.push(dataViewConfig[j].divId);
  }
  return divIds;
}

/**
 * Setup the binders checkboxes
 */
function setupBindersCheckboxes() {
  var bindersDiv = document.getElementById('binders');
  var propList = [
    'WindowLevel',
    'Position',
    'Zoom',
    'Offset',
    'Opacity'
  ];
  var binders = [];
  // add all binders at startup
  for (var b = 0; b < propList.length; ++b) {
    binders.push(new dwv.gui[propList[b] + 'Binder']);
  }
  _app.setLayerGroupsBinders(binders);

  /**
   * Add a binder.
   *
   * @param {string} propName The name of the property to bind.
   */
  function addBinder(propName) {
    binders.push(new dwv.gui[propName + 'Binder']);
    _app.setLayerGroupsBinders(binders);
  }
  /**
   * Remove a binder.
   *
   * @param {string} propName The name of the property to bind.
   */
  function removeBinder(propName) {
    for (var i = 0; i < binders.length; ++i) {
      if (binders[i] instanceof dwv.gui[propName + 'Binder']) {
        binders.splice(i, 1);
      }
    }
    _app.setLayerGroupsBinders(binders);
  }
  /**
   * Get the input change handler for a binder.
   *
   * @param {string} propName The name of the property to bind.
   * @returns {object} The handler.
   */
  function getOnInputChange(propName) {
    return function (event) {
      if (event.target.checked) {
        addBinder(propName);
      } else {
        removeBinder(propName);
      }
    };
  }
  // individual binders
  for (var i = 0; i < propList.length; ++i) {
    var propName = propList[i];

    var input = document.createElement('input');
    input.id = 'binder-' + i;
    input.type = 'checkbox';
    input.checked = true;
    input.onchange = getOnInputChange(propName);

    var label = document.createElement('label');
    label.htmlFor = input.id;
    label.appendChild(document.createTextNode(propName));

    bindersDiv.appendChild(input);
    bindersDiv.appendChild(label);
  }

  // check all
  var allInput = document.createElement('input');
  allInput.id = 'binder-all';
  allInput.type = 'checkbox';
  allInput.checked = true;
  allInput.onchange = function () {
    for (var j = 0; j < propList.length; ++j) {
      document.getElementById('binder-' + j).click();
    }
  };
  var allLabel = document.createElement('label');
  allLabel.htmlFor = allInput.id;
  allLabel.appendChild(document.createTextNode('all'));
  bindersDiv.appendChild(allInput);
  bindersDiv.appendChild(allLabel);
}

/**
 * Setup the tools checkboxes
 */
function setupToolsCheckboxes() {
  var toolsDiv = document.getElementById('tools');
  var keys = Object.keys(_tools);

  var getChangeTool = function (tool) {
    return function () {
      _app.setTool(tool);
      if (tool === 'Draw') {
        _app.setToolFeatures({shapeName: 'Rectangle'});
      }
    };
  };

  var getKeyCheck = function (char, input) {
    return function (event) {
      if (!event.ctrlKey &&
        !event.altKey &&
        !event.shiftKey &&
        event.key === char) {
        input.click();
      }
    };
  };

  for (var i = 0; i < keys.length; ++i) {
    var key = keys[i];

    var input = document.createElement('input');
    input.id = 'tool-' + i;
    input.name = 'tools';
    input.type = 'radio';
    input.onchange = getChangeTool(key);

    if (key === 'Scroll') {
      input.checked = true;
    }

    var label = document.createElement('label');
    label.htmlFor = input.id;
    label.appendChild(document.createTextNode(key));

    toolsDiv.appendChild(input);
    toolsDiv.appendChild(label);

    // keyboard shortcut
    window.addEventListener(
      'keydown', getKeyCheck(key[0].toLowerCase(), input));
  }
}

/**
 * Get the selected tool
 *
 * @returns {string} The tool name.
 */
function getSelectedTool() {
  var toolsInput = document.getElementsByName('tools');
  var toolIndex = null;
  for (var j = 0; j < toolsInput.length; ++j) {
    if (toolsInput[j].checked) {
      toolIndex = j;
      break;
    }
  }
  return Object.keys(_tools)[toolIndex];
}

/**
 * Bind app to controls.
 */
function bindAppToControls() {
  _app.addEventListener('wlchange', onWLChange);
  _app.addEventListener('opacitychange', onOpacityChange);
}

/**
 * Unbind app to controls.
 */
function unbindAppToControls() {
  _app.removeEventListener('wlchange', onWLChange);
  _app.removeEventListener('opacitychange', onOpacityChange);
}

/**
 * Handle app wl change.
 *
 * @param {object} event The change event.
 */
function onWLChange(event) {
  // width number
  var elemId = 'width-' + event.dataid + '-number';
  var elem = document.getElementById(elemId);
  if (elem) {
    elem.value = event.value[1];
  } else {
    console.warn('wl change: HTML not ready?');
  }
  // width range
  elemId = 'width-' + event.dataid + '-range';
  elem = document.getElementById(elemId);
  if (elem) {
    elem.value = event.value[1];
  }
  // center number
  elemId = 'center-' + event.dataid + '-number';
  elem = document.getElementById(elemId);
  if (elem) {
    elem.value = event.value[0];
  }
  // center range
  elemId = 'center-' + event.dataid + '-range';
  elem = document.getElementById(elemId);
  if (elem) {
    elem.value = event.value[0];
  }
}

/**
 * Handle app opacity change.
 *
 * @param {object} event The change event.
 */
function onOpacityChange(event) {
  var value = parseFloat(event.value[0]).toPrecision(3);
  // number
  var elemId = 'opacity-' + event.dataid + '-number';
  var elem = document.getElementById(elemId);
  if (elem) {
    elem.value = value;
  } else {
    console.warn('opacity change: HTML not ready?');
  }
  // range
  elemId = 'opacity-' + event.dataid + '-range';
  elem = document.getElementById(elemId);
  if (elem) {
    elem.value = value;
  }
}

/**
 * Clear the data table.
 */
function clearDataTable() {
  var detailsDiv = document.getElementById('layersdetails');
  detailsDiv.innerHTML = '';
}

/**
 * Get a control div: label, range and number field.
 *
 * @param {string} id The control id.
 * @param {string} name The control name.
 * @param {number} min The control minimum value.
 * @param {number} max The control maximum value.
 * @param {number} value The control value.
 * @param {Function} callback The callback on control value change.
 * @param {number} precision Optional number field float precision.
 * @returns {object} The control div.
 */
function getControlDiv(id, name, min, max, value, callback, precision) {
  var range = document.createElement('input');
  range.id = id + '-range';
  range.className = 'ctrl-range';
  range.type = 'range';
  range.min = min.toPrecision(precision);
  range.max = max.toPrecision(precision);
  range.step = ((max - min) * 0.01).toPrecision(precision);
  range.value = value;

  var label = document.createElement('label');
  label.id = id + '-label';
  label.className = 'ctrl-label';
  label.htmlFor = range.id;
  label.appendChild(document.createTextNode(name));

  var number = document.createElement('input');
  number.id = id + '-number';
  number.className = 'ctrl-number';
  number.type = 'number';
  number.min = range.min;
  number.max = range.max;
  number.step = range.step;
  number.value = parseFloat(value).toPrecision(precision);

  // callback and bind range and number
  number.oninput = function () {
    range.value = this.value;
    callback(this.value);
  };
  range.oninput = function () {
    number.value = parseFloat(this.value).toPrecision(precision);
    callback(this.value);
  };

  var div = document.createElement('div');
  div.id = id + '-ctrl';
  div.className = 'ctrl';
  div.appendChild(label);
  div.appendChild(range);
  div.appendChild(number);

  return div;
}

/**
 * Add a data row.
 *
 * @param {number} id The data index.
 */
function addDataRow(id) {
  // bind app to controls on first id
  if (id === 0) {
    bindAppToControls();
  }

  var dataViewConfigs = _app.getDataViewConfig();
  var allLayerGroupDivIds = getLayerGroupDivIds(dataViewConfigs);
  // use first view layer
  var vls = _app.getViewLayersByDataIndex(id);
  var vl = vls[0];
  var vc = vl.getViewController();
  var wl = vc.getWindowLevel();

  var table = document.getElementById('layerstable');
  var body;
  // create table if not present
  if (!table) {
    table = document.createElement('table');
    table.id = 'layerstable';
    var header = table.createTHead();
    var trow = header.insertRow(0);
    var insertTCell = function (text) {
      var th = document.createElement('th');
      th.innerHTML = text;
      trow.appendChild(th);
    };
    insertTCell('Id');
    for (var j = 0; j < allLayerGroupDivIds.length; ++j) {
      insertTCell('LG' + j);
    }
    insertTCell('Alpha Range');
    insertTCell('Contrast');
    insertTCell('Alpha');
    body = table.createTBody();
    var div = document.getElementById('layersdetails');
    div.appendChild(table);
  } else {
    body = table.getElementsByTagName('tbody')[0];
  }

  // add new layer row
  var row = body.insertRow();
  var cell;

  // cell: id
  cell = row.insertCell();
  cell.appendChild(document.createTextNode(id));

  // cell: radio
  var viewConfig = dataViewConfigs[id];
  if (typeof viewConfig === 'undefined') {
    viewConfig = dataViewConfigs['*'];
  }
  var dataLayerGroupsIds = getDataLayerGroupIds(viewConfig);
  for (var l = 0; l < allLayerGroupDivIds.length; ++l) {
    var layerGroupDivId = allLayerGroupDivIds[l];
    cell = row.insertCell();
    if (!dataLayerGroupsIds.includes(layerGroupDivId)) {
      continue;
    }
    var radio = document.createElement('input');
    radio.type = 'radio';
    radio.name = 'layerselect-' + l;
    radio.id = 'layerselect-' + layerGroupDivId + '-' + id;
    radio.checked = true;
    radio.onchange = function (event) {
      var fullId = event.target.id;
      var split = fullId.split('-');
      var groupDivId = split[1];
      var dataId = split[2];
      var lg = _app.getLayerGroupByDivId(groupDivId);
      lg.setActiveViewLayerByDataIndex(parseInt(dataId, 10));
    };
    cell.appendChild(radio);
  }

  var image = _app.getImage(vl.getDataIndex());
  var dataRange = image.getDataRange();
  var rescaledDataRange = image.getRescaledDataRange();
  var floatPrecision = 4;

  // cell: alpha range
  cell = row.insertCell();
  var minId = 'value-min-' + id;
  var maxId = 'value-max-' + id;
  // callback
  var changeAlphaFunc = function () {
    var min = parseFloat(document.getElementById(minId + '-number').value);
    var max = parseFloat(document.getElementById(maxId + '-number').value);
    var func = function (value) {
      if (value >= min && value <= max) {
        return 255;
      }
      return 0;
    };
    for (var i = 0; i < vls.length; ++i) {
      vls[i].getViewController().setViewAlphaFunction(func);
    }
  };
  // add controls
  cell.appendChild(getControlDiv(minId, 'min',
    dataRange.min, dataRange.max, dataRange.min,
    changeAlphaFunc, floatPrecision));
  cell.appendChild(getControlDiv(maxId, 'max',
    dataRange.min, dataRange.max, dataRange.max,
    changeAlphaFunc, floatPrecision));

  // cell: contrast
  cell = row.insertCell();
  var widthId = 'width-' + id;
  var centerId = 'center-' + id;
  // callback
  var changeContrast = function () {
    var width =
      parseFloat(document.getElementById(widthId + '-number').value);
    var center =
      parseFloat(document.getElementById(centerId + '-number').value);
    vc.setWindowLevel(center, width);
  };
  // add controls
  cell.appendChild(getControlDiv(widthId, 'width',
    0, rescaledDataRange.max - rescaledDataRange.min, wl.width,
    changeContrast, floatPrecision));
  cell.appendChild(getControlDiv(centerId, 'center',
    rescaledDataRange.min, rescaledDataRange.max, wl.center,
    changeContrast, floatPrecision));

  // cell: opactiy
  cell = row.insertCell();
  var opacityId = 'opacity-' + id;
  // callback
  var changeOpacity = function (value) {
    vl.setOpacity(value);
    vl.draw();
  };
  // add controls
  cell.appendChild(getControlDiv(opacityId, 'opacity',
    0, 1, vl.getOpacity(), changeOpacity, floatPrecision));
}

/**
 * Compare two pos pat keys.
 *
 * @param {string} a The key of the first item.
 * @param {string} b The key of the second item.
 * @returns {number} Negative if a<b, positive if a>b.
 */
function comparePosPat(a, b) {
  var za = parseFloat(a.split('\\').at(-1));
  var zb = parseFloat(b.split('\\').at(-1));
  return za - zb;
}

/**
 * Sort an object with pos pat string keys.
 *
 * @param {object} obj The object to sort
 * @returns {object} The sorted object.
 */
function sortByPosPatKey(obj) {
  var keys = Object.keys(obj);
  keys.sort(comparePosPat);
  var sorted = new Map();
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    sorted.set(key, obj[key]);
  }
  return sorted;
}

/**
 * Get a rounding function for a specific precision.
 *
 * @param {number} precision The rounding precision.
 * @returns {Function} The rounding function.
 */
function getPrecisionRound(precision) {
  return function (x) {
    return dwv.utils.precisionRound(x, precision);
  };
}

/**
 * Log the DICCOM seg segments ordered by frame position patients.
 *
 * @param {object} elements The DICOM seg elements.
 */
function logFramePosPats(elements) {
  var perFrame = elements.PerFrameFunctionalGroupsSequence.value;
  var perPos = {};
  for (var i = 0; i < perFrame.length; ++i) {
    var posSq = perFrame[i].PlanePositionSequence.value;
    var pos = posSq[0].ImagePositionPatient.value;
    if (typeof perPos[pos] === 'undefined') {
      perPos[pos] = [];
    }
    var frameSq = perFrame[i].FrameContentSequence.value;
    var dim = frameSq[0].DimensionIndexValues.value;
    perPos[pos].push(dim);
  }
  console.log('DICOM SEG Segments', sortByPosPatKey(perPos));
}