Initial work on Nucleus parity merge

This commit is contained in:
Emanuel F. Oliveira committed 2017-01-29 22:11:17 -02:00
1 parent aa88fe181a
commit 680beb163f
38 files changed
+4353 -1

No files matched your search

@@ -0,0 +1,90 @@
import { Random } from 'meteor/random';
import { Metadata } from './metadata/Metadata';
import { InstanceMetadata } from './metadata/InstanceMetadata';
export class DisplaySet {
constructor(instances) {
this._uid = Random.id();
this._attributes = {};
this._instances = [];
if (instances instanceof Array) {
instances.forEach( instance => this.addInstance(instance));
}
}
getUID() {
return this._uid;
}
/**
* Retrieve the number of instances within the current display set.
* @returns {number} The number of instances in the current display set.
*/
getInstanceCount() {
return this._instances.length;
}
/**
* Find an instance by index.
* @param {number} index An integer representing a list index.
* @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
*/
getInstanceByIndex(index) {
let found; // undefined by default...
if (Metadata.isValidIndex(index)) {
found = this._instances[index];
}
return found;
}
/**
* Invokes the supplied callback for each instance in the current display set passing
* two arguments: instance (InstanceMetadata) and index (the integer index of the instance within the current display set)
* @param {function} callback The callback function which will be invoked for each instance in the display set.
* @returns {undefined} Nothing is returned.
*/
forEachInstance(callback) {
if (Metadata.isValidCallback(callback)) {
this._instances.forEach((instance, index) => {
callback.call(null, instance, index);
});
}
}
/**
* Append an instance to the current series.
* @param {InstanceMetadata} instance The instance to be added to the current series.
* @returns {boolean} Returns true on success, false otherwise.
*/
addInstance(instance) {
let result = false;
if (instance instanceof InstanceMetadata) {
this._instances.push(instance);
result = true;
}
return result;
}
setAttribute(attribute, value) {
this._attributes[attribute] = value;
}
getAttribute(attribute) {
return this._attributes[attribute];
}
setAttributes(attributes) {
if (typeof attributes === 'object' && attributes !== null) {
const _attributes = this._attributes;
const hasOwn = Object.prototype.hasOwnProperty;
for (let attribute in attributes) {
if (hasOwn.call(attributes, attribute)) {
_attributes[attribute] = attributes[attribute];
}
}
}
}
}
@@ -0,0 +1,50 @@
import { Random } from 'meteor/random';
/**
* This class defines an ImageSet object which will be used across the viewer. This object represents
* a list of images that are associated by any arbitrary criteria being thus content agnostic. Besides the
* main attributes (images and uid) it allows additional attributes to be appended to it (currently
* indiscriminately, but this should be changed).
*/
export class ImageSet {
constructor(images) {
if (Array.isArray(images) !== true) {
throw new TypeError('ImageSet expects an array of images...');
}
// Main ImageSet attributes
this.images = images; // Array of images
this.uid = Random.id(); // Unique ID of the instance
}
setAttribute(attribute, value) {
this[attribute] = value;
}
getAttribute(attribute) {
return this[attribute];
}
setAttributes(attributes) {
if (typeof attributes === 'object' && attributes !== null) {
const imageSet = this, hasOwn = Object.prototype.hasOwnProperty;
for (let attribute in attributes) {
if (hasOwn.call(attributes, attribute)) {
imageSet[attribute] = attributes[attribute];
}
}
}
}
getImage(index) {
return this.images[index];
}
sortBy(sortingCallback) {
return this.images.sort(sortingCallback);
}
}
@@ -0,0 +1,589 @@
import { Blaze } from 'meteor/blaze';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
import { Random } from 'meteor/random';
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
// Displays Series in Viewports given a Protocol and list of Studies
export class LayoutManager {
constructor(parentNode, studies) {
OHIF.log.info('LayoutManager constructor');
this.parentNode = parentNode;
this.studies = studies;
this.viewportData = [];
this.layoutTemplateName = 'gridLayout';
this.layoutProps = {
rows: 1,
columns: 1
};
this.isZoomed = false;
const updateSessionFn = () => Tracker.afterFlush(() => Session.set('LayoutManagerUpdated', Random.id()));
this.updateSession = _.throttle(updateSessionFn, 300);
}
getNumberOfViewports() {
return this.layoutProps.rows * this.layoutProps.columns;
}
setDefaultViewportData() {
OHIF.log.info('LayoutManager setDefaultViewportData');
const self = this;
// Get the number of vieports to be rendered
const viewportsAmount = this.getNumberOfViewports();
// Store the old viewport data and reset the current
const oldViewportData = self.viewportData;
// Get the studies and display sets sequence map
const sequenceMap = this.getDisplaySetSequenceMap();
// Check if the display sets are sequenced
const isSequenced = this.isDisplaySetsSequenced(sequenceMap);
// Define the current viewport index and the viewport data array
let currentViewportIndex = 0;
if (viewportsAmount > oldViewportData.length && oldViewportData.length && isSequenced) {
// Keep the displayed display sets
self.viewportData = oldViewportData;
currentViewportIndex = oldViewportData.length;
}
else if (viewportsAmount <= oldViewportData.length) {
// Reduce the original displayed display sets
self.viewportData = oldViewportData.slice(0, viewportsAmount);
return;
}
else {
// Reset all display sets
self.viewportData = [];
}
// Get all the display sets for the viewer studies
let displaySets = [];
this.studies.forEach(study => {
study.series.forEach(displaySet => {
displaySet.images.length && displaySets.push(displaySet);
});
});
// Get the display sets that will be appended to the current ones
let appendix;
const currentLength = self.viewportData.length;
if (currentLength) {
// TODO: isolate displaySets array by study (maybe a map?)
const beginIndex = sequenceMap.values().next().value[0].displaySetIndex + currentLength;
const endIndex = beginIndex + (viewportsAmount - currentLength);
appendix = displaySets.slice(beginIndex, endIndex);
}
else {
// Get available display sets from the first to the grid size
appendix = displaySets.slice(0, viewportsAmount);
}
// Generate the additional data based on the appendix
const additionalData = [];
appendix.forEach((displaySet, index) => {
additionalData.push({
viewportIndex: currentViewportIndex + index,
studyInstanceUid: displaySet.studyInstanceUid,
seriesInstanceUid: displaySet.seriesInstanceUid,
displaySetInstanceUid: displaySet.displaySetInstanceUid,
sopInstanceUid: displaySet.images[0].sopInstanceUid
});
});
// Append the additional data with the viewport data
self.viewportData = self.viewportData.concat(additionalData);
// Push empty objects if the amount is lesser than the grid size
while (self.viewportData.length < viewportsAmount) {
self.viewportData.push({});
}
}
updateViewports() {
OHIF.log.info('LayoutManager updateViewports');
if (!this.viewportData ||
!this.viewportData.length ||
this.viewportData.length !== this.getNumberOfViewports()) {
this.setDefaultViewportData();
}
// imageViewerViewports occasionally needs relevant layout data in order to set
// the element style of the viewport in question
const layoutProps = this.layoutProps;
const data = $.extend({
viewportData: []
}, layoutProps);
this.viewportData.forEach( viewportData => {
const viewportDataAndLayoutProps = $.extend(viewportData, layoutProps);
data.viewportData.push(viewportDataAndLayoutProps);
});
const layoutTemplate = Template[this.layoutTemplateName];
$(this.parentNode).html('');
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
this.updateSession();
this.isZoomed = false;
}
/**
* This function destroys and re-renders the imageViewerViewport template.
* It uses the data provided to load a new display set into the produced viewport.
*
* @param viewportIndex
* @param data
*/
rerenderViewportWithNewDisplaySet(viewportIndex, data) {
OHIF.log.info(`LayoutManager rerenderViewportWithNewDisplaySet: ${viewportIndex}`);
// The parent container is identified because it is later removed from the DOM
const element = $('.imageViewerViewport').eq(viewportIndex);
const container = element.parents('.viewportContainer').get(0);
// Record the current viewportIndex so this can be passed into the re-rendering call
data.viewportIndex = viewportIndex;
// Update the dictionary of loaded displaySet for the specified viewport
this.viewportData[viewportIndex] = {
viewportIndex: viewportIndex,
displaySetInstanceUid: data.displaySetInstanceUid,
seriesInstanceUid: data.seriesInstanceUid,
studyInstanceUid: data.studyInstanceUid,
renderedCallback: data.renderedCallback,
currentImageIdIndex: data.currentImageIdIndex || 0
};
// Remove the hover styling
element.find('canvas').not('.magnifyTool').removeClass('faded');
// Remove the whole template, add in the new one
const viewportContainer = element.parents('.removable');
const newViewportContainer = document.createElement('div');
newViewportContainer.className = 'removable';
// Remove the parent element of the template
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
viewportContainer.remove();
container.appendChild(newViewportContainer);
// Render and insert the template
Blaze.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
this.updateSession();
}
enlargeViewport(viewportIndex) {
OHIF.log.info(`LayoutManager enlargeViewport: ${viewportIndex}`);
if (!this.viewportData ||
!this.viewportData.length) {
return;
}
// Clone the array for later
this.previousViewportData = this.viewportData.slice(0);
const singleViewportData = $.extend({}, this.viewportData[viewportIndex]);
singleViewportData.rows = 1;
singleViewportData.columns = 1;
singleViewportData.viewportIndex = 0;
const data = {
viewportData: [singleViewportData],
rows: 1,
columns: 1
};
const layoutTemplate = Template['gridLayout'];
$(this.parentNode).html('');
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
this.isZoomed = true;
this.zoomedViewportIndex = viewportIndex;
this.viewportData = data.viewportData;
this.updateSession();
}
resetPreviousLayout() {
OHIF.log.info('LayoutManager resetPreviousLayout');
if (!this.isZoomed) {
return;
}
this.previousViewportData[this.zoomedViewportIndex] = $.extend({}, this.viewportData[0]);
this.previousViewportData[this.zoomedViewportIndex].viewportIndex = this.zoomedViewportIndex;
this.viewportData = this.previousViewportData;
this.updateViewports();
}
toggleEnlargement(viewportIndex) {
OHIF.log.info(`LayoutManager toggleEnlargement: ${viewportIndex}`);
if (this.isZoomed) {
this.resetPreviousLayout();
}
else {
// Don't enlarge the viewport if we only have one Viewport
// to begin with
if (this.getNumberOfViewports() > 1) {
this.enlargeViewport(viewportIndex);
}
}
}
// Return the display sets map sequence of display sets and viewports
getDisplaySetSequenceMap() {
OHIF.log.info('LayoutManager getDisplaySetSequenceMap');
// Get the viewport data list
const viewportDataList = this.viewportData;
// Create a map to control the display set sequence
const sequenceMap = new Map();
// Iterate over each viewport and register its details on the sequence map
viewportDataList.forEach((viewportData, viewportIndex) => {
// Get the current study
const currentStudy = _.findWhere(this.studies, {
studyInstanceUid: viewportData.studyInstanceUid
}) || this.studies[0];
// Get the display sets
const displaySets = currentStudy.series;
// Get the current display set
const displaySet = _.findWhere(displaySets, {
displaySetInstanceUid: viewportData.displaySetInstanceUid
});
// Get the current instance index (using 9999 to sort greater than -1)
let displaySetIndex = _.indexOf(displaySets, displaySet);
displaySetIndex = displaySetIndex < 0 ? 9999 : displaySetIndex;
// Try to get a map entry for current study or create it if not present
let studyViewports = sequenceMap.get(currentStudy);
if (!studyViewports) {
studyViewports = [];
sequenceMap.set(currentStudy, studyViewports);
}
// Register the viewport index and the display set index on the map
studyViewports.push({
viewportIndex,
displaySetIndex
});
});
// Return the generated sequence map
return sequenceMap;
}
// Check if all the display sets and viewports are sequenced
isDisplaySetsSequenced(definedSequenceMap) {
OHIF.log.info('LayoutManager isDisplaySetsSequenced');
let isSequenced = true;
// Get the studies and display sets sequence map
const sequenceMap = definedSequenceMap || this.getDisplaySetSequenceMap();
sequenceMap.forEach((studyViewports, study) => {
let lastDisplaySetIndex = null;
let lastViewportIndex = null;
studyViewports.forEach(({ viewportIndex, displaySetIndex }, index) => {
// Check if the sequence is wrong
if (
displaySetIndex !== 9999 &&
lastViewportIndex !== null &&
lastDisplaySetIndex !== null &&
displaySetIndex !== null &&
(viewportIndex - 1 !== lastViewportIndex ||
displaySetIndex - 1 !== lastDisplaySetIndex)
) {
// Set the sequenced flag as false;
isSequenced = false;
}
// Update the last viewport index
lastViewportIndex = viewportIndex;
// Update the last display set index
lastDisplaySetIndex = displaySetIndex;
});
});
return isSequenced;
}
// Check if is possible to move display sets on a specific direction
canMoveDisplaySets(isNext) {
OHIF.log.info('LayoutManager canMoveDisplaySets');
// Get the setting that defines if the display set navigation is multiple
const isMultiple = OHIF.uiSettings.displaySetNavigationMultipleViewports;
// Get the setting that allow display set navigation looping over series
const allowLooping = OHIF.uiSettings.displaySetNavigationLoopOverSeries;
// Get the studies and display sets sequence map
const sequenceMap = this.getDisplaySetSequenceMap();
// Check if the display sets are sequenced
const isSequenced = this.isDisplaySetsSequenced(sequenceMap);
// Get Active Viewport Index if isMultiple is false
const activeViewportIndex = !isMultiple ? Session.get('activeViewport') : null;
// Check if is next and looping is blocked
if (isNext && !allowLooping) {
// Check if the end was reached
let endReached = true;
sequenceMap.forEach((studyViewports, study) => {
// Get active viewport index if isMultiple is false ortherwise get last
const studyViewport = studyViewports[activeViewportIndex !== null ? activeViewportIndex : studyViewports.length - 1];
if (!studyViewport) {
return;
}
const viewportIndex = studyViewport.displaySetIndex;
const layoutViewports = studyViewports.length;
const amount = study.displaySets.length;
const move = !isMultiple ? 1 : ((amount % layoutViewports) || layoutViewports);
const lastStepIndex = amount - move;
// 9999 for index means empty viewport, see getDisplaySetSequenceMap function
if (viewportIndex !== 9999 && viewportIndex !== lastStepIndex) {
endReached = false;
}
});
// Return false if end is not reached yet
if ((!isMultiple || isSequenced) && endReached) {
return false;
}
}
// Check if is previous and looping is blocked
if (!isNext && !allowLooping) {
// Check if the begin was reached
let beginReached = true;
if(activeViewportIndex >= 0) {
sequenceMap.forEach((studyViewports, study) => {
// Get active viewport index if isMultiple is false ortherwise get first
const studyViewport = studyViewports[activeViewportIndex !== null ? activeViewportIndex : 0];
if (!studyViewport) {
return;
}
const viewportIndex = studyViewport.displaySetIndex;
const layoutViewports = studyViewports.length;
// 9999 for index means empty viewport, see getDisplaySetSequenceMap function
if (viewportIndex !== 9999 && viewportIndex - layoutViewports !== -layoutViewports) {
beginReached = false;
}
});
}
// Return false if begin is not reached yet
if ((!isMultiple || isSequenced) && beginReached) {
return false;
}
}
return true;
}
// Move display sets forward or backward in the given viewport index
moveSingleViewportDisplaySets(viewportIndex, isNext) {
OHIF.log.info(`LayoutManager moveSingleViewportDisplaySets: ${viewportIndex}`);
// Get the setting that allow display set navigation looping over series
const allowLooping = OHIF.uiSettings.displaySetNavigationLoopOverSeries;
// Get the selected viewport data
const viewportData = this.viewportData[viewportIndex];
// Get the current study
const currentStudy = _.findWhere(this.studies, {
studyInstanceUid: viewportData.studyInstanceUid
}) || this.studies[0];
// Get the display sets
const displaySets = currentStudy.displaySets;
// Get the current display set
const currentDisplaySet = _.findWhere(displaySets, {
displaySetInstanceUid: viewportData.displaySetInstanceUid
});
// Get the new index and ensure that it will exists in display sets
let newIndex = _.indexOf(displaySets, currentDisplaySet);
if (isNext) {
newIndex++;
if (newIndex >= displaySets.length) {
// Stop here if looping is not allowed
if (!allowLooping) {
return;
}
newIndex = 0;
}
} else {
newIndex--;
if (newIndex < 0) {
// Stop here if looping is not allowed
if (!allowLooping) {
return;
}
newIndex = displaySets.length - 1;
}
}
// Get the display set data for the new index
const newDisplaySetData = displaySets[newIndex];
// Rerender the viewport using the new display set data
this.rerenderViewportWithNewDisplaySet(viewportIndex, newDisplaySetData);
}
// Move multiple display sets forward or backward in all viewports
moveMultipleViewportDisplaySets(isNext) {
OHIF.log.info('LayoutManager moveMultipleViewportDisplaySets');
// Get the setting that allow display set navigation looping over series
const allowLooping = OHIF.uiSettings.displaySetNavigationLoopOverSeries;
// Create a map to control the display set sequence
const sequenceMap = new this.getDisplaySetSequenceMap();
// Check if the display sets are sequenced
const isSequenced = this.isDisplaySetsSequenced(sequenceMap);
const displaySetsToRender = [];
// Iterate over the studies map and move its display sets
sequenceMap.forEach((studyViewports, study) => {
// Sort the viewports on the study by the display set index
studyViewports.sort((a, b) => a.displaySetIndex > b.displaySetIndex);
// Get the study display sets
const displaySets = study.displaySets;
// Calculate the base index
const firstIndex = studyViewports[0].displaySetIndex;
const steps = studyViewports.length;
const rest = firstIndex % steps;
let baseIndex = rest ? firstIndex - rest : firstIndex;
const direction = isNext ? 1 : -1;
baseIndex += steps * direction;
const amount = displaySets.length;
// Check if the indexes are sequenced or will overflow the array bounds
if (baseIndex >= amount) {
const move = (amount % steps) || steps;
const lastStepIndex = amount - move;
if (firstIndex + steps !== lastStepIndex + steps) {
// Reset the index if the display sets are sequenced but shifted
baseIndex = lastStepIndex;
} else if (!allowLooping) {
// Stop here if looping is not allowed
return;
} else {
// Start over the series if looping is allowed
baseIndex = 0;
}
} else if (baseIndex < 0) {
if (firstIndex > 0) {
// Reset the index if the display sets are sequenced but shifted
baseIndex = 0;
} else if (!allowLooping) {
// Stop here if looping is not allowed
return;
} else {
// Go to the series' end if looping is allowed
baseIndex = (amount - 1) - ((amount - 1) % steps);
}
} else if (!isSequenced) {
// Reset the sequence if indexes are not sequenced
baseIndex = 0;
}
// Iterate over the current study viewports
studyViewports.forEach(({ viewportIndex }, index) => {
// Get the new displaySet index to be rendered in viewport
const newIndex = baseIndex + index;
// Get the display set data for the new index
const displaySetData = displaySets[newIndex] || {};
// Add the current display set that on the render list
displaySetsToRender.push(displaySetData);
});
});
// Sort the display sets
const sortingFunction = OHIF.utils.sortBy({
name: 'studyInstanceUid'
}, {
name: 'instanceNumber'
}, {
name: 'seriesNumber'
});
displaySetsToRender.sort((a, b) => sortingFunction(a, b));
// Iterate over each display set data and render on its respective viewport
displaySetsToRender.forEach((data, index) => {
this.rerenderViewportWithNewDisplaySet(index, data);
});
}
// Move display sets forward or backward
moveDisplaySets(isNext) {
OHIF.log.info('LayoutManager moveDisplaySets');
//Check if navigation is on a single or multiple viewports
if (OHIF.uiSettings.displaySetNavigationMultipleViewports) {
// Move display sets on multiple viewports
this.moveMultipleViewportDisplaySets(isNext);
} else {
// Get the selected viewport index
const viewportIndex = Session.get('activeViewport');
// Move display sets on a single viewport
this.moveSingleViewportDisplaySets(viewportIndex, isNext);
}
}
isStudyLoadedIntoViewport(studyInstanceUid, viewportIndex) {
return (this.viewportData.find(item => item.studyInstanceUid === studyInstanceUid && item.viewportIndex === viewportIndex) !== void 0);
}
isMultipleLayout() {
return this.layoutProps.row !== 1 && this.layoutProps.columns !== 1;
}
};
@@ -0,0 +1,14 @@
// @TODO: improve this object
/**
* Objects to be used to throw errors, specially
* in Trackers functions (afterFlush, Flush).
*/
export class OHIFError extends Error {
constructor(message) {
super();
this.message = message;
this.stack = (new Error()).stack;
this.name = this.constructor.name;
}
}
@@ -0,0 +1,153 @@
import { Session } from 'meteor/session';
import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
import { getInstanceClassDefaultViewport } from '../instanceClassSpecificViewport';
// Manage resizing viewports triggered by window resize
export class ResizeViewportManager {
constructor() {
OHIF.log.info('ResizeViewportManager');
}
// Reposition Study Series Quick Switch based whether side bars are opened or not
repositionStudySeriesQuickSwitch() {
OHIF.log.info('ResizeViewportManager repositionStudySeriesQuickSwitch');
const activeTab = Session.get('activeContentId');
if(activeTab === 'viewerTab') {
const nViewports = OHIF.viewerbase.layoutManager.viewportData.length;
if(nViewports && nViewports > 1) {
const viewer = $('#viewer');
const leftSidebar = viewer.find('.sidebar-left.sidebar-open');
const rightSidebar = viewer.find('.sidebar-right.sidebar-open');
const leftQuickSwitch = $('.quickSwitchWrapper.left');
const rightQuickSwitch = $('.quickSwitchWrapper.right');
const hasLeftSidebar = leftSidebar.length > 0;
const hasRightSidebar = rightSidebar.length > 0;
rightQuickSwitch.removeClass('left-sidebar-only');
leftQuickSwitch.removeClass('right-sidebar-only');
let leftOffset = 0;
if(hasLeftSidebar) {
leftOffset = ( leftSidebar.width()/$(window).width() ) * 100;
if(!hasRightSidebar) {
rightQuickSwitch.addClass('left-sidebar-only');
}
}
if(hasRightSidebar && !hasLeftSidebar) {
leftQuickSwitch.addClass('right-sidebar-only');
}
const leftPosition = ( ($('#imageViewerViewports').width() / nViewports) / $(window).width() ) * 100 + leftOffset;
const rightPosition = 100 - leftPosition;
leftQuickSwitch.css('right', rightPosition + '%');
rightQuickSwitch.css('left', leftPosition + '%');
}
}
}
// Relocate dialogs positions
relocateDialogs(){
OHIF.log.info('ResizeViewportManager relocateDialogs');
const bottomRightDialogs = $('#cineDialog, #annotationDialog, #textMarkerOptionsDialog');
bottomRightDialogs.css({
top: '', // This removes the CSS property completely
left: '',
bottom: 0,
right: 0
});
const centerDialogs = $('.draggableDialog').not(bottomRightDialogs);
centerDialogs.css({
top: 0,
left:0,
bottom: 0,
right: 0
});
}
// Resize viewport scrollbars
resizeScrollbars(element) {
OHIF.log.info('ResizeViewportManager resizeScrollbars');
const currentOverlay = $(element).siblings('.imageViewerViewportOverlay');
const imageControls = currentOverlay.find('.imageControls');
currentOverlay.find('.imageControls').height($(element).height());
// Set it's width to its parent's height
// (because webkit is stupid and can't style vertical sliders)
const scrollbar = currentOverlay.find('#scrollbar');
scrollbar.height(scrollbar.parent().height() - 20);
const currentImageSlider = currentOverlay.find('#imageSlider');
const overlayHeight = currentImageSlider.parent().height();
const browserInfo = cornerstoneTools.getBrowserInfo();
if (browserInfo.indexOf('IE') > -1) {
currentImageSlider.height(overlayHeight);
} else {
currentImageSlider.width(overlayHeight);
}
}
// Resize a single viewport element
resizeViewportElement(element) {
let enabledElement;
try {
enabledElement = cornerstone.getEnabledElement(element);
} catch(error) {
return;
}
cornerstone.resize(element, true);
if (enabledElement.fitToWindow === false) {
const imageId = enabledElement.image.imageId;
const instance = cornerstoneTools.metaData.get('instance', imageId);
const instanceClassViewport = getInstanceClassDefaultViewport(instance, enabledElement, imageId);
cornerstone.setViewport(element, instanceClassViewport);
}
}
// Resize each viewport element
resizeViewportElements() {
this.relocateDialogs();
const viewportResizeTimer = setTimeout(() => {
this.repositionStudySeriesQuickSwitch();
const elements = $('.imageViewerViewport').not('.empty');
elements.each((index, element) => {
this.resizeViewportElement(element);
this.resizeScrollbars(element);
});
}, 1);
}
// Function to override resizeViewportElements function
setResizeViewportElement(resizeViewportElements) {
this.resizeViewportElements = resizeViewportElements;
}
// Avoid doing DOM manipulation during the resize handler
// because it is fired very often.
// Resizing is therefore performed 100 ms after the resize event stops.
handleResize() {
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
OHIF.log.info('ResizeViewportManager resizeViewportElements');
this.resizeViewportElements();
}, 100);
}
}
@@ -0,0 +1,330 @@
import { Random } from 'meteor/random';
import { ReactiveVar } from 'meteor/reactive-var';
const FUNCTION = 'function';
const OBJECT = 'object';
const STRING = 'string';
const PROPERTY_SEPARATOR = '.';
const ORDER_ASC = 'asc';
const ORDER_DESC = 'desc';
const MIN_COUNT = 0x00000000;
const MAX_COUNT = 0x7FFFFFFF;
export class TypeSafeCollection {
constructor() {
this._operationCount = new ReactiveVar(MIN_COUNT);
this._elementList = [];
}
/**
* Private Methods
*/
_invalidate() {
let count = this._operationCount.get();
this._operationCount.set(count < MAX_COUNT ? count + 1 : MIN_COUNT);
}
_elements(silent) {
(silent === true || this._operationCount.get());
return this._elementList;
}
_elementWithPayload(payload, silent) {
return this._elements(silent).find(item => item.payload === payload);
}
_elementWithId(id, silent) {
return this._elements(silent).find(item => item.id === id);
}
/**
* Static Methods
*/
/**
* Retrieve an object's property value by name
* @param {Object} object The object we want read the property from...
* @param {String} propertyName The property to be read (e.g., 'address.street.name' or 'address.street.number'
* to read object.address.street.name or object.address.street.number);
* @returns {Any} Returns whatever the property holds or undefined if the property cannot be read or reached.
*/
static getPropertyValue(object, propertyName) {
let propertyValue;
if (typeof object === OBJECT && object !== null && typeof propertyName === STRING) {
let fragments = propertyName.split(PROPERTY_SEPARATOR),
fragmentCount = fragments.length;
if (fragmentCount > 0) {
let firstFragment = fragments[0],
remainingFragments = fragmentCount > 1 ? fragments.slice(1).join(PROPERTY_SEPARATOR) : null;
propertyValue = object[firstFragment];
if (remainingFragments !== null) {
propertyValue = TypeSafeCollection.getPropertyValue(propertyValue, remainingFragments);
}
}
}
return propertyValue;
}
/**
* Checks if a sorting specifier is valid.
* A valid sorting specifier consists of an array of arrays being each subarray a pair
* in the format ["property name", "sorting order"].
* The following exemple can be used to sort studies by "date"" and use "time" to break ties in descending order.
* [ [ 'study.date', 'desc' ], [ 'study.time', 'desc' ] ]
* @param {Array} specifiers The sorting specifier to be tested.
* @returns {boolean} Returns true if the specifiers are valid, false otherwise.
*/
static isValidSortingSpecifier(specifiers) {
let result = true;
if (specifiers instanceof Array && specifiers.length > 0) {
for (let i = specifiers.length - 1; i >= 0; i--) {
const item = specifiers[i];
if (item instanceof Array) {
const property = item[0];
const order = item[1];
if (typeof property === STRING && (order === ORDER_ASC || order === ORDER_DESC)) {
continue;
}
}
result = false;
break;
}
}
return result;
}
/**
* Sorts an array based on sorting specifier options.
* @param {Array} list The that needs to be sorted.
* @param {Array} specifiers An array of specifiers. Please read isValidSortingSpecifier method definition for further details.
* @returns {void} No value is returned. The array is sorted in place.
*/
static sortListBy(list, specifiers) {
const thisClass = TypeSafeCollection;
if (list instanceof Array && thisClass.isValidSortingSpecifier(specifiers)) {
const getPropertyValue = thisClass.getPropertyValue;
const specifierCount = specifiers.length;
list.sort((a, b) => {
let index = 0;
while (index < specifierCount) {
const specifier = specifiers[index];
const property = specifier[0];
const order = specifier[1] === ORDER_DESC ? -1 : 1;
const aValue = getPropertyValue(a, property);
const bValue = getPropertyValue(b, property);
// @TODO: should we check for the types being compared, like:
// ~~ if (typeof aValue !== typeof bValue) continue;
// Not sure because dates, for example, can be correctly compared to numbers...
if (aValue < bValue) {
return order * -1;
}
if (aValue > bValue) {
return order * 1;
}
if (++index >= specifierCount) {
return 0;
}
}
});
} else {
throw new Error('TypeSafeCollection::sortListBy Invalid Arguments');
}
}
/**
* Public Methods
*/
updateById(id, payload) {
let result = false,
found = this._elementWithPayload(payload, true);
if (found) {
// nothing to do since the element is already in the collection...
if (found.id === id) {
// set result to true since the ids match...
result = true;
this._invalidate();
}
} else {
found = this._elementWithId(id, true);
if (found) {
found.payload = payload;
result = true;
this._invalidate();
}
}
return result;
}
update(payload) {
let result = false,
found = this._elementWithPayload(payload, true);
if (found) {
// nothing to do since the element is already in the collection...
result = true;
this._invalidate();
}
return result;
}
insert(payload) {
let id = null,
found = this._elementWithPayload(payload, true);
if (!found) {
id = Random.id();
this._elements(true).push({ id, payload });
this._invalidate();
}
return id;
}
removeAll() {
let all = this._elements(true),
length = all.length;
for (let i = length - 1; i >= 0; i--) {
let item = all[i];
delete item.id;
delete item.payload;
all[i] = null;
}
all.splice(0, length);
this._invalidate();
}
remove(propertyMap) {
let found = this.findAllBy(propertyMap),
foundCount = found.length,
removed = [];
if (foundCount > 0) {
const all = this._elements(true);
for (let i = foundCount - 1; i >= 0; i--) {
let item = found[i];
all.splice(item[2], 1);
removed.push(item[0]);
}
this._invalidate();
}
return removed;
}
getElementId(payload) {
let found = this._elementWithPayload(payload);
return found && found.id;
}
findById(id) {
let found = this._elementWithId(id);
return found && found.payload;
}
indexOfElement(payload) {
return this._elements().indexOf(this._elementWithPayload(payload, true));
}
indexOfId(id) {
return this._elements().indexOf(this._elementWithId(id, true));
}
getElementByIndex(index) {
let found = ((this._elements())[index >= 0 ? index : -1]);
return found && found.payload;
}
// the reactive var will not be notified if no valid callback is supplied...
find(callback) {
let found;
if (typeof callback === FUNCTION) {
found = this._elements().find((item, index) => {
return callback.call(this, item.payload, index);
});
}
return found && found.payload;
}
// the reactive var will not be notified if no valid property map is supplied...
findBy(propertyMap) {
let found;
if (typeof propertyMap === OBJECT && propertyMap !== null) {
const hasOwn = Object.prototype.hasOwnProperty;
found = this._elements().find(item => {
const payload = item.payload;
for (let propertyName in propertyMap) {
if (hasOwn.call(propertyMap, propertyName)) {
if (propertyMap[propertyName] !== TypeSafeCollection.getPropertyValue(payload, propertyName)) {
return false;
}
}
}
return true;
});
}
return found && found.payload;
}
/**
* Find all elements that match a specified property map.
* Attention!!! The reactive source will not be notified if no valid property map is supplied...
* @param {Object} propertyMap A property map that will be macthed against all collection elements.
* @param {Object} options A set of options. Currently only "options.sort" option is supported.
* @param {Object.SortingSpecifier} options.sort An optional sorting specifier. If a sorting specifier is supplied
* but it's not valid, an exception will be thrown.
* @returns {Array} An array with all elements stored in the collection.
*/
findAllBy(propertyMap, options) {
let found = [];
if (typeof propertyMap === OBJECT && propertyMap !== null) {
const hasOwn = Object.prototype.hasOwnProperty;
this._elements().forEach((item, index) => {
const payload = item.payload;
for (let propertyName in propertyMap) {
if (hasOwn.call(propertyMap, propertyName)) {
if (propertyMap[propertyName] !== TypeSafeCollection.getPropertyValue(payload, propertyName)) {
return; // skip this element since it does not match the criteria...
}
}
}
// Match! Add it to the found list...
found.push([ payload, item.id, index ]);
});
}
if (options instanceof Object && 'sort' in options) {
TypeSafeCollection.sortListBy(found, options.sort);
}
return found;
}
// the reactive var will not be notified if no valid callback is supplied...
forEach(callback) {
if (typeof callback === FUNCTION) {
this._elements().forEach((item, index) => {
callback.call(this, item.payload, item.id, index);
});
}
}
/**
* Count the number of elements currently in the collection.
* @returns {number} The current number of elements in the collection.
*/
count() {
return this._elements().length;
}
/**
* Returns a list with all elements of the collection optionally sorted by a sorting specifier criteria.
* @param {Object} options A set of options. Currently only "options.sort" option is supported.
* @param {Object.SortingSpecifier} options.sort An optional sorting specifier. If a sorting specifier is supplied
* but it's not valid, an exception will be thrown.
* @returns {Array} An array with all elements stored in the collection.
*/
all(options) {
let list = this._elements().map(item => item.payload);
if (options instanceof Object && 'sort' in options) {
TypeSafeCollection.sortListBy(list, options.sort);
}
return list;
}
}
@@ -0,0 +1,173 @@
import { Metadata } from './Metadata';
const UNDEFINED = 'undefined';
const NUMBER = 'number';
const STRING = 'string';
const REGEX_TAG = /^x[0-9a-f]{8}$/;
export class InstanceMetadata extends Metadata {
constructor(data) {
super(data);
this._sopInstanceUID = null;
}
/**
* Returns the SOPInstanceUID of the current instance.
*/
getSOPInstanceUID() {
return this._sopInstanceUID;
}
// @TODO: Improve this... (E.g.: blob data)
getStringValue(tagOrProperty, index, defaultValue) {
let value = this.getRawValue(tagOrProperty, defaultValue);
if (typeof value !== STRING && typeof value !== UNDEFINED) {
value = value.toString();
}
return InstanceMetadata.getIndexedValue(value, index, defaultValue);
}
// @TODO: Improve this... (E.g.: blob data)
getFloatValue(tagOrProperty, index, defaultValue) {
let value = this.getRawValue(tagOrProperty, defaultValue);
value = InstanceMetadata.getIndexedValue(value, index, defaultValue);
if(value instanceof Array) {
value.forEach( (val, idx) => {
value[idx] = parseFloat(val);
});
return value;
}
return typeof value === STRING ? parseFloat(value) : value;
}
// @TODO: Improve this... (E.g.: blob data)
getIntValue(tagOrProperty, index, defaultValue) {
let value = this.getRawValue(tagOrProperty, defaultValue);
value = InstanceMetadata.getIndexedValue(value, index, defaultValue);
if(value instanceof Array) {
value.forEach( (val, idx) => {
value[idx] = parseFloat(val);
});
return value;
}
return typeof value === STRING ? parseInt(value) : value;
}
/**
*
*/
getRawValue(tagOrProperty, defaultValue) {
/**
* Please override this method on a specialized class.
*/
}
/**
* Compares the current instance with another one.
* @param {InstanceMetadata} instance An instance of the InstanceMetadata class.
* @returns {boolean} Returns true if both instances refer to the same instance.
*/
equals(instance) {
const self = this;
return (
instance === self ||
(
instance instanceof InstanceMetadata &&
instance.getSOPInstanceUID() === self.getSOPInstanceUID()
)
);
}
/**
* Check if the tagOrProperty exists
* @param {String} tagOrProperty tag or property be checked
* @return {Boolean} True if the tag or property exists or false if doesn't
*/
tagExists(tagOrProperty) {
/**
* Please override this method
*/
}
/**
* Returns a URI for the specified resource related to this instance (if an instance).
* @param {string} resource The name of the resource. This name cannot have any leading or
* trailing slashes
* @param {string|number} [optParameter] An optional parameter. This cannot have any leading or
* trailing slashes
* @returns {string|undefined} The constructed URI for the specified resource related to this
* instance or undefined if this sequence is a sub-sequence (i.e.: it has no related resources of
* its own).
*/
getResourceUri(resource, optParameter) {
/**
* Please override this method
*/
}
/**
* Static Methods
*/
static getTagInfo(tagOrProperty) {
let tagName = null,
propertyName = null;
if (typeof tagOrProperty === NUMBER) {
// if it's a number, build an hexadecimal representation...
tagName = 'x' + ('00000000' + tagOrProperty.toString(16)).substr(-8);
} else if (typeof tagOrProperty === STRING && REGEX_TAG.test(tagOrProperty)) {
tagName = tagOrProperty;
} else {
// use it as a property name otherwise...
propertyName = tagOrProperty;
}
// @TODO: map tagName to propertyName and vice versa
return { tagName, propertyName };
}
/**
* Get an value based that can be index based. This function is called by all getters. See above functions.
* - If value is a String and has indexes:
* - If undefined index: returns an array of the split values.
* - If defined index:
* - If invalid: returns defaultValue
* - If valid: returns the indexed value
* - If value is not a String, returns default value.
*/
static getIndexedValue(value, index, defaultValue) {
let result = defaultValue;
if (typeof value === STRING) {
const hasIndexValues = value.indexOf('\\') !== -1;
result = value;
if(hasIndexValues) {
const splitValues = value.split('\\');
if (Metadata.isValidIndex(index)) {
const indexedValue = splitValues[index];
result = typeof indexedValue !== STRING ? defaultValue : indexedValue;
}
else {
result = splitValues;
}
}
}
return result;
}
}
@@ -0,0 +1,40 @@
/**
* Constants
*/
const STRING = 'string';
const NUMBER = 'number';
const FUNCTION = 'function';
export class Metadata {
/**
* Constructor and Instance Methods
*/
constructor(data) {
this._data = data;
}
getData() {
return this._data;
}
/**
* Static Methods
*/
static isValidUID(uid) {
return typeof uid === STRING && uid.length > 0;
}
static isValidIndex(index) {
return typeof index === NUMBER && index >= 0 && (index | 0) === index;
}
static isValidCallback(callback) {
return typeof callback === FUNCTION;
}
}
@@ -0,0 +1,128 @@
# Study Metadata Module
This module defines the API/Data-Model by which OHIF Viewerbase package and possibly distinct viewer
implementations can access studies metadata. This module does not attempt to define any means of
*loading* study metadata from any data end-point but only how the data that has been previously
loaded into the application context will be accessed by any of the routines or algorithm implementations
that need the data.
## Intro
For various reasons like sorting, grouping or simply rendering study information, OHIF Viewerbase package
and applications depending on it usualy have the need to access study metadata. Before the current
initiative there was no uniform way of achieving that since each implementation provides study metadata
on its own specific ways. The application and the package itself needed to have a deep knowledge of the
data structures provided by the data endpoint to perform any of the operations mentioned above, meaning
that any data access code needed to be adapted or rewritten.
The intent of the current module is to provide a fairly consistent and flexible API/Data-Model by which
OHIF Viewerbase package (and different viewer implementations that depend on it) can manipulate DICOM matadata
retrieved from distinct data end points (e.g., a proprietary back end servers) in uniform ways with minor
to no modifications needed.
## Implementation
The current API implementation defines three classes of objects: `StudyMetadata`, `SeriesMetadata`
and `InstanceMetadata`. Inside OHIF Viewerbase package, every access to Study, Series or SOP Instance
metadata is achieved by the interface exposed by these three classes. By inheriting from them and
overriding or extending their methods, different applications with different data models can adapt
even the most peculiar data structures to the uniform interface defined by those classes. Together
these classes define a flexible and extensible data manipulation layer leaving routines and
algorithms that depend on that data untouched.
## Design Decisions & "*Protected*" Members
In order to provide for good programming practices, attributes and methods meant to be used exclusevily by
the classes themselves (for internal purposes only) were written with an initial '_' character, being thus treated
as "*protected*" members. The idea behind this practice was never to hide them from the programmers
(what makes debugging tasks painful) but only advise for something that's not part of the official public API
and thus should not be relied on. Usage of "protected" members makes the code less readable and prone to
compatibility issues.
As an example, the initial implementation of the `StudyMetadata` class defined the attribute `_studyInstanceUID`
and the method `getStudyInstanceUID`. This implies that whenever the *StudyInstanceUID* of a given study needs
to be retrieved the `getStudyInstanceUID` method should be called instead of directly accessing the
attribute `_studyInstanceUID` (which might not even be populated since `getStudyInstanceUID` can be possiblity
overriden by a subclass to satisfy specific implementation needs, leaving the attribute `_studyInstanceUID` unused).
Ex:
```javascript
let studyUID = myStudy.getStudyInstanceUID(); // GOOD! :-)
[ ... ]
let otherStudyUID = anotherStudy._studyInstanceUID; // BAD... :-(
```
Another important topic is the preference of *methods* over *attributes* on the public API. This design
decision was made to ensure extensibility and flexibility (methods are extensible while standalone
attributes are not, and can be adapted – through overrides, for example – to support even the most
peculiar data models) even though the overhead a few additional function calls may incur.
## Abstract Classes
Some classes defined in this module are "*abstract*" classes (even though JavaScript does not *officially*
support such programming facility). They are *abstract* in the sense that a few methods (very important ones,
by the way) were left "*blank*" (unimplemented, or more precisely implemented as empty NOP functions) in
order to be implemented by specialized subclasses. Methods believed to be more generic were implemented in
an attempt to satify most implementation needs but nothing prevents a subclass from overriding them as well
(again, flexibility and extensibility are design goals). Most implemented methods rely on the implementation
of an unimplemented method. For example, the method `getStringValue` from `InstanceMetadata` class, which
has indeed been implemented and is meant to retrieve a metadata value as a string, internally calls the
`getRawValue` method which *was NOT implemented* and is meant to query the internal data structures for the
requested metadata value and return it *as is*. Used in that way, an application would not benefit much
from the already implemented methods. On the other hand, by simply overriding the `getRawValue` method
on a specialized class to deal with the intrinsics of its internal data structures, this very application
would now benefit from all already implemented methods.
The following code snippet tries to illustrate the idea:
```javascript
// -- InstanceMetadata.js
class InstanceMetadata {
[ ... ]
getRawValue(tagOrProperty, defaultValue) {
// Please implement this method in a specialized subclass...
}
[ ... ]
getStringValue(tagOrProperty, index, defaultValue) {
let rawValue = this.getRawValue(tagOrProperty, '');
// parse the returned value into a string...
[ ... ]
return stringValue;
}
[ ... ]
}
// -- MyFancyAppInstanceMetadata.js
class MyFancyAppInstanceMetadata extends InstanceMetadata {
// Overriding this method will make all methods implemented in the super class
// that rely on it to be immediately available...
getRawValue(tagOrProperty, defaultValue) {
let rawValue;
// retrieve raw value from internal data structures...
[ ... ]
return rawValue;
}
}
// -- main.js
[ ... ]
let sopInstaceMetadata = new MyFancyAppInstanceMetadata(myInternalData);
if (sopInstaceMetadata instanceof MyFancyAppInstanceMetadata) { // true
// this code will be executed...
}
if (sopInstaceMetadata instanceof InstanceMetadata) { // also true
// this code will also be executed...
}
// The following will also work since the internal "getRawValue" call inside
// "getStringValue" method will now be satisfied... (thanks to the override)
let patientName = sopInstaceMetadata.getStringValue('PatientName', '');
[ ... ]
```
_Copyright &copy; 2016 nucleushealth&trade;. All rights reserved_
@@ -0,0 +1,109 @@
import { Metadata } from './Metadata';
import { InstanceMetadata } from './InstanceMetadata';
export class SeriesMetadata extends Metadata {
constructor(data) {
super(data);
this._seriesInstanceUID = null;
this._instances = []; // InstanceMetadata[]
}
/**
* Returns the SeriesInstanceUID of the current series.
*/
getSeriesInstanceUID() {
return this._seriesInstanceUID;
}
/**
* Append an instance to the current series.
* @param {InstanceMetadata} instance The instance to be added to the current series.
* @returns {boolean} Returns true on success, false otherwise.
*/
addInstance(instance) {
let result = false;
if (instance instanceof InstanceMetadata && this.getInstanceByUID(instance.getSOPInstanceUID()) === void 0) {
this._instances.push(instance);
result = true;
}
return result;
}
/**
* Find an instance by index.
* @param {number} index An integer representing a list index.
* @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
*/
getInstanceByIndex(index) {
let found; // undefined by default...
if (Metadata.isValidIndex(index)) {
found = this._instances[index];
}
return found;
}
/**
* Find an instance by SOPInstanceUID.
* @param {string} uid An UID string.
* @returns {InstanceMetadata} Returns a InstanceMetadata instance when found or undefined otherwise.
*/
getInstanceByUID(uid) {
let found; // undefined by default...
if (Metadata.isValidUID(uid)) {
found = this._instances.find(instance => {
return instance.getSOPInstanceUID() === uid;
});
}
return found;
}
/**
* Retrieve the number of instances within the current series.
* @returns {number} The number of instances in the current series.
*/
getInstanceCount() {
return this._instances.length;
}
/**
* Invokes the supplied callback for each instance in the current series passing
* two arguments: instance (an InstanceMetadata instance) and index (the integer
* index of the instance within the current series)
* @param {function} callback The callback function which will be invoked for each instance in the series.
* @returns {undefined} Nothing is returned.
*/
forEachInstance(callback) {
if (Metadata.isValidCallback(callback)) {
this._instances.forEach((instance, index) => {
callback.call(null, instance, index);
});
}
}
/**
* Find the index of an instance inside the series.
* @param {InstanceMetadata} instance An instance of the SeriesMetadata class.
* @returns {number} The index of the instance inside the series or -1 if not found.
*/
indexOfInstance(instance) {
return this._instances.indexOf(instance);
}
/**
* Compares the current series with another one.
* @param {SeriesMetadata} series An instance of the SeriesMetadata class.
* @returns {boolean} Returns true if both instances refer to the same series.
*/
equals(series) {
const self = this;
return (
series === self ||
(
series instanceof SeriesMetadata &&
series.getSeriesInstanceUID() === self.getSeriesInstanceUID()
)
);
}
}
@@ -0,0 +1,134 @@
import { Metadata } from './Metadata';
import { SeriesMetadata } from './SeriesMetadata';
export class StudyMetadata extends Metadata {
constructor(data) {
super(data);
this._studyInstanceUID = null;
this._series = []; // SeriesMetadata[]
}
/**
* Returns the StudyInstanceUID of the current study.
*/
getStudyInstanceUID() {
return this._studyInstanceUID;
}
/**
* Append a series to the current study.
* @param {SeriesMetadata} series The series to be added to the current study.
* @returns {boolean} Returns true on success, false otherwise.
*/
addSeries(series) {
let result = false;
if (series instanceof SeriesMetadata && this.getSeriesByUID(series.getSeriesInstanceUID()) === void 0) {
this._series.push(series);
result = true;
}
return result;
}
/**
* Find a series by index.
* @param {number} index An integer representing a list index.
* @returns {SeriesMetadata} Returns a SeriesMetadata instance when found or undefined otherwise.
*/
getSeriesByIndex(index) {
let found; // undefined by default...
if (Metadata.isValidIndex(index)) {
found = this._series[index];
}
return found;
}
/**
* Find a series by SeriesInstanceUID.
* @param {string} uid An UID string.
* @returns {SeriesMetadata} Returns a SeriesMetadata instance when found or undefined otherwise.
*/
getSeriesByUID(uid) {
let found; // undefined by default...
if (Metadata.isValidUID(uid)) {
found = this._series.find(series => {
return series.getSeriesInstanceUID() === uid;
});
}
return found;
}
/**
* Retrieve the number of series within the current study.
* @returns {number} The number of series in the current study.
*/
getSeriesCount() {
return this._series.length;
}
/**
* Retrieve the number of instances within the current study.
* @returns {number} The number of instances in the current study.
*/
getInstanceCount() {
return this._series.reduce((sum, series) => {
return sum + series.getInstanceCount();
}, 0);
}
/**
* Invokes the supplied callback for each series in the current study passing
* two arguments: series (a SeriesMetadata instance) and index (the integer
* index of the series within the current study)
* @param {function} callback The callback function which will be invoked for each series instance.
* @returns {undefined} Nothing is returned.
*/
forEachSeries(callback) {
if (Metadata.isValidCallback(callback)) {
this._series.forEach((series, index) => {
callback.call(null, series, index);
});
}
}
/**
* Find the index of a series inside the study.
* @param {SeriesMetadata} series An instance of the SeriesMetadata class.
* @returns {number} The index of the series inside the study or -1 if not found.
*/
indexOfSeries(series) {
return this._series.indexOf(series);
}
/**
* Compares the current study instance with another one.
* @param {StudyMetadata} study An instance of the StudyMetadata class.
* @returns {boolean} Returns true if both instances refer to the same study.
*/
equals(study) {
const self = this;
return (
study === self ||
(
study instanceof StudyMetadata &&
study.getStudyInstanceUID() === self.getStudyInstanceUID()
)
);
}
/**
* Get first instance of the first series
* @return {InstanceMetadata} InstanceMetadata class object or undefined if it doenst exist
*/
getFirstInstance() {
let firstInstance;
const firstSeries = this.getSeriesByIndex(0);
if (firstSeries) {
firstInstance = firstSeries.getInstanceByIndex(0);
}
return firstInstance;
}
}