Initial work on Nucleus parity merge

This commit is contained in:
Emanuel F. Oliveira 2017-01-16 12:47:29 -02:00
parent aa88fe181a
commit 680beb163f
38 changed files with 4353 additions and 1 deletions

View File

@ -12,7 +12,8 @@ const OHIF = {
};
// Expose the OHIF object to the client if it is on development mode
if (Meteor.isDevelopment && Meteor.isClient) {
// @TODO: remove this after applying namespace to this package
if (Meteor.isClient) {
window.OHIF = OHIF;
}

View File

@ -0,0 +1,99 @@
<template name="textMarkerDialogs">
<dialog id="textMarkerOptionsDialog" oncontextmenu="return false" class="textMarkerDialog noselect">
<h5>Spine Labels</h5>
<a class="closeTextMarkerDialogs">{{> iconCrossCircle}}</a>
<div class='optionsDiv'>
<div class="select2-wrapper optionBox">
<label for="startFrom">Label: </label>
<select name="start" id="startFrom" tabindex="-1" class="select2">
<!-- Cervical spine !-->
<option value="C1" selected>C1</option>
<option value="C2">C2</option>
<option value="C3">C3</option>
<option value="C4">C4</option>
<option value="C5">C5</option>
<option value="C6">C6</option>
<option value="C7">C7</option>
<!-- Thoracic spine !-->
<option value="T1">T1</option>
<option value="T2">T2</option>
<option value="T3">T3</option>
<option value="T4">T4</option>
<option value="T5">T5</option>
<option value="T6">T6</option>
<option value="T7">T7</option>
<option value="T8">T8</option>
<option value="T9">T9</option>
<option value="T10">T10</option>
<option value="T11">T11</option>
<option value="T12">T12</option>
<!-- Lumbar spine !-->
<option value="L1">L1</option>
<option value="L2">L2</option>
<option value="L3">L3</option>
<option value="L4">L4</option>
<option value="L5">L5</option>
</select>
</div>
<div class="optionBox">
<div class="btn-group iconSwitch" data-toggle="buttons">
<label for="ascending" class="btn btn-link" title="Add labels from head to feet (down) or feet to head (up)" data-toggle="tooltip" data-container="body">
<input type="checkbox" id="ascending"> <span class="on"><i class="fa fa-long-arrow-up" aria-hidden="true"></i></span> <span class="off"><i class="fa fa-long-arrow-down" aria-hidden="true"></i></span>
</label>
</div>
</div>
<button id="clearLabels" class="btn viewerBtn btn-xs" type="button">
Clear Labels <i class="viewerIcon-clear"></i>
</button>
</div>
</dialog>
<dialog id="textMarkerRelabelDialog" oncontextmenu="return false" class="textMarkerDialog noselect">
<h5>Change Spine Label</h5>
<div class="relabelOptions">
<div class="select2-wrapper optionBox">
<label for="relabelSelect">Label:</label>
<select name="relabelSelect" class="relabelSelect">
<!-- Cervical spine !-->
<option value="C1">C1</option>
<option value="C2">C2</option>
<option value="C3">C3</option>
<option value="C4">C4</option>
<option value="C5">C5</option>
<option value="C6">C6</option>
<option value="C7">C7</option>
<!-- Thoracic spine !-->
<option value="T1">T1</option>
<option value="T2">T2</option>
<option value="T3">T3</option>
<option value="T4">T4</option>
<option value="T5">T5</option>
<option value="T6">T6</option>
<option value="T7">T7</option>
<option value="T8">T8</option>
<option value="T9">T9</option>
<option value="T10">T10</option>
<option value="T11">T11</option>
<option value="T12">T12</option>
<!-- Lumbar spine !-->
<option value="L1">L1</option>
<option value="L2">L2</option>
<option value="L3">L3</option>
<option value="L4">L4</option>
<option value="L5">L5</option>
</select>
</div>
</div>
<div class="relabelButtons">
<button class="relabelRemove btn btn-xs btn-secondary viewerBtn">Delete</button>
<button class="relabelConfirm btn btn-xs viewerBtn">Save</button>
</div>
<div class="dialog arrow">
</div>
</dialog>
</template>

View File

@ -0,0 +1,124 @@
/**
* @TODO: add this to OHIF's Viewers
*/
import { Template } from 'meteor/templating';
import { $ } from 'meteor/jquery';
import { toolManager } from '../../../lib/toolManager';
import { viewportUtils } from '../../../lib/viewportUtils';
Template.textMarkerDialogs.events({
'change #startFrom'(e) {
const config = cornerstoneTools.textMarker.getConfiguration();
config.current = $(e.target).val();
//console.log("Changed starting point to: " + config.current);
},
'change #ascending'(e) {
const config = cornerstoneTools.textMarker.getConfiguration();
config.ascending = $(e.target).is(':checked');
const currentIndex = config.markers.indexOf(config.current);
config.current = config.markers[currentIndex];
const nextMarker = config.current;
$('#startFrom').val(nextMarker).trigger('change');
//console.log("Changed ascending to: " + config.ascending);
},
'click #clearLabels'() {
const element = viewportUtils.getActiveViewportElement();
const toolType = 'textMarker';
const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
const toolState = toolStateManager.toolState;
// We might want to make this a convenience function in cornerstoneTools
const stack = cornerstoneTools.getToolState(element, 'stack');
if (stack && stack.data.length && stack.data[0].imageIds.length) {
const imageIds = stack.data[0].imageIds;
// Clear the tool data for each image in the stack
imageIds.forEach( imageId => {
if(toolState.hasOwnProperty(imageId)) {
const toolData = toolState[imageId];
if (toolData.hasOwnProperty(toolType)) {
delete toolData[toolType];
}
}
});
}
cornerstone.updateImage(element);
},
'click .closeTextMarkerDialogs'() {
const defaultTool = toolManager.getDefaultTool();
toolManager.setActiveTool(defaultTool);
document.getElementById('textMarkerOptionsDialog').close();
$('#spine').removeClass('active');
$('#' + defaultTool).addClass('active');
}
});
Template.textMarkerDialogs.onRendered(function() {
const optionsDialog = $('#textMarkerOptionsDialog');
optionsDialog.draggable();
dialogPolyfill.registerDialog(optionsDialog.get(0));
const relabelDialog = $('#textMarkerRelabelDialog');
relabelDialog.draggable();
dialogPolyfill.registerDialog(relabelDialog.get(0));
$(document).on('click', event => {
if (!$(event.target).closest('.select2-wrapper').length) {
setTimeout(() => {
$('#startFrom, .relabelSelect').select2('close');
}, 200);
}
});
$(document).on('touchmove', event => {
if (!$(event.target).closest('.select2-container').length) {
setTimeout(() => {
$('#startFrom, .relabelSelect').select2('close');
}, 200);
}
});
$(() => {
FastClick.attach(document.body);
const $customSelects = $('#startFrom, .relabelSelect')
$customSelects.select2({
/**
* Adds needsclick class to all DOM elements in the Select2 results list
* so they can be accessible on iOS mobile when FastClick is initiated too.
*/
templateResult(result, container) {
if (!result.id) {
return result.text;
}
container.className += ' needsclick';
return result.text;
},
placeholder: 'C1',
minimumResultsForSearch: -1,
theme: 'viewerDropdown'
});
/**
* Additional to tweaking the templateResult option in Select2,
* add needsclick class to all DOM elements in the Select2 container,
* so they can be accessible on iOS mobile when FastClick is initiated too.
*
* More info about needsclick:
* https://github.com/ftlabs/fastclick#ignore-certain-elements-with-needsclick
*
*/
$customSelects.each( (index, el) =>{
$(el).data('select2').$container.find('*').addClass('needsclick');
});
});
});

View File

@ -0,0 +1,147 @@
.textMarkerDialog
z-index: 1000
width: 260px
position: absolute
top: auto
left: auto
bottom: 3px
right: 3px
margin: auto
padding:.5em
background-image: linear-gradient(#2E608D, #23557F)
box-sizing: border-box
border: 1px black solid
border-radius: 5px
height: 81px
max-height: 81px
h5
color: #D1EDFB;
font-size: 14px;
line-height: 22px;
font-weight: 600;
cursor:default
.handle
width: 40px;
float: left;
height: 22px;
display: block;
margin-right: 10px;
position: relative;
margin-top: 1px;
margin-left: 1px;
#startFrom
margin-right:10px
#clearLabels
margin-left:10px
float:right
.optionBox
color: #d1edfb
margin-bottom: 5px
float: left
span.subLabel
width: 50px
display: block
float: left
line-height: 30px
> label
color: #d1edfb
float: left
min-width: 40px
line-height: 24px
font-size: 12px
font-weight: 600
.closeTextMarkerDialogs
position: absolute
top: 5px
right: 4px
padding: 5px
cursor: pointer
svg
width: 19px
height: 19px
fill: #d1edfb
&:hover
svg
fill: #FFF
.dialog.arrow
left: 50%
margin-left: -11px
border-top-width: 0
border-bottom-color: #999999
border-bottom-color: rgba(0, 0, 0, 0.25)
border-width: 11px
bottom: -10px
position: absolute
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid #23557f;
&:after
content: " "
top: 1px
margin-left: -10px
border-top-width: 0
border-bottom-color: #23557f
border-width: 10px
#textMarkerRelabelDialog
margin: 0
.relabelOptions
padding: 15px 0px 0px 0px
.relabelSelect
margin-left: 5px
#textMarkerOptionsDialog
.optionsDiv
padding-top: 10px
.relabelButtons
text-align:right
button.viewerBtn
background-color: #5D9ACE;
border: 1px solid #6EAEE4;
font-size: 12px !important;
color:#FFF
line-height: 21px;
&:hover
color:#FFF
button.btn-secondary
background-color: #CFE3F5;
border: 1px solid #E5F3FF;
font-size: 12px !important;
color:#0D416D
&:hover
color:#0D416D
.iconSwitch
margin:0px 5px
.btn
span
display: none
.on
display: none
.off
display: block
&.btn-link, &.active, &:active, &:hover, &:focus, &.active:focus
background-color: #225682;
border: 1px solid #7AB0DE;
box-shadow: none;
padding: 1px 7px;
border-radius: 4px;
color: #d1edfb;
box-shadow: none;
outline: none;
&:hover
color:#fff
&.active
.on
display:block
.off
display: none

View File

@ -0,0 +1,191 @@
import { Viewerbase } from '../namespace';
/**
* Exported Functions
*/
// getElementIfNotEmpty
import { getElementIfNotEmpty } from './lib/getElementIfNotEmpty';
Viewerbase.getElementIfNotEmpty = getElementIfNotEmpty;
// getStackDataIfNotEmpty
import { getStackDataIfNotEmpty } from './lib/getStackDataIfNotEmpty';
Viewerbase.getStackDataIfNotEmpty = getStackDataIfNotEmpty;
// switchToImageRelative
import { switchToImageRelative } from './lib/switchToImageRelative';
Viewerbase.switchToImageRelative = switchToImageRelative;
// switchToImageByIndex
import { switchToImageByIndex } from './lib/switchToImageByIndex';
Viewerbase.switchToImageByIndex = switchToImageByIndex;
// getFrameOfReferenceUID
import { getFrameOfReferenceUID } from './lib/getFrameOfReferenceUID';
Viewerbase.getFrameOfReferenceUID = getFrameOfReferenceUID;
// updateCrosshairsSynchronizer
import { updateCrosshairsSynchronizer } from './lib/updateCrosshairsSynchronizer';
Viewerbase.updateCrosshairsSynchronizer = updateCrosshairsSynchronizer;
// getImageId
import { getImageId } from './lib/getImageId';
Viewerbase.getImageId = getImageId;
// setActiveViewport
import { setActiveViewport } from './lib/setActiveViewport';
Viewerbase.setActiveViewport = setActiveViewport;
// classifyImageOrientation
import { classifyImageOrientation } from './lib/classifyImageOrientation';
Viewerbase.classifyImageOrientation = classifyImageOrientation;
// setFocusToActiveViewport
import { setFocusToActiveViewport } from './lib/setFocusToActiveViewport';
Viewerbase.setFocusToActiveViewport = setFocusToActiveViewport;
// getWADORSImageId
import { getWADORSImageId } from './lib/getWADORSImageId';
Viewerbase.getWADORSImageId = getWADORSImageId;
// updateAllViewports
import { updateAllViewports } from './lib/updateAllViewports';
Viewerbase.updateAllViewports = updateAllViewports;
// sortStudy
import { sortStudy } from './lib/sortStudy';
Viewerbase.sortStudy = sortStudy;
// updateOrientationMarkers
import { updateOrientationMarkers } from './lib/updateOrientationMarkers';
Viewerbase.updateOrientationMarkers = updateOrientationMarkers;
// isImage
import { isImage } from './lib/isImage';
Viewerbase.isImage = isImage;
// getInstanceClassDefaultViewport, setInstanceClassDefaultViewportFunction
import { getInstanceClassDefaultViewport, setInstanceClassDefaultViewportFunction } from './lib/instanceClassSpecificViewport';
Viewerbase.getInstanceClassDefaultViewport = getInstanceClassDefaultViewport;
Viewerbase.setInstanceClassDefaultViewportFunction = setInstanceClassDefaultViewportFunction;
// setMammogramViewportAlignment
import { setMammogramViewportAlignment } from './lib/setMammogramViewportAlignment';
Viewerbase.setMammogramViewportAlignment = setMammogramViewportAlignment;
// addMetaData, addSpecificMetadata, updateMetaData
import { addMetaData, addSpecificMetadata, updateMetaData } from './lib/metaDataProvider';
Viewerbase.addMetaData = addMetaData;
Viewerbase.addSpecificMetadata = addSpecificMetadata;
Viewerbase.updateMetaData = updateMetaData;
/**
* Exported Namespaces (sub-namespaces)
*/
// imageViewerViewportData.*
import { imageViewerViewportData } from './lib/imageViewerViewportData';
Viewerbase.imageViewerViewportData = imageViewerViewportData;
// panelNavigation.*
import { panelNavigation } from './lib/panelNavigation';
Viewerbase.panelNavigation = panelNavigation;
// seriesNavigation.*
import { seriesNavigation } from './lib/seriesNavigation';
Viewerbase.seriesNavigation = seriesNavigation;
// WLPresets.*
import { WLPresets } from './lib/WLPresets';
Viewerbase.wlPresets = WLPresets;
// hotkeyUtils.*
import { hotkeyUtils } from './lib/hotkeyUtils';
Viewerbase.hotkeyUtils = hotkeyUtils;
// viewportOverlayUtils.*
import { viewportOverlayUtils } from './lib/viewportOverlayUtils';
Viewerbase.viewportOverlayUtils = viewportOverlayUtils;
// viewportUtils.*
import { viewportUtils } from './lib/viewportUtils';
Viewerbase.viewportUtils = viewportUtils;
// thumbnailDragHandlers.*
import { thumbnailDragHandlers } from './lib/thumbnailDragHandlers';
Viewerbase.thumbnailDragHandlers = thumbnailDragHandlers;
// dialogUtils.*
import { dialogUtils } from './lib/dialogUtils';
Viewerbase.dialogUtils = dialogUtils;
// unloadHandlers.*
import { unloadHandlers } from './lib/unloadHandlers';
Viewerbase.unloadHandlers = unloadHandlers;
// sortingManager.*
import { sortingManager } from './lib/sortingManager';
Viewerbase.sortingManager = sortingManager;
// crosshairsSynchronizers.*
import { crosshairsSynchronizers } from './lib/crosshairsSynchronizers';
Viewerbase.crosshairsSynchronizers = crosshairsSynchronizers;
/**
* Exported Singletons
*/
// StackManager as "stackManager" (since it's a plain object instance, the exported name starts with a lowercase letter)
import { StackManager } from './lib/StackManager';
Viewerbase.stackManager = StackManager;
// toolManager
import { toolManager } from './lib/toolManager';
Viewerbase.toolManager = toolManager;
/**
* Exported Helpers
*/
import { helpers } from './lib/helpers/';
Viewerbase.helpers = helpers;
/**
* Exported Collections
*/
// sopClassDictionary
import { sopClassDictionary } from './lib/sopClassDictionary';
Viewerbase.sopClassDictionary = sopClassDictionary;
/**
* Exported Classes
*/
// ImageSet
import { ImageSet } from './lib/classes/ImageSet';
Viewerbase.ImageSet = ImageSet;
// ResizeViewportManager
import { ResizeViewportManager } from './lib/classes/ResizeViewportManager';
Viewerbase.ResizeViewportManager = ResizeViewportManager;
// StudyMetadata, SeriesMetadata, InstanceMetadata
import { StudyMetadata } from './lib/classes/metadata/StudyMetadata';
import { SeriesMetadata } from './lib/classes/metadata/SeriesMetadata';
import { InstanceMetadata } from './lib/classes/metadata/InstanceMetadata';
Viewerbase.metadata = { StudyMetadata, SeriesMetadata, InstanceMetadata };
// TypeSafeCollection
import { TypeSafeCollection } from './lib/classes/TypeSafeCollection';
Viewerbase.TypeSafeCollection = TypeSafeCollection;
// OHIFError
import { OHIFError } from './lib/classes/OHIFError';
Viewerbase.OHIFError = OHIFError;
/**
* Imports for Side Effects Only
*/
import './lib/debugReactivity';

View File

@ -0,0 +1,64 @@
// Default Configuration to handle Metadata from WADO or DIMSE,
// currently retrieved through Meteor server
let configuration = {
// These are examples, they need to actually check the properties of the Objects
getSeries: (study, seriesNumOrUid) => {
// Use Array to find the appropriate Series
return study.seriesList.find(series => series['seriesNumber'] === value);
},
getInstance: (series, instanceNumOrUid) => {
return series.instances.find(instance => instance['instanceNumber'] === instanceNumOrUid)
}
getInstanceTagRaw: (instance, tag) => {
// (this won't actually work yet)
return instance[tag];
}
getInstanceTagNum: (instance, tag) => {
// (this won't actually work yet)
return parseFloat(instance[tag]);
}
};
// Example Nucleus configuration
//
configuration = {
getSeries: (study, seriesNumOrUid) => study.studyView.series(seriesNumOrInstanceUid),
getInstance: (series, instanceNumOrUid) => series.seriesView.instance(instanceNumOrUid),
getInstanceTagRaw: (instance, tag) => instance.instanceView.raw(tag),
getInstanceTagNum: (instance, tag) => instance.instanceView.num(tag),
getCustom(instance, custom) => instance.instanceView[custom]
}
class StudyMetadataView {
series(tag, value) {
// Retrieves Series by Series Number or SeriesInstanceUid
return configuration.getSeries(study, tag, value);
}
}
class SeriesMetadataView {
instances(tag, value) {
// Retrieves Instances by Instances Number or SOPInstanceUid
return configuration.getInstance(series, tag, value);
}
}
class InstanceMetadataView {
/* Retrieves raw value of tag from instance */
raw(tag) {
return configuration.getInstanceTagRaw(instance, tag);
}
/* Retrieves raw value of tag from instance */
num(tag) {
return configuration.getInstanceTagNum(instance, tag);
}
custom(custom) {
return configuration.getCustom(instance, custom)
}
// etc.. Date, DateTime, ...? Sequence...
}

View File

@ -0,0 +1,124 @@
import { OHIF } from 'meteor/ohif:core';
import { getImageId } from './getImageId';
import { addMetaData } from './metaDataProvider';
import { OHIFError } from './classes/OHIFError';
let stackMap = {};
let configuration = {};
const stackUpdatedCallbacks = [];
/**
* Loop through the current series and add metadata to the
* Cornerstone meta data provider. This will be used to fill information
* into the viewport overlays, and to calculate reference lines and orientation markers
* @param {Object} stackMap stackMap object
* @param {Object} study Study object
* @param {Object} displaySet The set of images to make the stack from
* @return {Array} Array with image IDs
*/
function createAndAddStack(stackMap, study, displaySet) {
const numImages = displaySet.images.length;
const imageIds = [];
let imageId;
displaySet.images.forEach((image, imageIndex) => {
const metaData = {
instance: image,
series: displaySet, // TODO: Check this
study: study,
numImages: numImages,
imageIndex: imageIndex + 1
};
const numFrames = image.numFrames;
if (numFrames > 1) {
OHIF.log.info('Multiframe image detected');
for (let i = 0; i < numFrames; i++) {
metaData.frame = i;
imageId = getImageId(image, i);
imageIds.push(imageId);
addMetaData(imageId, metaData);
}
}
else {
imageId = getImageId(image);
imageIds.push(imageId);
addMetaData(imageId, metaData);
}
});
return imageIds;
}
configuration = {
createAndAddStack: createAndAddStack
};
/**
* This object contains all the functions needed for interacting with the stack manager.
* Generally, findStack is the only function used. If you want to know when new stacks
* come in, you can register a callback with addStackUpdatedCallback.
* clearStacks and makeAndAddStack should not used outside of loadStudy and removeStudies.
*/
const StackManager = {
/**
* Removes all current stacks
*/
clearStacks() {
stackMap = {};
},
/**
* Create a stack from an image set, as well as add in the metadata on a per image bases.
* @param study The study who's metadata will be added
* @param displaySet The set of images to make the stack from
* @return {Array} Array with image IDs
*/
makeAndAddStack(study, displaySet) {
return configuration.createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks);
},
/**
* Find a stack from the currently created stacks.
* @param displaySetInstanceUid The UID of the stack to find.
* @returns {*} undefined if not found, otherwise the stack object is returned.
*/
findStack(displaySetInstanceUid) {
return stackMap[displaySetInstanceUid];
},
/**
* Gets the underlying map of displaySetInstanceUid to stack object.
* WARNING: Do not change this object. It directly affects the manager.
* @returns {{}} map of displaySetInstanceUid -> stack.
*/
getAllStacks() {
return stackMap;
},
/**
* Adds in a callback to be called on a stack being added / updated.
* @param callback must accept at minimum one argument,
* which is the stack that was added / updated.
*/
addStackUpdatedCallback(callback) {
if (typeof callback !== 'function') {
throw new OHIFError('callback must be provided as a function');
}
stackUpdatedCallbacks.push(callback);
},
/**
* Return configuration
*/
getConfiguration() {
return configuration;
},
/**
* Set configuration, in order to provide compatibility
* with other systems by overriding this functions
* @param {Object} config object with functions to be overrided
*
* For now, only makeAndAddStack can be overrided
*/
setConfiguration(config) {
configuration = config;
}
};
export { StackManager };

View File

@ -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];
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
};

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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_

