Added hotkey support to viewerbase

This commit is contained in:
Erik Ziegler committed 2015-12-09 15:48:24 +01:00
1 parent a005e5b359
commit da3101be93
59 files changed
+1002 -71

No files matched your search

@@ -0,0 +1,15 @@
<template name="hidingPanel">
<div class="hidingPanel">
<div class="toggleBtnContainer">
<button id="btnCollapse">
<i id="btnCollapseIcon" class="btnCollapseIcon fa fa-angle-left fa-lg"></i>
</button>
</div>
<div class="studyBrowserContainer">
{{#if studyDateIsShown}}
{{> studyDateList}}
{{/if}}
{{> studyBrowser}}
</div>
</div>
</template>
@@ -0,0 +1,51 @@
Template.hidingPanel.events({
'click button#btnCollapse': function(e,template) {
// Check hidingPanel is open or not
var hidingPanelOpen = template.hidingPanelOpen.get();
hidingPanelOpen = !hidingPanelOpen;
template.hidingPanelOpen.set(hidingPanelOpen);
// Get hidingPanel width from hidingPanelWidth reactiveVar
var contentId = this.contentId;
var hidingPanelElement = $("#"+contentId).find(".hidingPanel");
var hidingPanelWidth = template.hidingPanelWidth.get();
if(hidingPanelWidth == 0) {
hidingPanelWidth = $(hidingPanelElement).width();
template.hidingPanelWidth.set(parseInt(hidingPanelWidth));
}
$(hidingPanelElement).toggleClass("hidingPanelCollapse");
var studyBrowserContainerElement = $("#"+contentId).find(".studyBrowserContainer");
$(studyBrowserContainerElement).toggleClass("studyBrowserContainerCollapse");
var btnCollapseIconElement = $("#"+contentId).find("#btnCollapseIcon");
$(btnCollapseIconElement).toggleClass("btnCollapseIcon-collapse");
// Set viewportAndLesionTable width according to hiding panel status
if (hidingPanelOpen) {
var viewportAndLesionTableElement = $("#"+contentId).find("#viewportAndLesionTable");
var viewportAndLesionTableElementWidth = $(viewportAndLesionTableElement).width();
$(viewportAndLesionTableElement).width(viewportAndLesionTableElementWidth - hidingPanelWidth +"px");
} else {
var viewportAndLesionTableElement = $("#"+contentId).find("#viewportAndLesionTable");
var viewportAndLesionTableElementWidth = $(viewportAndLesionTableElement).width();
$(viewportAndLesionTableElement).width(viewportAndLesionTableElementWidth + hidingPanelWidth +"px");
}
}
});
Template.hidingPanel.helpers({
'studyDateIsShown': function() {
return true;
}
});
Template.hidingPanel.onCreated(function() {
this.hidingPanelOpen = new ReactiveVar(true);
this.hidingPanelWidth = new ReactiveVar(0);
});
@@ -0,0 +1,70 @@
.hidingPanel,.hidingPanelCollapse, .toggleBtnContainer, .studyBrowserContainer, .studyBrowserContainerCollapse, #btnCollapse,.btnCollapseIcon, .btnCollapseIcon-collapse, .viewportStyle {
-o-transition: all 0.3s ease-out;
-ms-transition: all 0.3s ease-out;
-moz-transition: all 0.3s ease-out;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.hidingPanel {
width: 120px;
height: 100%;
position: relative;
float: left;
}
.hidingPanelCollapse{
width: 0px;
}
.toggleBtnContainer{
position: absolute;
top: 0;
left: 0;
width: 25px;
height: 25px;
}
.studyBrowserContainer {
position: absolute;
display: block;
margin-top: 30px;
height: calc(100% - 30px);
width: 100%;
opacity: 1;
}
.studyBrowserContainerCollapse {
opacity: 0;
width: 0px;
}
.studyBrowserContainerCollapseDisplay {
display: none;
}
#btnCollapse{
position: absolute;
display: block;
width: 25px;
height: 25px;
line-height: 25px;
padding: 0;
border: 0;
border-top: 1px solid #e6e6e6;
background: #f6f6f6;
color: #666;
font-size: 0.875rem;
text-align: center;
cursor: pointer;
}
.btnCollapseIcon-collapse{
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
@@ -0,0 +1,33 @@
<template name="layout">
<nav class="navbar navbar-default noselect" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img height="30px" src="/images/logo.png">
<h4 class="name">Open Health Imaging Foundation</h4>
</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li>
<a href="http://www.ohif.org">About</a>
</li>
<li>
<form class="navbar-form">
<div class="form-group">
{{> loginButtons align="right"}}
</div>
</form>
</li>
</ul>
</div>
</div>
</nav>
{{> yield}}
</template>
@@ -0,0 +1,47 @@
.noselect
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none
.navbar
margin-bottom: 0
border-radius: 0
color: #888888
background-color: #060606
border: none
font-family: "Sanchez"
color: #C1C1C1
.navbar-default .navbar-nav > li > a
color: #C1C1C1
font-weight: bold
font-size: 12pt
.navbar-form
height: 50px
margin: 0
padding: 15px 3px
font-weight: bold
font-size: 12pt
line-height: 20px
.navbar-brand
display: inline-block
img
float: left
margin-right: 10px
margin-top: -5px
h4.name
float: left
margin: 0
font-family: "Sanchez"
color: #C1C1C1
@media (min-width: 768px)
.navbar
border-radius: 0px
@@ -0,0 +1,5 @@
<template name="loadingText">
<div class="loadingTextDiv">
<h5>Loading... <i class="fa fa-spinner fa-pulse"></i></h5>
</div>
</template>
@@ -0,0 +1,5 @@
.loadingTextDiv
margin: 30px
h5
text-align: center
@@ -0,0 +1,3 @@
<template name="login">
{{> atForm}}
</template>
@@ -0,0 +1,3 @@
<template name="notFound">
Not found
</template>
@@ -0,0 +1,7 @@
<template name="removableBackdrop">
<div class="removableBackdrop noselect"
oncontextmenu='return false;'
unselectable='on'
onselectstart='return false;'>
</div>
</template>
@@ -0,0 +1,9 @@
.removableBackdrop
position: fixed
top: 0
right: 0
bottom: 0
left: 0
z-index: 50
background-color: transparent
@@ -0,0 +1,10 @@
<template name="imageThumbnail">
<div class="imageThumbnail">
<div class="imageThumbnailLoadingIndicator thumbnailLoadingIndicator">
<p>Loading {{percentComplete}}</p>
</div>
<div class="imageThumbnailErrorLoadingIndicator thumbnailLoadingIndicator">
<p>Error</p>
</div>
</div>
</template>
@@ -0,0 +1,37 @@
Template.imageThumbnail.onRendered(function() {
var instance = this.data.stack.instances[0];
var element = this.find('.imageThumbnail');
cornerstone.disable(element);
$(element).find('canvas').remove();
cornerstone.enable(element);
var imageId = getImageId(instance);
var elem = $(element);
elem.find('.imageThumbnailLoadingIndicator').css('display', 'block');
var thumbnailIndex = $('.imageThumbnail').index(element);
ThumbnailLoading[thumbnailIndex] = imageId;
this.data.thumbnailIndex = thumbnailIndex;
cornerstone.loadAndCacheImage(imageId).then(function(image) {
cornerstone.displayImage(element, image);
delete ThumbnailLoading[thumbnailIndex];
elem.find('.imageThumbnailLoadingIndicator').css('display', 'none');
}, function(error) {
elem.find('.imageThumbnailErrorLoadingIndicator').css('display', 'block');
});
});
Template.imageThumbnail.helpers({
'percentComplete': function() {
var percentComplete = Session.get('CornerstoneThumbnailLoadProgress' + this.thumbnailIndex);
if (percentComplete && percentComplete !== 100) {
return percentComplete + '%';
}
}
});
@@ -0,0 +1,23 @@
.imageThumbnail
width: 90px
height: 90px
background-color: black
margin: 0 auto
position: relative
.thumbnailLoadingIndicator
display: none
pointer-events: none
color: #f8a13f
height: 20px
width: 100%
top: 0
left: 0
right: 0
bottom: 0
margin: auto
position: absolute
p
text-align: center
font-size: 10pt
@@ -0,0 +1,7 @@
<template name="studyBrowser">
<div class="studyBrowser">
{{ #each studies }}
{{ >thumbnails }}
{{ /each }}
</div>
</template>
@@ -0,0 +1,5 @@
Template.studyBrowser.helpers({
studies : function() {
return ViewerStudies.find({selected: true});
}
});
@@ -0,0 +1,9 @@
.studyBrowser
float: left
height: 100%
width: 100%
min-width: 120px
overflow-y: auto
overflow-x: hidden
background-color: black
padding-bottom: 20px
@@ -0,0 +1,6 @@
<template name="thumbnailEntry">
<div class="thumbnailEntry">
{{ >imageThumbnail }}
<div class='seriesDescription noselect'>{{seriesDescription}}</div>
</div>
</template>
@@ -0,0 +1,220 @@
function cloneElement(element, targetId) {
// Clone the DOM element
var clone = element.cloneNode(true);
// Find any canvas children to clone
var clonedCanvases = $(clone).find('canvas');
clonedCanvases.each(function(canvasIndex, clonedCanvas) {
// Draw from the original canvas to the cloned canvas
var context = clonedCanvas.getContext('2d');
var 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;
}
function thumbnailDragStartHandler(e, data) {
// Prevent any scrolling behaviour normally caused by the original event
e.originalEvent.preventDefault();
// Identify the current study and series index from the thumbnail's DOM position
var targetThumbnail = e.currentTarget;
var imageThumbnail = $(targetThumbnail);
// Store this data for use during drag and drop
OHIF.viewer.dragAndDropData = data;
// Clone the image thumbnail
var targetId = targetThumbnail.id + 'DragClone';
var clone = cloneElement(targetThumbnail, targetId);
clone.classList.add('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 parent of the target
targetThumbnail.parentNode.appendChild(clone);
// Set the cursor x and y positions from the current touch/mouse coordinates
// Handle touchStart cases
if (e.type === 'touchstart') {
cursorX = e.originalEvent.touches[0].pageX;
cursorY = e.originalEvent.touches[0].pageY;
} else {
cursorX = e.pageX;
cursorY = e.pageY;
// Also hook up event handlers for mouse events
$(document).on('mousemove', function(e) {
thumbnailDragHandler(e, targetThumbnail);
});
$(document).on('mouseup', function(e) {
thumbnailDragEndHandler(e, targetThumbnail);
});
}
// This block gets the current position of the touch/mouse
// relative to the thumbnail itself
//
// i.e. Where did the user grab it from?
var position = imageThumbnail.offset();
var left = position.left;
var top = position.top;
// This difference is saved for later so the element movement looks normal
diffX = cursorX - left;
diffY = cursorY - top;
// This sets the default style properties of the cloned element so it is
// ready to be dragged around the page
$(clone).css({
top: top,
left: left,
position: 'absolute',
'z-index': 100,
visibility: 'hidden'
});
}
function thumbnailDragHandler(e, target) {
// Get the touch/mouse coordinates from the event
var cursorX,
cursorY;
if (e.type === 'touchmove') {
cursorX = e.originalEvent.changedTouches[0].pageX;
cursorY = e.originalEvent.changedTouches[0].pageY;
} else {
cursorX = e.pageX;
cursorY = e.pageY;
}
// Find the clone element and update it's position on the page
var clone = $('#' + target.id + 'DragClone');
clone.css({
top: cursorY - diffY,
left: cursorX - diffX,
position: 'absolute',
'z-index': 100,
visibility: 'visible'
});
// Identify the element below the current cursor position
var elemBelow = document.elementFromPoint(cursorX + diffX, cursorY + diffY);
// 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
var 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";
}
}
function thumbnailDragEndHandler(e, target) {
// Remove the mouse event listeners
$(document).off('mousemove mouseup');
// Reset the cursor style to the default
document.body.style.cursor = "auto";
// Get the cloned element
var clone = $('#' + target.id + 'DragClone');
// If it doesn't exist, stop here
if (!clone.length) {
return;
}
var top = clone.offset().top;
var left = clone.offset().left;
// Identify the element below the cloned element position
var elemBelow = document.elementFromPoint(left + diffX, top + diffY);
// 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');
var element;
var viewportsDraggedOver = $(elemBelow).parents('.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 store drag and drop data, stop here
if (!OHIF.viewer.dragAndDropData) {
return false;
}
// Rerender the viewport using the drag and drop data
rerenderViewportWithNewSeries(element, OHIF.viewer.dragAndDropData);
Session.get('UseHangingProtocol', false);
return false;
}
Template.thumbnailEntry.onRendered(function() {
var entry = this.find('.thumbnailEntry');
$(entry).data('seriesInstanceUid', Template.parentData(0).seriesInstanceUid);
$(entry).data('studyInstanceUid', Template.parentData(1).studyInstanceUid);
});
Template.thumbnailEntry.events({
// Touch drag/drop events
'touchstart .thumbnailEntry, mousedown .thumbnailEntry': function(e) {
var data = {
studyInstanceUid: this.stack.studyInstanceUid,
seriesInstanceUid: this.stack.seriesInstanceUid
};
thumbnailDragStartHandler(e, data);
},
'touchmove .thumbnailEntry': function(e) {
thumbnailDragHandler(e, e.currentTarget);
},
'touchend .thumbnailEntry': function(e){
thumbnailDragEndHandler(e, e.currentTarget);
}
});
Template.thumbnailEntry.helpers({
seriesDescription : function() {
return this.stack.seriesDescription || '';
}
});
@@ -0,0 +1,24 @@
.thumbnailEntry
width: 105px
padding: 5px
background-color: black
border: 1px solid #424242
margin: 0 auto
cursor: copy
cursor: -webkit-grab
cursor: -moz-grab
.seriesDescription
text-align: center
color: #555
word-wrap: break-word
&:hover
background-color: white
&.active
background-color: #424242
border-color: #4e4e4e
.seriesDescription
color: white
@@ -0,0 +1,5 @@
<template name="thumbnails">
{{#each thumbnails}}
{{ >thumbnailEntry }}
{{/each}}
</template>
@@ -0,0 +1,15 @@
Template.thumbnails.helpers({
thumbnails: function() {
var study = this;
var stacks = createStacks(study);
var array = [];
stacks.forEach(function(stack, index) {
array.push({
thumbnailIndex: index,
stack: stack
});
});
return array;
}
});
@@ -0,0 +1,20 @@
<template name="hangingProtocolButtons">
<button id="previousPresentationGroup" type="button"
class="imageViewerCommand btn btn-sm btn-default"
data-container="body"
data-toggle="tooltip"
data-placement="bottom"
title="Previous presentation group"
disabled="{{ #unless isPreviousAvailable }} true {{ /unless }}">
<span class="fa fa-step-backward"></span>
</button>
<button id="nextPresentationGroup" type="button"
class="imageViewerCommand btn btn-sm btn-default"
data-container="body"
data-toggle="tooltip"
data-placement="bottom"
title="Next presentation group"
disabled="{{ #unless isNextAvailable }} true {{ /unless }}">
<span class="fa fa-step-forward"></span>
</button>
</template>
@@ -0,0 +1,11 @@
Template.hangingProtocolButtons.helpers({
'isNextAvailable': function() {
var presentationGroup = Session.get('WindowManagerPresentationGroup');
var numPresentationGroups = WindowManager.getNumPresentationGroups();
return presentationGroup < numPresentationGroups;
},
'isPreviousAvailable': function() {
var presentationGroup = Session.get('WindowManagerPresentationGroup');
return presentationGroup > 1;
}
});
@@ -0,0 +1,13 @@
<template name="imageViewerViewport">
<div class='imageViewerViewport'
oncontextmenu='return false;'
unselectable='on'
onselectstart='return false;'>
</div>
<div class='viewportInstructions'>
Please drag a stack here to view images.
</div>
{{ >loadingIndicator }}
{{ >viewportOverlay }}
{{ >viewportOrientationMarkers }}
</template>
@@ -0,0 +1,532 @@
var allCornerstoneEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
'CornerstoneToolsMultiTouchDragStart';
/**
* This function loads a study series into a viewport element.
*
* @param data {object} Object containing the study, series, and viewport element to be used
*/
function loadSeriesIntoViewport(data, templateData) {
log.info("imageViewerViewport loadSeriesIntoViewport");
// Make sure we have all the data required to render the series
if (!data.study || !data.series || !data.element) {
return;
}
// Get the current element and it's index in the list of all viewports
// The viewport index is often used to store information about a viewport element
var element = data.element;
var viewportIndex = $(".imageViewerViewport").index(element);
// Get the contentID of the current worklist tab, if the viewport is running
// alongside the worklist package
var contentId = Session.get('activeContentId');
// If the viewer is inside a tab, create an object related to the specified viewport
// This data will be saved so that the tab can be reloaded to the same state after tabs
// are switched
if (contentId) {
ViewerData[contentId].loadedSeriesData[viewportIndex] = {};
}
// Create an empty array to populate with image IDs
var imageIds = [];
// 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
var series = data.series;
var numImages = series.instances.length;
var imageId;
series.instances.forEach(function(instance, imageIndex) {
var metaData = {
instance: instance,
series: series,
study: data.study,
numImages: numImages,
imageIndex: imageIndex + 1
};
var numFrames = instance.numFrames;
if (numFrames > 1) {
log.info('Multiframe image detected');
for (var i = 0; i < numFrames; i++) {
metaData.frame = i;
imageId = getImageId(instance, i);
imageIds.push(imageId);
addMetaData(imageId, metaData);
}
} else {
imageId = getImageId(instance);
imageIds.push(imageId);
addMetaData(imageId, metaData);
}
});
// Define the current image stack using the newly created image IDs
var stack = {
currentImageIdIndex: data.currentImageIdIndex || 0,
imageIds: imageIds
};
// Get the current image ID for the stack that will be rendered
var imageId = imageIds[stack.currentImageIdIndex];
// Save the current image ID inside the template data so it can be
// retrieved from the template helpers
templateData.imageId = imageId;
// Save the current image ID inside the ViewportLoading object.
//
// The ViewportLoading object relates the viewport elements with whichever
// image is currently being loaded into them. This is useful so that we can
// place progress (download %) for each image inside the proper viewports.
ViewportLoading[viewportIndex] = imageId;
// Enable Cornerstone for the viewport element
//
// NOTE: This uses the experimental WebGL renderer for Cornerstone!
// If you have problems, replace it with this line instead:
// cornerstone.enable(element);
cornerstone.enable(element, cornerstone.webGL.renderer.render);
// Get the handler functions that will run when loading has finished or thrown
// an error. These are used to show/hide loading / error text boxes on each viewport.
var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
// Start loading the image.
cornerstone.loadAndCacheImage(imageId).then(function(image) {
// If there is a saved object containing Cornerstone viewport data
// (e.g. scale, invert, window settings) in the input data, apply it now.
//
// Otherwise, display the loaded image in the viewport element with the
// default viewport settings.
if (data.viewport) {
cornerstone.displayImage(element, image, data.viewport);
} else {
cornerstone.displayImage(element, image);
}
// Remove the data for this viewport from the ViewportLoading object
// This will stop the loading percentage complete from being displayed.
delete ViewportLoading[viewportIndex];
// Call the handler function that represents the end of the image loading phase
// (e.g. hide the progress text box)
endLoadingHandler(element);
// Resize the canvas to fit the current viewport element size. Fit the displayed
// image to the canvas dimensions.
cornerstone.resize(element, true);
// Remove the 'empty' class from the viewport to hide any instruction text
element.classList.remove('empty');
// Hide the viewport instructions (i.e. 'Drag a stack here') and show
// the viewport overlay data.
$(element).siblings('.viewportInstructions').hide();
$(element).siblings('.imageViewerViewportOverlay').show();
// Add stack state managers for the stack tool, CINE tool, and reference lines
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
// Get the current viewport settings
var viewport = cornerstone.getViewport(element);
// Enable orientation markers, if applicable
updateOrientationMarkers(element);
// Clear any old stack data
cornerstoneTools.clearToolState(element, 'stack');
cornerstoneTools.addToolState(element, 'stack', stack);
// Enable mouse, mouseWheel, and touch input on the element
cornerstoneTools.mouseInput.enable(element);
cornerstoneTools.touchInput.enable(element);
cornerstoneTools.mouseWheelInput.enable(element);
// Use the tool manager to enable the currently active tool for this
// newly rendered element
var activeTool = toolManager.getActiveTool();
toolManager.setActiveTool(activeTool, [element]);
// Define a function to run whenever the Cornerstone viewport is rendered
// (e.g. following a change of window or zoom)
function onImageRendered(e, eventData) {
log.info('imageViewerViewport onImageRendered');
// Use Session to trigger reactive updates in the viewportOverlay helper functions
// This lets the viewport overlay always display correct window / zoom values
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
// Save the current viewport into the ViewerData global variable, as well as the
// Meteor Session. This lets the viewport be saved/reloaded on a hot-code reload
var viewport = cornerstone.getViewport(element);
ViewerData[contentId].loadedSeriesData[viewportIndex].viewport = viewport;
Session.set('ViewerData', ViewerData);
}
// Attach the onImageRendered callback to the CornerstoneImageRendered event
$(element).off('CornerstoneImageRendered', onImageRendered);
$(element).on('CornerstoneImageRendered', onImageRendered);
//TODO: ********************************************
//TODO: Delete a lesion, if ctrl+d or del is pressed after lesion selected
//TODO: getTimepointObject should be global function to use everywhere
function getTimepointObject(imageId) {
var study = cornerstoneTools.metaData.get('study', imageId);
return Timepoints.findOne({timepointName: study.studyDate});
}
// ctrl+d is used to detect delete event if del key is not found on keyboard.
// Id key is down
var keyMap = {17: false, 68: false, 46: false};
function keyPressDownHandler(e, lesionData){
if (e.keyCode in keyMap) {
keyMap[e.keyCode] = true;
if (keyMap[46] || keyMap[17] && keyMap[68]) {
var toolType = lesionData.isTarget ? 'lesion' : 'nonTarget';
var imageId = lesionData.imageId;
var toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.toolState;
Object.keys(toolState).forEach(function(imageIdKey){
if(imageIdKey === imageId) {
var toolStateArray = toolState[imageId][toolType].data;
toolStateArray.forEach(function(data, index) {
if(data.lesionNumber === lesionData.lesionNumber){
toolState[imageId][toolType].data.splice(index, 1);
// Remove from collection
var patientId = data.patientId;
var enabledElement = cornerstone.getEnabledElement(element);
var elementImageId = enabledElement.image.imageId;
var timepointData = getTimepointObject(elementImageId);
var lesionObject = {
patientId: patientId,
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget,
timepointId: timepointData.timepointID
};
// Remove patient measurement
Meteor.call('removePatientMeasurement', lesionObject);
//Update element
cornerstone.updateImage(element);
}
});
}
});
}
}
}
// When key is up
function keyPressUpHandler(e){
if (e.keyCode in keyMap) {
keyMap[e.keyCode] = false;
}
}
// CornerstoneToolsMouseDown event callback
function onMouseDown(e,eventData) {
var element = e.currentTarget;
var distanceSq = 5;
var coords = eventData.startPoints.canvas;
var toolTypes = ["lesion", "nonTarget"];
toolTypes.forEach(function(toolType){
var toolData = cornerstoneTools.getToolState(element, toolType);
// now check to see if there is a handle we can move
if (toolData) {
for (var i = 0; i < toolData.data.length; i++) {
var data = toolData.data[i];
var handle = cornerstoneTools.getHandleNearImagePoint(element, data.handles, coords, distanceSq);
if(handle) {
$(document).off("keydown");
$(document).on('keydown', function(e){
keyPressDownHandler(e,data);
return;
});
$(document).off("keyup");
$(document).on('keyup',keyPressUpHandler);
return;
}
}
}
});
}
$(element).on('CornerstoneToolsMouseDown', onMouseDown);
$(element).on('CornerstoneToolsMouseDown', onMouseDown);
//TODO: Delete a lesion ends
//TODO: ********************************************
// Set a random value for the Session variable in order to trigger an overlay update
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
// Define a function to run whenever the Cornerstone viewport changes images
// (e.g. during scrolling)
function onNewImage(e, eventData) {
log.info('imageViewerViewport onNewImage');
// Update the templateData with the new imageId
// This allows the template helpers to update reactively
templateData.imageId = eventData.enabledElement.image.imageId;
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
// If this viewport is displaying a stack of images, save the current image
// index in the stack to the global ViewerData object, as well as the Meteor Session.
var stack = cornerstoneTools.getToolState(element, 'stack');
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
var imageIdIndex = stack.data[0].imageIds.indexOf(templateData.imageId);
ViewerData[contentId].loadedSeriesData[viewportIndex].currentImageIdIndex = imageIdIndex;
Session.set('ViewerData', ViewerData);
}
}
// Attach the onNewImage callback to the CornerstoneNewImage event
$(element).off('CornerstoneNewImage', onNewImage);
$(element).on('CornerstoneNewImage', onNewImage);
// Set a random value for the Session variable in order to trigger an overlay update
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
// Define a function to trigger an event whenever a new viewport is being used
// This is used to update the value of the "active viewport", when the user interacts
// with a new viewport element
function sendActivationTrigger(e, eventData) {
// Check if the current active viewport in the Meteor Session
// Is the same as the viewport in which the activation event was fired.
// If it was, no changes are necessary, so stop here.
var activeViewportIndex = Session.get('activeViewport');
var viewportIndex = $(".imageViewerViewport").index(eventData.element);
if (viewportIndex === activeViewportIndex) {
return;
}
log.info('imageViewerViewport sendActivationTrigger');
// Otherwise, trigger an 'ActivateViewport' event to be handled by the Template event
// handler
eventData.viewportIndex = viewportIndex;
var customEvent = $.Event('ActivateViewport', eventData);
// Need to overwrite the type set in the original event
customEvent.type = 'ActivateViewport';
$(e.target).trigger(customEvent, eventData);
}
// Attach the sendActivationTrigger function to all of the Cornerstone interaction events
$(element).off(allCornerstoneEvents, sendActivationTrigger);
$(element).on(allCornerstoneEvents, sendActivationTrigger);
// Store the current series data inside a global variable
OHIF.viewer.loadedSeriesData[viewportIndex] = {
studyInstanceUid: data.studyInstanceUid,
seriesInstanceUid: data.seriesInstanceUid,
currentImageIdIndex: data.currentImageIdIndex,
viewport: viewport
};
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
// Check if image plane (orientation / loction) data is present for the current image
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
// If it is, and reference lines are enabled, add this element to the global synchronizer
// that is used for updating reference lines, and enable reference lines for this viewport.
if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
OHIF.viewer.updateImageSynchronizer.add(element);
displayReferenceLines(element);
}
if (viewportIndex === Session.get('activeViewport')) {
enablePrefetchOnElement(element);
}
// Run any renderedCallback that exists in the data context
if (data.renderedCallback && typeof data.renderedCallback === "function") {
data.renderedCallback(element);
}
}, function(error) {
// If something goes wrong while loading the image, fire the error handler.
errorLoadingHandler(element, imageId, error);
});
}
/**
* This function sets series for the study and calls LoadSeriesIntoViewport function
*
* @param data includes study data
* @param seriesInstanceUid series information which is loaded in Template
* @param templateData currentData of Template
*
*/
function setSeries(data,seriesInstanceUid, templateData){
var study = data.study;
study.seriesList.every(function(series) {
if (series.seriesInstanceUid === seriesInstanceUid) {
data.series = series;
return false;
}
return true;
});
// If we didn't find anything, stop here
if (!data.series) {
return;
}
// Otherwise, load pass the data object into loadSeriesIntoViewport
loadSeriesIntoViewport(data, templateData);
}
/**
* This function searches an object to return the keys that contain a specific value
*
* @param object {object} The object to be searched
* @param value The value to be found
*
* @returns {array} The keys for which the object has the specified value
*/
function getKeysByValue(object, value) {
// http://stackoverflow.com/questions/9907419/javascript-object-get-key-by-value
return Object.keys(object).filter(function(key) {
return object[key] === value;
});
}
Meteor.startup(function() {
// On Meteor startup, define the global objects used to store loading imageIds
// by viewport / thumbnail element
ViewportLoading = {};
ThumbnailLoading = {};
// Whenever the CornerstoneImageLoadProgress is fired, identify which viewports
// the "in-progress" image is to be displayed in. Then pass the percent complete
// via the Meteor Session to the other templates to be displayed in the relevant viewports.
$(cornerstone).on('CornerstoneImageLoadProgress', function(e, eventData) {
viewportIndices = getKeysByValue(ViewportLoading, eventData.imageId);
viewportIndices.forEach(function(viewportIndex) {
Session.set('CornerstoneLoadProgress' + viewportIndex, eventData.percentComplete);
});
thumbnailIndices = getKeysByValue(ThumbnailLoading, eventData.imageId);
thumbnailIndices.forEach(function(thumbnailIndex) {
Session.set('CornerstoneThumbnailLoadProgress' + thumbnailIndex, eventData.percentComplete);
});
});
var config = {
magnifySize: 300,
magnificationLevel: 3
};
cornerstoneTools.magnify.setConfiguration(config);
});
Template.imageViewerViewport.onRendered(function() {
var templateData = Template.currentData();
log.info("imageViewerViewport onRendered");
// When the imageViewerViewport template is rendered
var element = this.find(".imageViewerViewport");
// Display the loading indicator for this element
$(element).siblings('.imageViewerLoadingIndicator').css('display', 'block');
// Get the current active viewport index, if this viewport has the same index,
// add the CSS 'active' class to highlight this viewport.
var activeViewport = Session.get('activeViewport');
if (activeViewport === this.data.viewportIndex) {
$('#imageViewerViewports .viewportContainer').removeClass('active');
$(element).parents('.viewportContainer').addClass('active');
}
// Create a data object to pass to the series loading function (loadSeriesIntoViewport)
var data = {
element: element,
viewport: this.data.viewport,
currentImageIdIndex: this.data.currentImageIdIndex,
studyInstanceUid: this.data.studyInstanceUid,
seriesInstanceUid: this.data.seriesInstanceUid,
renderedCallback: this.data.renderedCallback,
activeViewport: activeViewport
};
// If no seriesInstanceUid or studyInstanceUid were supplied, display the drag/drop
// instructions and then stop here since we don't know what to display in the viewport.
if (!this.data.seriesInstanceUid || !this.data.studyInstanceUid) {
element.classList.add('empty');
$(element).siblings('.imageViewerLoadingIndicator').css('display', 'none');
$(element).siblings('.viewportInstructions').show();
return;
}
// Look through the ViewerStudies collection for a
// study with this studyInstanceUid
var study = ViewerStudies.findOne({
studyInstanceUid: this.data.studyInstanceUid
});
var seriesInstanceUid = this.data.seriesInstanceUid;
// TODO: This code block might be refactored
// Load previous measurement study when reloading a patient
if (!study) {
Meteor.call('GetStudyMetadata', this.data.studyInstanceUid, function(error, study) {
// Once we have retrieved the data, we sort the series' by series
// and instance number in ascending order
if(!study){
return;
}
sortStudy(study);
data.study = study;
setSeries(data, seriesInstanceUid, templateData);
return;
});
}
data.study = study;
setSeries(data, seriesInstanceUid, templateData);
});
Template.imageViewerViewport.onDestroyed(function() {
log.info("imageViewerViewport onDestroyed");
// When a viewport element is being destroyed
var element = this.find(".imageViewerViewport");
// Try to stop any currently playing clips
// Otherwise the interval will continuously throw errors
try {
cornerstoneTools.stopClip(element);
} catch(error) {
log.warn(error);
}
// Disable the viewport element with Cornerstone
// This also triggers the removal of the element from all available
// synchronizers, such as the one used for reference lines.
cornerstone.disable(element);
});
Template.imageViewerViewport.events({
'ActivateViewport .imageViewerViewport': function(e) {
log.info("imageViewerViewport ActivateViewport");
setActiveViewport(e.currentTarget);
}
});
@@ -0,0 +1,41 @@
.imageViewerViewport
width: 100%
height: 100%
background-color: black
// Prevents the entire page from getting larger
// when the magnify tool is near the sides/corners of the page
overflow: hidden
.viewportInstructions
display: none
font-size: 13px
color: #e4ad00
line-height: 18px
pointer-events: none // Necessary for drag/drop through to cornerstone element below
text-align: center
position: absolute
top: 0
bottom: 0
right: 0
left: 0
margin: auto
height: 20px
/* These are some good CSS settings for a circular magnifying glass
Try removing the border-radius to make a square magnifying glass */
.magnifyTool{
border: 4px white solid;
box-shadow: 2px 2px 10px #1e1e1e;
border-radius: 50%;
display: none;
cursor: none;
}
/* prevents selection when left click dragging */
.disable-selection {
-moz-user-select: none; -webkit-user-select: none; -ms-user-select:none; user-select:none;
}
/* prevents cursor from changing to the i bar on the overlays*/
.noIbar {
cursor:default;
}
@@ -0,0 +1,11 @@
<template name="imageViewerViewports">
<div id='imageViewerViewports'>
{{ #each viewerWindow }}
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
<div class="removable">
{{ >imageViewerViewport}}
</div>
</div>
{{ /each }}
</div>
</template>
@@ -0,0 +1,186 @@
ViewerWindows = new Meteor.Collection(null);
Template.imageViewerViewports.helpers({
height: function() {
var viewportRows = this.viewportRows || 1;
return 100 / viewportRows;
},
width: function() {
var viewportColumns = this.viewportColumns || 1;
return 100 / viewportColumns;
},
viewerWindow: function() {
ViewerWindows = new Meteor.Collection(null);
log.info("imageViewerViewports viewportArray");
var viewportRows = this.viewportRows || 1;
var viewportColumns = this.viewportColumns || 1;
var contentId = this.contentId || $("#viewer").parents(".tab-pane.active").attr('id');
if (this.viewportRows && this.viewportColumns) {
viewportRows = this.viewportRows || 1;
viewportColumns = this.viewportColumns || 1;
} else if (ViewerData[contentId].viewportRows && ViewerData[contentId].viewportColumns) {
viewportRows = ViewerData[contentId].viewportRows;
viewportColumns = ViewerData[contentId].viewportColumns;
}
var viewportData;
if (!$.isEmptyObject(ViewerData[contentId].loadedSeriesData)) {
viewportData = ViewerData[contentId].loadedSeriesData;
}
var inputData = {
viewportColumns: viewportColumns,
viewportRows: viewportRows
};
inputData.DisplaySetPresentationGroup = Session.get('WindowManagerPresentationGroup');
var hangingProtocolViewportData = WindowManager.getHangingProtocol(inputData);
if (Session.get('UseHangingProtocol')) {
viewportData = hangingProtocolViewportData.viewports;
viewportRows = hangingProtocolViewportData.viewportRows || viewportRows;
viewportColumns = hangingProtocolViewportData.viewportColumns || viewportColumns;
}
// Update viewerData
ViewerData[contentId].viewportRows = viewportRows;
ViewerData[contentId].viewportColumns = viewportColumns;
Session.set("ViewerData", ViewerData);
this.viewportRows = viewportRows;
this.viewportColumns = viewportColumns;
var numViewports = viewportRows * viewportColumns;
for (var i=0; i < numViewports; ++i) {
var data = {
viewportIndex: i,
// These two are necessary because otherwise the width and height helpers
// don't get the right data context. Seems to be related to the "each" loop.
viewportColumns: viewportColumns,
viewportRows: viewportRows
};
if (viewportData && !$.isEmptyObject(viewportData[i])) {
data.seriesInstanceUid = viewportData[i].seriesInstanceUid;
data.studyInstanceUid = viewportData[i].studyInstanceUid;
data.currentImageIdIndex = viewportData[i].currentImageIdIndex;
data.viewport = viewportData[i].viewport;
} else if (hangingProtocolViewportData && !$.isEmptyObject(hangingProtocolViewportData.viewports[i])) {
data.seriesInstanceUid = hangingProtocolViewportData.viewports[i].seriesInstanceUid;
data.studyInstanceUid = hangingProtocolViewportData.viewports[i].studyInstanceUid;
data.currentImageIdIndex = hangingProtocolViewportData.viewports[i].currentImageIdIndex;
data.viewport = hangingProtocolViewportData.viewports[i].viewport;
}
ViewerWindows.insert(data);
}
// Here we will find out if we need to load any other studies into the viewer
// We will make a list of unique studyInstanceUids
var uniqueStudyInstanceUids = [];
// Meteor doesn't support Mongo's 'distinct' function, so we have to do this in a loop
ViewerWindows.find().forEach(function(window) {
var studyInstanceUid = window.studyInstanceUid;
if (!studyInstanceUid) {
return;
}
// If this studyInstanceUid is already in the list, stop here
if (uniqueStudyInstanceUids.indexOf(studyInstanceUid) > -1) {
return;
}
// Otherwise, add it to the list
uniqueStudyInstanceUids.push(studyInstanceUid);
// If any of the associated studies is not already loaded, load it now
var loadedStudy = ViewerStudies.findOne({studyInstanceUid: studyInstanceUid});
if (!loadedStudy) {
// Load the study
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
// Sort the study's series and instances by series and instance number
sortStudy(study);
// Insert it into the ViewerStudies Collection
ViewerStudies.insert(study);
});
}
});
return ViewerWindows.find();
}
});
var savedSeriesData,
savedViewportRows,
savedViewportColumns;
Template.imageViewerViewports.events({
'CornerstoneMouseDoubleClick .imageViewerViewport': function(e) {
var container = $(".viewerMain").get(0);
var data;
var contentId = this.contentId || $("#viewer").parents(".tab-pane.active").attr('id');
// If there is more than one viewport on screen
// And one of them is double-clicked, it should be rendered alone
// If it is double-clicked again, the viewer should revert to the previous layout
if ($(e.currentTarget).hasClass('zoomed')) {
// Revert to saved settings
ViewerData[contentId].loadedSeriesData = $.extend(true, {}, savedSeriesData);
ViewerData[contentId].viewportRows = savedViewportRows;
ViewerData[contentId].viewportColumns = savedViewportColumns;
savedViewportRows = 0;
savedViewportColumns = 0;
data = {
viewportRows: ViewerData[contentId].viewportRows,
viewportColumns: ViewerData[contentId].viewportColumns
};
// Render the imageViewerViewports template with these settings
$('#imageViewerViewports').remove();
UI.renderWithData(Template.imageViewerViewports, data, container);
// Remove the 'zoomed' class from any viewports
$('.imageViewerViewport').removeClass('zoomed');
} else {
// Zoom to single viewport
// If only one viewport is on-screen, stop here
if (ViewerData[contentId].viewportRows === 1 &&
ViewerData[contentId].viewportColumns === 1) {
return;
}
// Save the current settings
savedSeriesData = $.extend(true, {}, ViewerData[contentId].loadedSeriesData);
savedViewportRows = ViewerData[contentId].viewportRows;
savedViewportColumns = ViewerData[contentId].viewportColumns;
// Get the clicked-on viewport's index
var viewportIndex = this.viewportIndex;
// Set the first viewport's data to be the same as the currently clicked-on viewport
ViewerData[contentId].loadedSeriesData[0] = ViewerData[contentId].loadedSeriesData[viewportIndex];
// Set the basic template data
data = {
viewportRows: 1,
viewportColumns: 1
};
// Render the imageViewerViewports template with these settings
$('#imageViewerViewports').remove();
UI.renderWithData(Template.imageViewerViewports, data, container);
// Add the 'zoomed' class to the lone remaining viewport
$('.imageViewerViewport').eq(0).addClass('zoomed');
}
}
});
@@ -0,0 +1,18 @@
#imageViewerViewports
height: calc(100% - 30px)
width: 100%
.viewportContainer
float: left
border: 2px solid #777
&.active, &:hover.active
border: 2px solid #9A6617
&:hover
border: 2px solid white
.removable
width: 100%
height: 100%
position: relative // Necessary so that the viewportOverlay is on top of the viewports
@@ -0,0 +1,6 @@
<template name="layoutButton">
<button id="layout" type="button" class="btn btn-sm btn-default dropdown-toggle" data-container="body" data-toggle="dropdown" aria-expanded="false" data-placement="right" title="Layout" rel="tooltip">
<span class="fa fa-th-large"></span>
</button>
{{ >layoutChooser }}
</template>
@@ -0,0 +1,21 @@
<template name="layoutChooser">
<div class="layoutChooser pull-right dropdown-menu" role="menu">
<table>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
</template>
@@ -0,0 +1,51 @@
/**
* Adds the 'hover' class to cells above and to the left of the current cell
* This is used to "fill in" the grid that the user will change the layout to,
* if they click on a specific table cell.
*
* @param currentCell
*/
function highlightCells(currentCell) {
var cells = $('.layoutChooser table td');
cells.removeClass('hover');
currentCell = $(currentCell);
var table = currentCell.parents('.layoutChooser table').get(0);
var rowIndex = currentCell.closest('tr').index();
var columnIndex = currentCell.index();
// Loop through the table row by row
// and cell by cell to apply the highlighting
for (var i = 0; i < table.rows.length; i++) {
row = table.rows[i];
if (i <= rowIndex) {
for (var j = 0; j < row.cells.length; j++) {
if (j <= columnIndex) {
cell = row.cells[j];
cell.classList.add('hover');
}
}
}
}
}
Template.layoutChooser.events({
'touchstart .layoutChooser table td, mouseenter .layoutChooser table td': function(evt) {
highlightCells(evt.currentTarget);
},
'click .layoutChooser table td': function(evt) {
$('#imageViewerViewports').remove();
var currentCell = $(evt.currentTarget);
var rowIndex = currentCell.closest('tr').index();
var columnIndex = currentCell.index();
// Add 1 because the indices start from zero
var data = {
viewportRows: rowIndex + 1,
viewportColumns: columnIndex + 1,
};
var container = $(".viewerMain").get(0);
UI.renderWithData(Template.imageViewerViewports, data, container);
}
});
@@ -0,0 +1,17 @@
.layoutChooser
width: 75px
min-width: 75px // to override bootstrap's dropdown-menu class
height: 75px
table
margin: 0 auto
td
cursor: pointer
border: 2px solid black
width: 20px
height: 20px
transition: background-color 0.1s linear
&:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave
background-color: gray
@@ -0,0 +1,14 @@
<template name="loadingIndicator">
<div class="imageViewerLoadingIndicator loadingIndicator">
<div class="indicatorContents">
<p>Loading... {{percentComplete}}</p>
</div>
</div>
<div class="imageViewerErrorLoadingIndicator loadingIndicator">
<div class="indicatorContents">
<h4>Error Loading Image</h4>
<p class='description'>An error has occurred.</p>
<p class='details'></p>
</div>
</div>
</template>
@@ -0,0 +1,70 @@
Meteor.startup(function() {
cornerstoneTools.loadHandlerManager.setStartLoadHandler(startLoadingHandler);
cornerstoneTools.loadHandlerManager.setEndLoadHandler(doneLoadingHandler);
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(errorLoadingHandler);
});
var loadHandlerTimeout;
startLoadingHandler = function(element) {
clearTimeout(loadHandlerTimeout);
loadHandlerTimeout = setTimeout(function() {
var elem = $(element);
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
elem.find('canvas').not('.magnifyTool').addClass("faded");
elem.siblings('.imageViewerLoadingIndicator').css('display', 'block');
}, OHIF.viewer.loadIndicatorDelay);
};
doneLoadingHandler = function(element) {
clearTimeout(loadHandlerTimeout);
var elem = $(element);
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
elem.find('canvas').not('.magnifyTool').removeClass("faded");
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
};
errorLoadingHandler = function(element, imageId, error, source) {
clearTimeout(loadHandlerTimeout);
var elem = $(element);
// Could probably chain all of these, but this is more readable
elem.find('canvas').not('.magnifyTool').removeClass("faded");
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
// Don't display errors from the stackPrefetch tool
if (source === "stackPrefetch") {
return;
}
var errorLoadingIndicator = elem.siblings('.imageViewerErrorLoadingIndicator');
errorLoadingIndicator.css('display', 'block');
var cleanedImageId = imageId;
// This is just used to expand upon some error messages that are sent
// when things fail. An example is a network error throwing the error
// which is only described as "network".
var errorDetails = {
network: "A network error has occurred"
// We need to expand this further when we see more obscure error messages
};
if (errorDetails.hasOwnProperty(error)) {
error = errorDetails[error];
}
errorLoadingIndicator.find('.description').text("An error has occurred while loading image: " + cleanedImageId);
if (error) {
errorLoadingIndicator.find('.details').text("Details: " + error);
}
};
Template.loadingIndicator.helpers({
'percentComplete': function(e) {
var percentComplete = Session.get('CornerstoneLoadProgress' + this.viewportIndex);
if (percentComplete && percentComplete !== 100) {
return percentComplete + '%';
}
}
});
@@ -0,0 +1,31 @@
.imageViewerLoadingIndicator
display: none
color: #ffa500
.faded
opacity: 0.5
.imageViewerErrorLoadingIndicator
display: none
background-color: rgba(0,0,0,0.75)
p, h4
text-align: center
color: #ffa500
padding: 4px 0px
p
font-size: 11pt
.loadingIndicator
pointer-events: none // Necessary for click-through to cornerstone element below
width: 100%
height: 100%
font-size: 18px
z-index: 100
.indicatorContents
position: absolute
top: 40%
width: 100%
text-align: center
@@ -0,0 +1,9 @@
<template name="playClipButton">
<button id="playClip" type="button" class="imageViewerCommand btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Play/Stop Clip">
{{ #if isPlaying }}
<span class="fa fa-stop"></span>
{{ else }}
<span class="fa fa-play"></span>
{{ /if }}
</button>
</template>
@@ -0,0 +1,34 @@
toggleCinePlay = function(element) {
if (!element) {
var activeViewport = Session.get('activeViewport');
element = $('.imageViewerViewport').get(activeViewport);
}
if (!element) {
return;
}
var viewportIndex = $('.imageViewerViewport').index(element);
var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
if (isPlaying === true) {
cornerstoneTools.stopClip(element);
} else {
cornerstoneTools.playClip(element);
}
OHIF.viewer.isPlaying[viewportIndex] = !OHIF.viewer.isPlaying[viewportIndex];
Session.set('UpdateCINE', Random.id());
};
Template.playClipButton.helpers({
'isPlaying': function() {
Session.get('UpdateCINE');
var activeViewport = Session.get('activeViewport');
return !!OHIF.viewer.isPlaying[activeViewport];
}
});
Template.playClipButton.events({
'click #playClip': function() {
toggleCinePlay();
}
});
@@ -0,0 +1,5 @@
<template name="simpleToolbarButton">
<button id="{{id}}" type="button" class="{{classes}} btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="{{title}}">
<span class="{{iconClasses}}"></span>
</button>
</template>
@@ -0,0 +1,21 @@
<template name="toolbar">
<div id='toolbar'>
<div class="btn-group">
{{ #each button }}
{{ >simpleToolbarButton}}
{{ /each }}
{{ #if includePlayClipButton }}
{{ >playClipButton }}
{{ /if }}
{{ #if includeLayoutButton }}
{{ >layoutButton }}
{{ /if }}
{{ #if includeHangingProtocolButtons }}
{{ >hangingProtocolButtons }}
{{ /if }}
</div>
</div>
</template>
@@ -0,0 +1,168 @@
function getDefaultButtonData() {
var buttonData = [];
buttonData.push({
id: 'wwwc',
title: 'WW/WC',
classes: 'imageViewerTool',
iconClasses: 'fa fa-sun-o'
});
buttonData.push({
id: 'wwwcRegion',
title: 'Window by Region',
classes: 'imageViewerTool',
iconClasses: 'fa fa-square'
});
buttonData.push({
id: 'magnify',
title: 'Magnify',
classes: 'imageViewerTool',
iconClasses: 'fa fa-circle'
});
buttonData.push({
id: 'annotate',
title: 'Annotation',
classes: 'imageViewerTool',
iconClasses: 'fa fa-arrows-h'
});
buttonData.push({
id: 'invert',
title: 'Invert',
classes: 'imageViewerCommand',
iconClasses: 'fa fa-adjust'
});
buttonData.push({
id: 'zoom',
title: 'Zoom',
classes: 'imageViewerTool',
iconClasses: 'fa fa-search'
});
buttonData.push({
id: 'pan',
title: 'Pan',
classes: 'imageViewerTool',
iconClasses: 'fa fa-arrows'
});
buttonData.push({
id: 'stackScroll',
title: 'Stack Scroll',
classes: 'imageViewerTool',
iconClasses: 'fa fa-bars'
});
buttonData.push({
id: 'length',
title: 'Length Measurement',
classes: 'imageViewerTool',
iconClasses: 'fa fa-arrows-v'
});
buttonData.push({
id: 'angle',
title: 'Angle Measurement',
classes: 'imageViewerTool',
iconClasses: 'fa fa-angle-left'
});
buttonData.push({
id: 'dragProbe',
title: 'Pixel Probe',
classes: 'imageViewerTool',
iconClasses: 'fa fa-dot-circle-o'
});
buttonData.push({
id: 'ellipticalRoi',
title: 'Elliptical ROI',
classes: 'imageViewerTool',
iconClasses: 'fa fa-circle-o'
});
buttonData.push({
id: 'rectangleRoi',
title: 'Rectangle ROI',
classes: 'imageViewerTool',
iconClasses: 'fa fa-square-o'
});
buttonData.push({
id: 'resetViewport',
title: 'Reset Viewport',
classes: 'imageViewerCommand',
iconClasses: 'fa fa-undo'
});
buttonData.push({
id: 'clearTools',
title: 'Clear tools',
classes: 'imageViewerCommand',
iconClasses: 'fa fa-trash'
});
return buttonData;
}
Template.toolbar.events({
'click .imageViewerTool': function(e) {
$(e.currentTarget).tooltip('hide');
var tool = e.currentTarget.id;
console.log('Setting active tool to: ' + tool);
var elements = $('.imageViewerViewport');
toolManager.setActiveTool(tool, elements);
},
'click .imageViewerCommand': function(e) {
$(e.currentTarget).tooltip('hide');
var command = e.currentTarget.id;
if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
return;
}
var activeViewport = Session.get('activeViewport');
var element = $('.imageViewerViewport').get(activeViewport);
OHIF.viewer.functionList[command](element);
}
});
Template.toolbar.onRendered(function() {
var tooltipButtons = $('[data-toggle="tooltip"]');
tooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
// Enable tooltips for the layout button
var extraTooltipButtons = $('[rel="tooltip"]');
extraTooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
});
Template.toolbar.helpers({
'button': function() {
if (this.toolbarOptions && this.toolbarOptions.buttonData) {
return this.toolbarOptions.buttonData;
}
return getDefaultButtonData();
},
'includePlayClipButton': function() {
if (this.toolbarOptions && this.toolbarOptions.includePlayClipButton !== undefined) {
return this.toolbarOptions.includePlayClipButton;
}
return true;
},
'includeLayoutButton': function() {
if (this.toolbarOptions && this.toolbarOptions.includeLayoutButton !== undefined) {
return this.toolbarOptions.includeLayoutButton;
}
return true;
},
'includeHangingProtocolButtons': function() {
if (this.toolbarOptions && this.toolbarOptions.includeHangingProtocolButtons !== undefined) {
return this.toolbarOptions.includeHangingProtocolButtons;
}
return true;
}
});
@@ -0,0 +1,16 @@
#toolbar
background-color: black
text-align: center
.btn-group
display: inline-block
button
text-align: center
color: #ffffff
background-color: #424242
border-color: #424242
outline: none
&.active, &:active
background-color: #3B678E
@@ -0,0 +1,8 @@
<template name="viewportOrientationMarkers">
<div class="viewportOrientationMarkers noselect">
<div class="topMid orientationMarker">
</div>
<div class="leftMid orientationMarker">
</div>
</div>
</template>
@@ -0,0 +1,57 @@
/**
* 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
*/
updateOrientationMarkers = function(element) {
// Get the current viewport settings
var viewport = cornerstone.getViewport(element);
var enabledElement = cornerstone.getEnabledElement(element);
var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
return;
}
var rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
var columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
var oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
var oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
var 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
var viewportOrientationMarkers = $(element).siblings(".viewportOrientationMarkers");
var topMarker = viewportOrientationMarkers.find('.topMid');
var 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);
}
};
@@ -0,0 +1,18 @@
.viewportOrientationMarkers
pointer-events: none // Necessary for click-through to cornerstone element below
font-size: 15px
color: rgb(204, 204, 204)
line-height: 18px
.orientationMarker
position: absolute
.topMid
top: 5px
left: 50%
.leftMid
top: 47%
left: 5px
@@ -0,0 +1,25 @@
<template name="viewportOverlay">
<div class="imageViewerViewportOverlay noselect">
<div class="topleft dicomTag">
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
<div class='priorIndicator'>{{prior}}</div>
</div>
<div class="topright dicomTag">
<div>{{studyDescription}}</div>
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
</div>
<div class="bottomright dicomTag">
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
<div>{{compression}}</div>
<div>{{wwwc}}</div>
</div>
<div class="bottomleft dicomTag">
<div>Ser: {{seriesNumber}}</div>
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
<div>{{frameRate}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
</div>
</div>
</template>
@@ -0,0 +1,179 @@
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;
}
function getPatient(property) {
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var patient = cornerstoneTools.metaData.get('patient', this.imageId);
if (!patient) {
return '';
}
return patient[property];
}
function getStudy(property) {
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var study = cornerstoneTools.metaData.get('study', this.imageId);
if (!study) {
return '';
}
return study[property];
}
function getSeries(property) {
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var series = cornerstoneTools.metaData.get('series', this.imageId);
if (!series) {
return '';
}
return series[property];
}
function getInstance(property) {
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return false;
}
var instance = cornerstoneTools.metaData.get('instance', this.imageId);
if (!instance) {
return '';
}
return instance[property];
}
function getImage(viewportIndex) {
var element = getElementIfNotEmpty(viewportIndex);
if (!element) {
return false;
}
var enabledElement;
try {
enabledElement = cornerstone.getEnabledElement(element);
} catch(error) {
return false;
}
if (!enabledElement || !enabledElement.image) {
return false;
}
return enabledElement.image;
}
Template.viewportOverlay.helpers({
wwwc: function() {
Session.get('CornerstoneImageRendered' + this.viewportIndex);
var element = getElementIfNotEmpty(this.viewportIndex);
if (!element) {
return '';
}
var viewport = cornerstone.getViewport(element);
if (!viewport) {
return '';
}
return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
},
zoom: function() {
Session.get('CornerstoneImageRendered' + this.viewportIndex);
var element = getElementIfNotEmpty(this.viewportIndex);
if (!element) {
return '';
}
var viewport = cornerstone.getViewport(element);
if (!viewport) {
return '';
}
return (viewport.scale * 100.0);
},
imageDimensions: function() {
Session.get('CornerstoneNewImage' + this.viewportIndex);
var image = getImage(this.viewportIndex);
if (!image) {
return '';
}
return image.width + ' x ' + image.height;
},
patientName : function() {
return getPatient.call(this, 'name');
},
patientId : function() {
return getPatient.call(this, 'id');
},
studyDate : function() {
return getStudy.call(this, 'studyDate');
},
studyTime : function() {
return getStudy.call(this, 'studyTime');
},
studyDescription : function() {
return getStudy.call(this, 'studyDescription');
},
seriesDescription : function() {
return getSeries.call(this, 'seriesDescription');
},
seriesNumber : function() {
return getSeries.call(this, 'seriesNumber');
},
imageNumber : function() {
return getInstance.call(this, 'instanceNumber');
},
imageIndex : function() {
return getInstance.call(this, 'index');
},
numImages : function() {
return getSeries.call(this, 'numImages');
},
prior : function() {
// This helper is updated whenever a new image is displayed in the viewport
Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) {
return;
}
// Make sure there are more than two studies loaded in the viewer
//
// Here we sort the collection in ascending order by study date, so
// that we can obtain the oldest study as the first element of the array
//
// TODO= Find out if we should encode studyDate as a Date in the ViewerStudies Collection
var viewportStudies = ViewerStudies.find({}, {sort: {studyDate: 1}});
if (viewportStudies.count() < 2) {
return;
}
// Get study data
var study = cornerstoneTools.metaData.get('study', this.imageId);
if (study.studyInstanceUid === viewportStudies.fetch()[0].studyInstanceUid) {
return 'Prior';
}
}
});
@@ -0,0 +1,31 @@
.imageViewerViewportOverlay
font-size: 13px
color: #e4ad00
line-height: 18px
display: none // Shown when an image is loaded
.dicomTag
pointer-events: none // Necessary for drag/drop through to cornerstone element below
position: absolute
.topleft
top: 3px
left: 3px
.topright
top: 3px
right: 3px
text-align: right
.bottomleft
bottom: 3px
left: 3px
.bottomright
bottom: 3px
right: 3px
text-align: right
.priorIndicator
font-weight: bold
color: yellow