View File

@ -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()
)
);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,57 @@
import { OHIF } from 'meteor/ohif:core';
/**
* This function returns a string describing an image orientation
* (e.g. axial, sagittal, coronal)
* If no classification is determined, it returns undefined
*
* @param {string} [imageId]
* @returns {string}
*/
export function classifyImageOrientation(imageId) {
// First we check if this imageId has already been classified, so we can return
// the classification from the cache
if (OHIF.viewer.linkSeries.classifiedOrientation.hasOwnProperty(imageId)) {
return OHIF.viewer.linkSeries.classifiedOrientation[imageId];
}
// Calculate the image plane normal
const imagePlane = cornerstoneTools.metaData.get('imagePlane', imageId);
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
return;
}
const imageNormal = imagePlane.rowCosines.clone().cross(imagePlane.columnCosines);
// These represent the normal vectors to three standard image planes
const normals = {
axial: new cornerstoneMath.Vector3(0, 0, 1),
coronal: new cornerstoneMath.Vector3(0, 1, 0),
sagittal: new cornerstoneMath.Vector3(1, 0, 0)
};
const PI = Math.PI;
// This loop checks the angle between the current image plane normal
// and that of the three standard image planes above. If the two
// normal vectors are within 15 degrees of each other, the current image
// is classified as the relevant standard image plane (e.g. axial).
let classifiedOrientation;
Object.keys(normals).some(orientation => {
let angleInRadians = imageNormal.angleTo(normals[orientation]);
angleInRadians = Math.abs(angleInRadians);
if (angleInRadians < PI / 12 || angleInRadians === PI) { // Pi / 12 radians = 15 degrees
classifiedOrientation = orientation;
return true;
}
});
// If no classification was determined, stop here and dump a warning to the console
if (!classifiedOrientation) {
return;
}
// Otherwise, update the cache with the classified orientation for this imageId
OHIF.viewer.linkSeries.classifiedOrientation[imageId] = classifiedOrientation;
return classifiedOrientation;
};

View File

@ -0,0 +1,4 @@
export const crosshairsSynchronizers = {
synchronizers: {}
};

View File

@ -0,0 +1,72 @@
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
import { $ } from 'meteor/jquery';
import { setFocusToActiveViewport } from './setFocusToActiveViewport';
let doneCallbackFunction;
/**
* Removes the backdrop abd closes opened dialog
* and focus to the active viewport. If a done callback is set,
* it's called before
* @param {Boolean} runCallback Indicate if callback function needs to be called. Default: true
*/
const closeHandler = (runCallback = true) => {
// Check if callback function exists
if (runCallback && typeof doneCallbackFunction === 'function') {
doneCallbackFunction();
}
// Hide the lesion dialog
$('#confirmDeleteDialog').css('display', 'none');
// Remove the backdrop
$('.removableBackdrop').remove();
// Remove the callback
doneCallbackFunction = undefined;
// Restore the focus to the active viewport
setFocusToActiveViewport();
};
/**
* Displays the confirmation dialog template and the removable backdrop element
*
* @param doneCallback A callback
* @param options
*/
const showConfirmDialog = (doneCallback, options) => {
// Show the backdrop
options = options || {};
Blaze.renderWithData(Template.removableBackdrop, options, document.body);
let confirmDeleteDialog = $('#confirmDeleteDialog');
confirmDeleteDialog.remove();
const viewer = document.getElementById('viewer');
Blaze.renderWithData(Template.confirmDeleteDialog, options, viewer);
// Make sure the context menu is closed when the user clicks away
$('.removableBackdrop').one('mousedown touchstart', () => {
// Close dialog without calling callback
closeHandler(false);
});
confirmDeleteDialog = $('#confirmDeleteDialog');
confirmDeleteDialog.css('display', 'block');
confirmDeleteDialog.focus();
// If callback function is defined, save it for closeHandler
if (doneCallback && typeof doneCallback === 'function') {
doneCallbackFunction = doneCallback;
}
};
const dialogUtils = {
showConfirmDialog,
closeHandler
};
export { dialogUtils };

View File

@ -0,0 +1,27 @@
import { $ } from 'meteor/jquery';
export function getElementIfNotEmpty(viewportIndex) {
// Meteor template helpers run more often than expected
// They often seem to run just before the whole template is rendered
// This meant that the onRendered event hadn't fired yet, so the
// element wasn't enabled / set empty yet. The check here
// for canvases under the 'enabled' element div is to prevent
// 'undefined' errors from the helper functions
var imageViewerViewports = $('.imageViewerViewport'),
element = imageViewerViewports.get(viewportIndex),
canvases = imageViewerViewports.eq(viewportIndex).find('canvas');
if (!element || $(element).hasClass('empty') || canvases.length === 0) {
return;
}
// Check to make sure the element is enabled.
try {
var enabledElement = cornerstone.getEnabledElement(element);
} catch(error) {
return;
}
return element;
}

View File

@ -0,0 +1,30 @@
/**
* Helper function to quickly obtain the frameOfReferenceUID
* for a given element from the enabled image's metadata.
*
* If no image, imagePlane, or frameOfReferenceUID is available,
* the function will return undefined.
*
* @param element
* @returns {string}
*/
export function getFrameOfReferenceUID(element) {
var enabledElement;
try {
enabledElement = cornerstone.getEnabledElement(element);
} catch(error) {
return;
}
if (!enabledElement || !enabledElement.image) {
return;
}
var imageId = enabledElement.image.imageId;
var imagePlane = cornerstoneTools.metaData.get('imagePlane', imageId);
if (!imagePlane || !imagePlane.frameOfReferenceUID) {
return;
}
return imagePlane.frameOfReferenceUID;
}

View File

@ -0,0 +1,251 @@
import { ImageSet } from './classes/ImageSet';
const sortBySeriesNumberAndAcquisition = (a, b) => {
if (a.seriesNumber && b.seriesNumber) {
if (a.seriesNumber !== b.seriesNumber) {
return a.seriesNumber - b.seriesNumber;
}
}
if (a.acquisitionDateTime && b.acquisitionDateTime) {
return a.acquisitionDateTime.localeCompare(b.acquisitionDateTime);
}
};
const makeImageSet = sopInstances => {
const imageSet = new ImageSet(sopInstances);
const sopInstance = sopInstances[0];
// list of attributes to be appended to ImageSet instance...
const attributes = {
seriesInstanceUid: sopInstance.getRawValue('x0020000e'),
seriesNumber: sopInstance.getIntValue('x00200011'),
seriesDescription: sopInstance.getRawValue('x0008103e', ''),
acquisitionDateTime: sopInstance.getRawValue('x00080022', '') + sopInstance.getRawValue('x00080032', ''),
numImageFrames: sopInstances.length,
frameRate: sopInstance.getFloatValue('x00181063', 0, 0),
studyInstanceUid: sopInstance.getRawValue('x0020000d'),
displaySetInstanceUid: imageSet.uid // create an alias for the imageSet UID
};
const frameTime = sopInstance.getFloatValue('x00181063', 0, 0);
const recommendedDisplayFrameRate = sopInstance.getFloatValue('x00082144', 0, 0);
if (frameTime) {
// FrameTime is in milliseconds, so we use 1000/frameTime to calculate
// the number of frames per second.
attributes.frameRate = 1000 / frameTime;
attributes.isClip = true;
} else if (recommendedDisplayFrameRate) {
// Recommended Display FrameRate is specified in frames per second as an integer
attributes.frameRate = recommendedDisplayFrameRate;
attributes.isClip = true;
}
imageSet.setAttributes(attributes);
// Sort the images in this series
imageSet.sortBy( (a, b) => {
const aInstanceNumber = a.getFloatValue('x00200013');
const bInstanceNumber = b.getFloatValue('x00200013');
if (aInstanceNumber && bInstanceNumber) {
if (aInstanceNumber !== bInstanceNumber) {
return aInstanceNumber - bInstanceNumber;
}
}
const aAcquisitionNumber = a.getFloatValue('x00200012');
const bAcquisitionNumber = b.getFloatValue('x00200012');
if (aAcquisitionNumber && bAcquisitionNumber) {
if (aAcquisitionNumber !== bAcquisitionNumber) {
return aAcquisitionNumber - bAcquisitionNumber;
}
}
const aAcquisitionDateTime = a.getRawValue('x00080022', '') + a.getRawValue('x00080032', '');
const bAcquisitionDateTime = b.getRawValue('x00080022', '') + b.getRawValue('x00080032', '');
if (aAcquisitionDateTime && bAcquisitionDateTime) {
return aAcquisitionDateTime.localeCompare(bAcquisitionDateTime);
}
});
return imageSet;
};
const isMultiFrame = instanceView => {
const numFrames = instanceView.getIntValue('x00280008');
return (numFrames > 1);
};
const isSingleImageModality = modality => {
const singleImageModalities = ['CR', 'MG', 'DX', 'RG', 'PX', 'XA', 'XR']
return singleImageModalities.indexOf(modality) > -1;
};
const seriesSplittingRules = [
{
tag: 'x00180010', // ContrastBolusAgent
},
{
tag: 'x00180086', // Echo Number
},
// --- Some other potential examples of tags to split on --- //
/*{
tag: 'x00180020', // Scanning Sequence
},
{
tag: 'x00180050', // Slice Thickness
},*/
/*{
tag: 'x00180081', // Echo Time
},
{
tag: 'x00181210', // Convolution Kernel
},*/
/*{
tag: 'x00200012', // Acquisition Number
}*/
];
const stackOfInstancesToDisplaySets = (sopInstances, seriesView) => {
// If no instances are provided, return an empty array
if (!sopInstances.length) {
return [];
}
// Create an empty array to store our display sets in
const displaySets = [];
// Use Underscore's groupBy function to group our sopInstances
// by a key. In our case, the key is a combined string which represents
// the value of the DICOM tags listed in the seriesSplittingRules array.
// Currently this uses tags for EchoNumber and ContrastBolusAgent.
const groups = _.groupBy(sopInstances, sopInstance => {
// Create an empty string to store our key
let result = '';
const instanceView = sopInstance.getData();
// For each rule, find out if the tag exists
seriesSplittingRules.forEach(rule => {
// Include a delimitation character between each tag value
result += ';'
// If the tag exists, concatenate its value to our key
if (instanceView.exists(rule.tag)) {
result += instanceView.raw(rule.tag);
}
});
// Return the key to be used for grouping.
return result;
})
const seriesInstanceCount = seriesView.getInstanceCount();
Object.keys(groups).forEach(groupKey => {
// For each group, obtain the instances in the stack
const stackInstances = groups[groupKey];
// Create a new display set from the images in this stack
const displaySet = makeImageSet(stackInstances);
displaySet.setAttribute('seriesInstanceCount', seriesInstanceCount);
displaySets.push(displaySet);
});
return displaySets;
};
/**
* This function extracts and produces sorted lists of image sets and non-viewable instance
* sets from a studyMetadata {StudyMetadata instance}.
*
* If an optional series number is specified, only the image set with this series number
* will be returned (as the sole element in an array).
*
* The function returns an object with two fields, imageSets and nonViewable, both of which
* are arrays. imageSets is an array of sorted image sets and nonViewable is an array of
* non-viewable sopInstances.
*
* @param studyMetadata
* @param {number} [seriesNumber] One series number. If specified, the function only returns the imageSet with this seriesNumber
*
* @returns {{imageSets: Array, nonViewable: Array}}
*/
getSortedData = function(studyMetadata, seriesNumber) {
let imageSets = [];
let nonViewable = [];
studyMetadata.forEachSeries((series, seriesNum) => {
const firstInstance = series.getInstanceByIndex(0);
// If a specific series number is given, skip all series except this one
if (seriesNumber !== undefined && firstInstance.getIntValue('x00200011', '') !== seriesNumber) {
return;
}
let sopInstances = [];
let imageSet;
const seriesInstanceCount = series.getInstanceCount();
series.forEachInstance(instanceMetadata => {
// Skip non image types (an image must have the 'rows' tag)
const rows = instanceMetadata.tagExists('x00280010');
if (!rows) {
nonViewable.push(instanceMetadata);
return;
}
const modality = firstInstance.getRawValue('x00080060', '');
const singleImageModality = isSingleImageModality(modality);
// If we detect a multi-frame instance
if (isMultiFrame(instanceMetadata)) {
// First, sort all sopInstances currently in our stack into display sets using
// our series splitting rules
const displaySets = stackOfInstancesToDisplaySets(sopInstances, series);
imageSets = imageSets.concat(displaySets);
// Next, make another display set for the multi-frame instance
imageSet = makeImageSet([ instanceMetadata ]);
imageSet.setAttributes({
seriesInstanceCount: seriesInstanceCount,
numImageFrames: instanceMetadata.getIntValue('x00280008', 0)
});
imageSets.push(imageSet);
// Reset our stack of sopInstances
sopInstances = [];
} else if (singleImageModality) {
imageSet = makeImageSet([ instanceMetadata ]);
imageSet.setAttribute('seriesInstanceCount', seriesInstanceCount);
imageSets.push(imageSet);
} else {
// If no multi-frame instances have been discovered
sopInstances.push(instanceMetadata);
}
});
// Create display sets from any remaining sopInstances in the series
const displaySets = stackOfInstancesToDisplaySets(sopInstances, series);
imageSets = imageSets.concat(displaySets);
});
// sort imageSets
imageSets.sort(sortBySeriesNumberAndAcquisition);
nonViewable.sort(sortBySeriesNumberAndAcquisition);
return {
imageSets,
nonViewable
};
};
export { getSortedData };

View File

@ -0,0 +1,22 @@
import { getElementIfNotEmpty } from './getElementIfNotEmpty.js';
export function getStackDataIfNotEmpty(viewportIndex) {
const element = getElementIfNotEmpty(viewportIndex);
if (!element) {
return;
}
const stackToolData = cornerstoneTools.getToolState(element, 'stack');
if (!stackToolData ||
!stackToolData.data ||
!stackToolData.data.length) {
return;
}
const stack = stackToolData.data[0];
if (!stack) {
return;
}
return stack;
}

View File

@ -0,0 +1,39 @@
/**
* Helpers with exposed symbols...
*/
import { isTouchDevice } from './isTouchDevice';
import { formatPN } from './formatPN';
import { formatDA } from './formatDA';
import { formatTM } from './formatTM';
/**
* Helpers with side effects only...
*/
import './formatJSDate';
import './jsDateFromNow';
import './formatNumberPrecision';
import './inc';
import './isDisplaySetActive';
import './getUsername';
import './capitalizeFirstLetter';
import './objectToPairs';
import './objectEach';
import './ifTypeIs';
import './prettyPrintStringify';
import './sorting';
import './studyThumbnails';
/**
* Exposed interface...
*/
const helpers = {
isTouchDevice,
formatPN,
formatDA,
formatTM
};
export { helpers };

View File

@ -0,0 +1,375 @@
import { Meteor } from 'meteor/meteor';
import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
import { toolManager } from './toolManager';
import { setActiveViewport } from './setActiveViewport';
import { switchToImageRelative } from './switchToImageRelative';
import { switchToImageByIndex } from './switchToImageByIndex';
import { viewportUtils } from './viewportUtils';
import { panelNavigation } from './panelNavigation';
import { seriesNavigation } from './seriesNavigation';
import { WLPresets } from './WLPresets';
// TODO: add this to namespace definitions
Meteor.startup(function() {
if (!OHIF.viewer) {
OHIF.viewer = {};
}
OHIF.viewer.loadIndicatorDelay = 200;
OHIF.viewer.defaultTool = 'wwwc';
OHIF.viewer.refLinesEnabled = true;
OHIF.viewer.isPlaying = {};
OHIF.viewer.cine = {
framesPerSecond: 24,
loop: true
};
OHIF.viewer.defaultHotkeys = {
defaultTool: 'ESC',
angle: 'A',
stackScroll: 'S',
pan: 'P',
magnify: 'M',
scrollDown: 'DOWN',
scrollUp: 'UP',
nextDisplaySet: 'PAGEDOWN',
previousDisplaySet: 'PAGEUP',
nextPanel: 'RIGHT',
previousPanel: 'LEFT',
invert: 'I',
flipV: 'V',
flipH: 'H',
wwwc: 'W',
zoom: 'Z',
cinePlay: 'SPACE',
rotateR: 'R',
rotateL: 'L',
toggleOverlayTags: 'SHIFT',
WLPresetSoftTissue: ['NUMPAD1', '1'],
WLPresetLung: ['NUMPAD2', '2'],
WLPresetLiver: ['NUMPAD3', '3'],
WLPresetBone: ['NUMPAD4', '4'],
WLPresetBrain: ['NUMPAD5', '5']
};
// For now
OHIF.viewer.hotkeys = OHIF.viewer.defaultHotkeys;
OHIF.viewer.hotkeyFunctions = {
wwwc() {
toolManager.setActiveTool('wwwc');
},
zoom() {
toolManager.setActiveTool('zoom');
},
angle() {
toolManager.setActiveTool('angle');
},
dragProbe() {
toolManager.setActiveTool('dragProbe');
},
ellipticalRoi() {
toolManager.setActiveTool('ellipticalRoi');
},
magnify() {
toolManager.setActiveTool('magnify');
},
annotate() {
toolManager.setActiveTool('annotate');
},
stackScroll() {
toolManager.setActiveTool('stackScroll');
},
pan() {
toolManager.setActiveTool('pan');
},
length() {
toolManager.setActiveTool('length');
},
spine() {
toolManager.setActiveTool('spine');
},
wwwcRegion() {
toolManager.setActiveTool('wwwcRegion');
},
zoomIn() {
const button = document.getElementById('zoomIn');
flashButton(button);
viewportUtils.zoomIn();
},
zoomOut() {
const button = document.getElementById('zoomOut');
flashButton(button);
viewportUtils.zoomOut();
},
zoomToFit() {
const button = document.getElementById('zoomToFit');
flashButton(button);
viewportUtils.zoomToFit();
},
scrollDown() {
const container = $('.viewportContainer.active');
const button = container.find('#nextImage').get(0);
if (!container.find('.imageViewerViewport').hasClass('empty')) {
flashButton(button);
switchToImageRelative(1);
}
},
scrollFirstImage() {
const container = $('.viewportContainer.active');
if (!container.find('.imageViewerViewport').hasClass('empty')) {
switchToImageByIndex(0);
}
},
scrollLastImage() {
const container = $('.viewportContainer.active');
if (!container.find('.imageViewerViewport').hasClass('empty')) {
switchToImageByIndex(-1);
}
},
scrollUp() {
const container = $('.viewportContainer.active');
if (!container.find('.imageViewerViewport').hasClass('empty')) {
const button = container.find('#prevImage').get(0);
flashButton(button);
switchToImageRelative(-1);
}
},
previousDisplaySet() {
OHIF.viewer.moveDisplaySets(false);
},
nextDisplaySet() {
OHIF.viewer.moveDisplaySets(true);
},
nextPanel() {
panelNavigation.loadNextActivePanel();
},
previousPanel() {
panelNavigation.loadPreviousActivePanel();
},
invert() {
const button = document.getElementById('invert');
flashButton(button);
viewportUtils.invert();
},
flipV() {
const button = document.getElementById('flipV');
flashButton(button);
viewportUtils.flipV();
},
flipH() {
const button = document.getElementById('flipH');
flashButton(button);
viewportUtils.flipH();
},
rotateR() {
const button = document.getElementById('rotateR');
flashButton(button);
viewportUtils.rotateR();
},
rotateL() {
const button = document.getElementById('rotateL');
flashButton(button);
viewportUtils.rotateL();
},
cinePlay() {
viewportUtils.toggleCinePlay();
},
defaultTool() {
const tool = toolManager.getDefaultTool();
toolManager.setActiveTool(tool);
},
toggleOverlayTags() {
const dicomTags = $('.imageViewerViewportOverlay .dicomTag');
if (dicomTags.eq(0).css('display') === 'none') {
dicomTags.show();
} else {
dicomTags.hide();
}
},
resetStack() {
const button = document.getElementById('resetStack');
flashButton(button);
resetStack();
},
clearImageAnnotations() {
const button = document.getElementById('clearImageAnnotations');
flashButton(button);
clearImageAnnotations();
},
nextSeries () {
seriesNavigation.loadNextSeries();
},
previousSeries () {
seriesNavigation.loadPreviousSeries();
},
cineDialog () {
/**
* TODO: This won't work in OHIF's, since this element
* doesn't exist
*/
const button = document.getElementById('cine');
flashButton(button);
viewportUtils.toggleCineDialog();
button.classList.toggle('active');
}
};
OHIF.viewer.loadedSeriesData = {};
OHIF.viewer.linkSeries = {
classifiedOrientation: {}
};
});
// Define a jQuery reverse function
$.fn.reverse = [].reverse;
/**
* Overrides OHIF's refLinesEnabled
* @param {Boolean} refLinesEnabled True to enable and False to disable
*/
function setOHIFRefLines(refLinesEnabled) {
OHIF.viewer.refLinesEnabled = refLinesEnabled;
}
/**
* Overrides OHIF's hotkeys
* @param {Object} hotkeys Object with hotkeys mapping
*/
function setOHIFHotkeys(hotkeys) {
OHIF.viewer.hotkeys = hotkeys;
}
/**
* Global function to merge different hotkeys configurations
* but avoiding conflicts between different keys with same action
* When this occurs, it will delete the action from OHIF's configuration
* So if you want to keep all OHIF's actions, use an unused-ohif-key
* Used for compatibility with others systems only
*
* @param hotkeysActions {object} Object with actions map
* @return {object}
*/
function mergeHotkeys(hotkeysActions) {
// Merge hotkeys, overriding OHIF's settings
let mergedHotkeys = {
...OHIF.viewer.defaultHotkeys,
...hotkeysActions
};
const defaultHotkeys = OHIF.viewer.defaultHotkeys;
const hotkeysKeys = Object.keys(hotkeysActions);
// Check for conflicts with same keys but different actions
Object.keys(defaultHotkeys).forEach(ohifAction => {
hotkeysKeys.forEach(definedAction => {
// Different action but same key:
// Remove action from merge if is not in "hotkeysActions"
// If it is, it's already merged so nothing to do
if(ohifAction !== definedAction && hotkeysActions[definedAction] === defaultHotkeys[ohifAction] && !hotkeysActions[ohifAction]) {
delete mergedHotkeys[ohifAction];
}
});
});
return mergedHotkeys;
}
/**
* Add an active class to a button for 100ms only
* to give the impressiont the button was pressed.
* This is for tools that don't keep the button "pressed"
* all the time the tool is active.
*
* @param button DOM Element for the button to be "flashed"
*/
function flashButton(button) {
if (!button) {
return;
}
button.classList.add('active');
setTimeout(() => {
button.classList.remove('active');
}, 100);
}
/**
* Binds a task to a hotkey keydown event
* @param {String} hotkey keyboard key
* @param {String} task task function name
*/
function bindHotkey(hotkey, task) {
var hotkeyFunctions = OHIF.viewer.hotkeyFunctions;
// Only bind defined, non-empty HotKeys
if (!hotkey || hotkey === '') {
return;
}
var fn;
if (task.indexOf('WLPreset') > -1) {
var presetName = task.replace('WLPreset', '');
fn = function() {
WLPresets.applyWLPresetToActiveElement(presetName);
};
} else {
fn = hotkeyFunctions[task];
// If the function doesn't exist in the
// hotkey function list, try the viewer-specific function list
if (!fn && OHIF.viewer && OHIF.viewer.functionList) {
fn = OHIF.viewer.functionList[task];
}
}
if (!fn) {
return;
}
var hotKeyForBinding = hotkey.toLowerCase();
$(document).bind('keydown', hotKeyForBinding, fn);
}
/**
* Binds all hotkeys keydown events to the tasks defined in
* OHIF.viewer.hotkeys or a given param
* @param {Object} hotkeys hotkey and task mapping (not required). If not given, uses OHIF.viewer.hotkeys
*/
function enableHotkeys(hotkeys) {
const viewerHotkeys = hotkeys || OHIF.viewer.hotkeys;
$(document).unbind('keydown');
Object.keys(viewerHotkeys).forEach(function(task) {
const taskHotkeys = viewerHotkeys[task];
if (!taskHotkeys || !taskHotkeys.length) {
return;
}
if (taskHotkeys instanceof Array) {
taskHotkeys.forEach(function(hotkey) {
bindHotkey(hotkey, task);
});
} else {
// taskHotkeys represents a single key
bindHotkey(taskHotkeys, task);
}
});
}
/**
* Export functions inside hotkeyUtils namespace.
*/
const hotkeyUtils = {
setOHIFRefLines, /* @TODO: find a better place for this... */
setOHIFHotkeys,
mergeHotkeys,
enableHotkeys
};
export { hotkeyUtils };

View File

@ -0,0 +1,7 @@
export const imageViewerViewportData = {
callbacks: {},
extendData() {
// No-Op function...
}
};

View File

@ -0,0 +1,53 @@
import { Session } from 'meteor/session';
import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
import { setActiveViewport } from './setActiveViewport';
const loadPreviousActivePanel = () => {
OHIF.log.info('nextActivePanel');
let currentIndex = Session.get('activeViewport');
currentIndex--;
const viewports = $('.imageViewerViewport');
const numViewports = viewports.length;
if (currentIndex < 0) {
currentIndex = numViewports - 1;
}
const element = viewports.get(currentIndex);
if (!element) {
return;
}
setActiveViewport(element);
};
const loadNextActivePanel = () => {
OHIF.log.info('nextActivePanel');
let currentIndex = Session.get('activeViewport');
currentIndex++;
const viewports = $('.imageViewerViewport');
const numViewports = viewports.length;
if (currentIndex >= numViewports) {
currentIndex = 0;
}
const element = viewports.get(currentIndex);
if (!element) {
return;
}
setActiveViewport(element);
};
/**
* Export functions inside panelNavigation namespace.
*/
const panelNavigation = {
loadPreviousActivePanel,
loadNextActivePanel
};
export { panelNavigation };

View File

@ -0,0 +1,91 @@
import { Session } from 'meteor/session';
import { OHIF } from 'meteor/ohif:core';
// @TODO: import symbols into local scope: Studies and ViewerData
const getStudyFromStudyInstanceUid = studyInstanceUid => {
// @TypeSafeStudies
return Studies.findBy({ studyInstanceUid });
};
const getNumberOfStacks = studyInstanceUid => {
// May not work if the Study contains non-image series?
const study = getStudyFromStudyInstanceUid(studyInstanceUid);
if (!study || !study.series || !study.series.length) {
return;
}
return study.series.length;
};
const getActiveViewportIndex = () => {
return Session.get('activeViewport');
};
const loadSeries = indexCalculator => {
if(typeof indexCalculator !== 'function') {
return;
}
const viewportIndex = getActiveViewportIndex();
const contentId = Session.get('activeContentId');
const viewerData = ViewerData[contentId].loadedSeriesData[viewportIndex];
if(!viewerData && !viewerData.studyInstanceUid) {
return;
}
const { studyInstanceUid, seriesInstanceUid, displaySetInstanceUid } = viewerData;
if(!studyInstanceUid) {
return;
}
const numberOfStacks = getNumberOfStacks(studyInstanceUid);
if (!numberOfStacks || numberOfStacks === 1) {
return;
}
const study = getStudyFromStudyInstanceUid(studyInstanceUid);
const displaySetInstanceUids = study.series.map(series => {
return series.displaySetInstanceUid;
});
const currentLoadedStackIndex = displaySetInstanceUids.indexOf(displaySetInstanceUid);
const newStackIndex = indexCalculator(currentLoadedStackIndex, numberOfStacks);
if (currentLoadedStackIndex === newStackIndex) {
return;
}
const newDisplaySetInstanceUid = displaySetInstanceUids[newStackIndex];
const data = {
displaySetInstanceUid: newDisplaySetInstanceUid,
viewportIndex,
studyInstanceUid,
seriesInstanceUid
};
OHIF.viewerbase.layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, data);
};
const loadPreviousSeries = () => {
const indexCalculator = currentLoadedStackIndex => Math.max(currentLoadedStackIndex - 1, 0);
loadSeries(indexCalculator);
};
const loadNextSeries = () => {
const indexCalculator = (currentLoadedStackIndex, numberOfStacks) => Math.min(currentLoadedStackIndex + 1, numberOfStacks - 1);
loadSeries(indexCalculator);
};
const seriesNavigation = {
loadPreviousSeries,
loadNextSeries
};
export { seriesNavigation };

View File

@ -0,0 +1,13 @@
import { getSortedData } from './getSortedData';
const getDisplaySets = (studyMetadata, seriesNumber, iteratorFunction) => {
const iteratorFn = typeof iteratorFunction !== 'function' ? getSortedData : iteratorFunction;
return iteratorFn(studyMetadata, seriesNumber);
};
const sortingManager = {
getDisplaySets
};
export { sortingManager };

View File

@ -0,0 +1,220 @@
import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
const cloneElement = (element, targetId) => {
// Clone the DOM element
const clone = element.cloneNode(true);
// Find any canvas children to clone
const clonedCanvases = $(clone).find('canvas');
clonedCanvases.each((canvasIndex, clonedCanvas) => {
// Draw from the original canvas to the cloned canvas
const context = clonedCanvas.getContext('2d');
const thumbnailCanvas = $(element).find('canvas').get(canvasIndex);
context.drawImage(thumbnailCanvas, 0, 0);
});
// Update the clone with the targetId
clone.id = targetId;
clone.style.visibility = 'hidden';
return clone;
};
const thumbnailDragStartHandler = (event, data) => {
// Prevent any scrolling behaviour normally caused by the original event
event.originalEvent.preventDefault();
// Identify the current study and series index from the thumbnail's DOM position
const targetThumbnail = event.currentTarget;
const $imageThumbnail = $(targetThumbnail);
// Clone the image thumbnail
const targetId = 'DragClone';
const clone = cloneElement(targetThumbnail, targetId);
const $clone = $(clone);
$clone.addClass('imageThumbnailClone');
// Set pointerEvents to pass through the clone DOM element
// This is necessary in order to identify what is below it
// when using document.elementFromPoint
clone.style.pointerEvents = 'none';
// Append the clone to the body
document.body.appendChild(clone);
// Set the cursor x and y positions from the current touch/mouse coordinates
let cursorX;
let cursorY;
// Handle touchStart cases
if (event.type === 'touchstart') {
cursorX = event.originalEvent.touches[0].pageX;
cursorY = event.originalEvent.touches[0].pageY;
}
else {
cursorX = event.pageX;
cursorY = event.pageY;
// Also hook up event handlers for mouse events
const handlers = {};
handlers.mousemove = event => thumbnailDragHandler(event);
handlers.mouseup = event => thumbnailDragEndHandler(event, data, handlers);
$(document).on('mousemove', handlers.mousemove);
$(document).on('mouseup', handlers.mouseup);
}
// This block gets the current offset of the touch/mouse
// relative to the window
//
// i.e. Where did the user grab it from?
const offset = $imageThumbnail.offset();
const { left, top } = offset;
// This difference is saved for later so the element movement looks normal
const diff = {
x: cursorX - left,
y: cursorY - top
};
$clone.data('diff', diff);
// This sets the default style properties of the cloned element so it is
// ready to be dragged around the page
$clone.css({
left: cursorX - diff.x,
position: 'fixed',
top: cursorY - diff.y,
visibility: 'hidden',
'z-index': 100000
});
};
const thumbnailDragHandler = event => {
// Get the touch/mouse coordinates from the event
let cursorX;
let cursorY;
if (event.type === 'touchmove') {
cursorX = event.originalEvent.changedTouches[0].pageX;
cursorY = event.originalEvent.changedTouches[0].pageY;
}
else {
cursorX = event.pageX;
cursorY = event.pageY;
}
// Find the clone element and update it's position on the page
const $clone = $('#DragClone');
const diff = $clone.data('diff');
$clone.css({
left: cursorX - diff.x,
position: 'fixed',
top: cursorY - diff.y,
visibility: 'visible',
'z-index': 100000
});
// Identify the element below the current cursor position
const elemBelow = document.elementFromPoint(cursorX, cursorY);
// If none exists, stop here
if (!elemBelow) {
return;
}
// Remove any current faded effects on viewports
$('.imageViewerViewport canvas').removeClass('faded');
// Figure out what to do depending on what we're dragging over
const $viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
if ($viewportsDraggedOver.length) {
// If we're dragging over a non-empty viewport, fade it and change the cursor style
$viewportsDraggedOver.find('canvas').not('.magnifyTool').addClass('faded');
document.body.style.cursor = 'copy';
}
else if (elemBelow.classList.contains('imageViewerViewport') && elemBelow.classList.contains('empty')) {
// If we're dragging over an empty viewport, just change the cursor style
document.body.style.cursor = 'copy';
}
else {
// Otherwise, keep the cursor as no-drop style
document.body.style.cursor = 'no-drop';
}
};
const thumbnailDragEndHandler = (event, data, handlers) => {
// Remove the mouse event listeners
if (handlers) {
$(document).off('mousemove', handlers.mousemove);
$(document).off('mouseup', handlers.mouseup);
}
// Reset the cursor style to the default
document.body.style.cursor = 'auto';
// Get the cloned element
const $clone = $('#DragClone');
// If it doesn't exist, stop here
if (!$clone.length) {
return;
}
const offset = $clone.offset();
const { top, left } = offset;
const diff = $clone.data('diff');
// Identify the element below the cloned element position
const elemBelow = document.elementFromPoint(left + diff.x, top + diff.y);
// Remove all cloned elements from the page
$('.imageThumbnailClone').remove();
// Remove any current faded effects on viewports
$('.imageViewerViewport canvas').removeClass('faded');
// If none exists, stop here
if (!elemBelow) {
return;
}
// Remove any fade effects on the element below
elemBelow.classList.remove('faded');
let element;
const $viewportsDraggedOver = $(elemBelow).closest('.imageViewerViewport');
if ($viewportsDraggedOver.length) {
// If we're dragging over a non-empty viewport, retrieve it
element = $viewportsDraggedOver.get(0);
}
else if (elemBelow.classList.contains('imageViewerViewport') &&
elemBelow.classList.contains('empty')) {
// If we're dragging over an empty viewport, retrieve that instead
element = elemBelow;
}
else {
// Otherwise, stop here
return false;
}
// If there is no stored drag and drop data, stop here
if (!data) {
return false;
}
// Get the dropped viewport index
const viewportIndex = $('.imageViewerViewport').index(element);
// Rerender the viewport using the dragged thumbnail data
OHIF.viewerbase.layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, data);
return false;
};
const thumbnailDragHandlers = {
thumbnailDragEndHandler,
thumbnailDragStartHandler,
thumbnailDragHandler
};
export { thumbnailDragHandlers };

View File

@ -0,0 +1,36 @@
import { $ } from 'meteor/jquery';
import { getFrameOfReferenceUID } from './getFrameOfReferenceUID';
import { crosshairsSynchronizers } from './crosshairsSynchronizers';
/**
* This function is used to maintain the updateImageSynchronizers
* that are using in the Crosshair tool. The function creates
* (and destroys any currently existing) a new synchronizer for the given
* frame of reference. It then searches for other viewports that share the same
* frame of reference, and adds those to the synchronizer. These viewports
* will now function together when the Crosshair tool is used.
*
* @param currentFrameOfReferenceUID
*/
export function updateCrosshairsSynchronizer(currentFrameOfReferenceUID) {
// Check if an old synchronizer exists, and if it does, destroy it
// If not, create a new one
let synchronizer = crosshairsSynchronizers.synchronizers[currentFrameOfReferenceUID];
if (synchronizer) {
// If it already exists, remove all source & target elements
synchronizer.destroy();
} else {
// Create a new synchronizer
crosshairsSynchronizers.synchronizers[currentFrameOfReferenceUID] = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
synchronizer = crosshairsSynchronizers.synchronizers[currentFrameOfReferenceUID];
}
// Add all elements that stem from the same frame of reference
$('.imageViewerViewport').each((index, element) => {
const frameOfReferenceUID = getFrameOfReferenceUID(element);
if (currentFrameOfReferenceUID !== frameOfReferenceUID) {
return;
}
synchronizer.add(element);
});
}

View File

@ -0,0 +1,65 @@
import { $ } from 'meteor/jquery';
/**
* Updates the orientation labels on a Cornerstone-enabled Viewport element
* when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
*
* @param element The DOM element of the Cornerstone viewport
* optional
* @param viewport The current viewport
*/
export function updateOrientationMarkers(element, viewport) {
// Get the current viewport settings
if(!viewport) {
viewport = cornerstone.getViewport(element);
}
// Updates the orientation labels on the viewport
const enabledElement = cornerstone.getEnabledElement(element);
const imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
return;
}
const rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
const columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
const oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
const oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
const markers = {
top: oppositeColumnString,
left: oppositeRowString
};
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
// the rotation applications
if (viewport.vflip) {
markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
}
if (viewport.hflip) {
markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
}
// Get the viewport orientation marker DOM elements
const viewportOrientationMarkers = $(element).siblings('.viewportOrientationMarkers');
const topMarker = viewportOrientationMarkers.find('.topMid');
const leftMarker = viewportOrientationMarkers.find('.leftMid');
// Swap the labels accordingly if the viewport has been rotated
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
if (viewport.rotation === 90 || viewport.rotation === -270) {
topMarker.text(markers.left);
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
} else if (viewport.rotation === -90 || viewport.rotation === 270) {
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
leftMarker.text(markers.top);
} else if (viewport.rotation === 180 || viewport.rotation === -180) {
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
} else {
topMarker.text(markers.top);
leftMarker.text(markers.left);
}
}

View File

@ -0,0 +1,100 @@
import { getElementIfNotEmpty } from './getElementIfNotEmpty';
const getPatient = function(property) {
if (!this.imageId) {
return false;
}
const patient = cornerstoneTools.metaData.get('patient', this.imageId);
if (!patient) {
return '';
}
return patient[property];
};
const getStudy = function(property) {
if (!this.imageId) {
return false;
}
const study = cornerstoneTools.metaData.get('study', this.imageId);
if (!study) {
return '';
}
return study[property];
};
const getSeries = function(property) {
if (!this.imageId) {
return false;
}
const series = cornerstoneTools.metaData.get('series', this.imageId);
if (!series) {
return '';
}
return series[property];
};
const getInstance = function(property) {
if (!this.imageId) {
return false;
}
const instance = cornerstoneTools.metaData.get('instance', this.imageId);
if (!instance) {
return '';
}
return instance[property];
};
const getTagDisplay = function(property) {
if (!this.imageId) {
return false;
}
const instance = cornerstoneTools.metaData.get('tagDisplay', this.imageId);
if (!instance) {
return '';
}
return instance[property];
};
const getImage = function(viewportIndex) {
const element = getElementIfNotEmpty(viewportIndex);
if (!element) {
return false;
}
let enabledElement;
try {
enabledElement = cornerstone.getEnabledElement(element);
} catch(error) {
return false;
}
if (!enabledElement || !enabledElement.image) {
return false;
}
return enabledElement.image;
};
const formatDateTime = (date, time) => `${date} ${time}`;
const viewportOverlayUtils = {
getPatient,
getStudy,
getSeries,
getInstance,
getTagDisplay,
getImage,
formatDateTime
};
export { viewportOverlayUtils };

View File

@ -0,0 +1,268 @@
import { Session } from 'meteor/session';
import { Random } from 'meteor/random';
import { $ } from 'meteor/jquery';
import { _ } from 'meteor/underscore';
// Local Modules
import { updateOrientationMarkers } from './updateOrientationMarkers';
import { getInstanceClassDefaultViewport } from './instanceClassSpecificViewport';
function getActiveViewportElement() {
const viewportIndex = Session.get('activeViewport') || 0;
return $('.imageViewerViewport').get(viewportIndex);
}
function zoomIn() {
const element = getActiveViewportElement();
if (!element) {
return;
}
let viewport = cornerstone.getViewport(element);
const scaleIncrement = 0.15;
const maximumScale = 10;
viewport.scale = Math.min(viewport.scale + scaleIncrement, maximumScale);
cornerstone.setViewport(element, viewport);
}
function zoomOut() {
const element = getActiveViewportElement();
if (!element) {
return;
}
let viewport = cornerstone.getViewport(element);
const scaleIncrement = 0.15;
const minimumScale = 0.05;
viewport.scale = Math.max(viewport.scale - scaleIncrement, minimumScale);
cornerstone.setViewport(element, viewport);
}
function zoomToFit() {
const element = getActiveViewportElement();
if (!element) {
return;
}
cornerstone.fitToWindow(element);
}
function rotateL() {
const element = getActiveViewportElement();
if (!element) {
return;
}
let viewport = cornerstone.getViewport(element);
viewport.rotation -= 90;
cornerstone.setViewport(element, viewport);
updateOrientationMarkers(element, viewport);
}
function rotateR() {
const element = getActiveViewportElement();
if (!element) {
return;
}
let viewport = cornerstone.getViewport(element);
viewport.rotation += 90;
cornerstone.setViewport(element, viewport);
updateOrientationMarkers(element, viewport);
}
function invert() {
const element = getActiveViewportElement();
if (!element) {
return;
}
let viewport = cornerstone.getViewport(element);
viewport.invert = (viewport.invert === false);
cornerstone.setViewport(element, viewport);
}
function flipV() {
const element = getActiveViewportElement();
let viewport = cornerstone.getViewport(element);
viewport.vflip = (viewport.vflip === false);
cornerstone.setViewport(element, viewport);
updateOrientationMarkers(element, viewport);
}
function flipH() {
const element = getActiveViewportElement();
let viewport = cornerstone.getViewport(element);
viewport.hflip = (viewport.hflip === false);
cornerstone.setViewport(element, viewport);
updateOrientationMarkers(element, viewport);
}
function resetViewport() {
const element = getActiveViewportElement();
const enabledElement = cornerstone.getEnabledElement(element);
if (enabledElement.fitToWindow === false) {
const imageId = enabledElement.image.imageId;
const instance = cornerstoneTools.metaData.get('instance', imageId);
enabledElement.viewport = cornerstone.getDefaultViewport(enabledElement.canvas, enabledElement.image);
const instanceClassDefaultViewport = getInstanceClassDefaultViewport(instance, enabledElement, imageId);
cornerstone.setViewport(element, instanceClassDefaultViewport);
} else {
cornerstone.reset(element);
}
}
function clearTools() {
const element = getActiveViewportElement();
const toolStateManager = cornerstoneTools.globalImageIdSpecificToolStateManager;
toolStateManager.clear(element);
cornerstone.updateImage(element);
}
// This function was originally defined alone inside client/lib/toggleDialog.js
// and has been moved here to avoid circular dependency issues.
function toggleDialog(element) {
const $element = $(element);
if($element.is('dialog')) {
if (element.hasAttribute('open')) {
stopAllClips();
element.close();
} else {
element.show();
}
}
else {
const isClosed = $element.hasClass('dialog-open');
$element.toggleClass('dialog-closed', isClosed);
$element.toggleClass('dialog-open', !isClosed);
}
Session.set('UpdateCINE', Random.id());
}
// Toggle the play/stop state for the cornerstone clip tool
function toggleCinePlay() {
// Get the active viewport element
const element = getActiveViewportElement();
// Check if it's playing the clip to toggle it
if (isPlaying()) {
cornerstoneTools.stopClip(element);
} else {
cornerstoneTools.playClip(element);
}
// Update the UpdateCINE session property
Session.set('UpdateCINE', Random.id());
}
// Show/hide the CINE dialog
function toggleCineDialog() {
var dialog = document.getElementById('cineDialog');
toggleDialog(dialog);
}
// Check if the clip is playing on the active viewport
function isPlaying() {
// Create a dependency on LayoutManagerUpdated and UpdateCINE session
Session.get('UpdateCINE');
Session.get('LayoutManagerUpdated');
// Get the viewport element and its current playClip tool state
const element = getActiveViewportElement();
// Empty Elements throws cornerstore exception
if (!element || !$(element).find('canvas').length) {
return;
}
const toolState = cornerstoneTools.getToolState(element, 'playClip');
// Stop here if the tool state is not defined yet
if (!toolState) {
return false;
}
// Get the clip state
const clipState = toolState.data[0];
if(clipState) {
// Return true if the clip is playing
return !_.isUndefined(clipState.intervalId);
}
return false;
}
// Check if a study has multiple frames
function hasMultipleFrames() {
// Its called everytime active viewport and/or layout change
Session.get('activeViewport');
Session.get('LayoutManagerUpdated');
const activeViewport = getActiveViewportElement();
// No active viewport yet: disable button
if(!activeViewport || !$(activeViewport).find('canvas').length) {
return true;
}
// Get images in the stack
const stackToolData = cornerstoneTools.getToolState(activeViewport, 'stack');
// No images in the stack, so disable button
if (!stackToolData || !stackToolData.data || !stackToolData.data.length) {
return true;
}
// Get number of images in the stack
const stackData = stackToolData.data[0];
const nImages = stackData.imageIds && stackData.imageIds.length ? stackData.imageIds.length : 1;
// Stack has just one image, so disable button
if(nImages === 1) {
return true;
}
return false;
}
// Stop clips on all non-empty elements
function stopAllClips() {
const elements = $('.imageViewerViewport').not('.empty');
elements.each( (index, element) => {
if ($(element).find('canvas').length) {
cornerstoneTools.stopClip(element);
}
});
}
// Create an event listener to update playing state when a clip stops playing
$(window).on('CornerstoneToolsClipStopped', () => Session.set('UpdateCINE', Random.id()));
/**
* Export functions inside viewportUtils namespace.
*/
const viewportUtils = {
getActiveViewportElement,
zoomIn,
zoomOut,
zoomToFit,
rotateL,
rotateR,
invert,
flipV,
flipH,
resetViewport,
clearTools,
toggleDialog,
toggleCinePlay,
toggleCineDialog,
isPlaying,
hasMultipleFrames,
stopAllClips
};
export { viewportUtils };

View File

@ -0,0 +1,39 @@
/**
* Import namespace...
*/
import { OHIF, Viewerbase } from './namespace.js';
/**
* Import scripts that will populate the Viewerbase namespace as a side effect only import. This is effectively the public API...
*/
import './client/'; // which is actually: import './client/index.js';
/**
* Export relevant objects...
*
* With the following export it becomes possible to import "OHIF" from "ohif:core" and "Viewerbase"
* from "ohif:viewerbase" using a single import (a shorthand), like this:
*
* import { OHIF } from 'meteor/ohif:viewerbase';
*
* Which is equivalent to:
*
* import { OHIF } from 'meteor/ohif:core';
* import 'meteor/ohif:viewerbase';
*
* The second (extended) format should be used when other OHIF packages are also to be used within
* the current module. This makes it explicit that the following imports will populate their
* respective namespaces within the to "OHIF" namespace. Example:
*
* import { OHIF } from 'meteor/ohif:core';
* import 'meteor/ohif:viewerbase';
* import 'meteor/ohif:hanging-protocols';
* [ ... ]
* OHIF.viewerbase.setActiveViewport(...);
* OHIF.hangingprotocols.doSomething(...);
*
*/
export { OHIF, Viewerbase };

View File

@ -0,0 +1,23 @@
/**
* Import main dependency...
*/
import { OHIF } from 'meteor/ohif:core';
/**
* Create Viewerbase namespace...
*/
const Viewerbase = {};
/**
* Append Viewerbase namespace to OHIF namespace...
*/
OHIF.viewerbase = Viewerbase;
/**
* Export relevant objects...
*/
export { OHIF, Viewerbase };