Merged LesionTracker and OHIF Viewer, refactored out common components

This commit is contained in:
Erik Ziegler committed 2015-10-13 14:14:50 +02:00
1 parent 34adbe3fbc
commit b774c22593
144 files changed
+27820

No files matched your search

@@ -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,230 @@
/**
* This function enables stack prefetching for a specified element (viewport)
* It first disables any prefetching currently occurring on any other viewports.
* Prefetching will not be enabled if the series is a clip.
*
* @param element
*/
function enablePrefetchOnElement() {
var viewportIndex = Session.get('ActiveViewport');
var element = $('.imageViewerViewport').get(viewportIndex);
console.log('Enabling prefetch on new element');
// Loop through all viewports and disable stackPrefetch
$('.imageViewerViewport').each(function() {
if (!$(this).find('canvas').length) {
return;
}
cornerstoneTools.stackPrefetch.disable(this);
});
// Make sure there is a stack to fetch
var stack = cornerstoneTools.getToolState(element, 'stack');
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
cornerstoneTools.stackPrefetch.enable(element);
}
}
function displayReferenceLines() {
console.log("Changing reference line display");
var viewportIndex = Session.get('ActiveViewport');
var element = $('.imageViewerViewport').get(viewportIndex);
$('.imageViewerViewport').each(function(index, element) {
if (!$(this).find('canvas').length) {
return;
}
if (index === viewportIndex) {
cornerstoneTools.referenceLines.tool.disable(element);
return;
}
cornerstoneTools.referenceLines.tool.enable(element, OHIF.viewer.updateImageSynchronizer);
});
}
function loadSeriesIntoViewport(data) {
if (!data.series || !data.viewport) {
return;
}
var study = data.study;
var series = data.series;
var element = data.viewport;
var viewportIndex = $(".imageViewerViewport").index(data.viewport);
var allEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
'CornerstoneToolsDragStartActive CornerstoneToolsMultiTouchDragStart';
var imageIds = [];
var numImages = series.instances.length;
series.instances.forEach(function(instance, imageIndex) {
var imageId = getImageId(instance);
imageIds.push(imageId);
var data = {
instance: instance,
series: series,
study: study,
numImages: numImages,
imageIndex: imageIndex + 1
};
addMetaData(imageId, data);
});
var stack = {
currentImageIdIndex: 0,
imageIds: imageIds
};
var imageId = imageIds[stack.currentImageIdIndex];
var templateData = Template.currentData();
templateData.imageId = imageId;
// NOTE: This uses the experimental WebGL renderer for Cornerstone!
cornerstone.enable(element, cornerstone.webGL.renderer.render);
// If you have problems, replace it with this line instead:
// cornerstone.enable(element);
cornerstone.loadAndCacheImage(imageId).then(function(image) {
cornerstone.displayImage(element, image);
element.classList.remove('empty');
$(element).siblings('.viewportInstructions').hide();
$(element).siblings('.imageViewerViewportOverlay').show();
cornerstone.resize(element, true);
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
// Enable orientation markers, if applicable
var viewport = cornerstone.getViewport(element);
updateOrientationMarkers(element, viewport);
// Clear any old stack data
cornerstoneTools.clearToolState(element, 'stack');
cornerstoneTools.addToolState(element, 'stack', stack);
// Enable mouse input
cornerstoneTools.mouseInput.enable(element);
cornerstoneTools.touchInput.enable(element);
cornerstoneTools.mouseWheelInput.enable(element);
var activeTool = toolManager.getActiveTool();
toolManager.setActiveTool(activeTool);
cornerstoneTools.magnify.enable(element);
function onImageRendered(e, eventData) {
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
}
$(element).off('CornerstoneImageRendered', onImageRendered);
$(element).on('CornerstoneImageRendered', onImageRendered);
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
function onNewImage(e, eventData) {
// 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());
}
$(element).off('CornerstoneNewImage', onNewImage);
$(element).on('CornerstoneNewImage', onNewImage);
function sendActivationTrigger(e, eventData) {
var activeViewportIndex = Session.get('ActiveViewport');
var viewportIndex = $(".imageViewerViewport").index(eventData.element);
if (viewportIndex === activeViewportIndex) {
return;
}
eventData.viewportIndex = viewportIndex;
var customEvent = jQuery.Event('ActivateViewport', eventData);
customEvent.type = 'ActivateViewport'; // Need to overwrite the type set in the touch tools
$(e.target).trigger(customEvent, eventData);
}
$(element).off(allEvents, sendActivationTrigger);
$(element).on(allEvents, sendActivationTrigger);
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
OHIF.viewer.updateImageSynchronizer.add(element);
}
enablePrefetchOnElement();
displayReferenceLines();
});
}
Template.imageViewerViewport.onRendered(function() {
var studies = Session.get('studies');
var viewport = this.find(".imageViewerViewport");
var viewportIndex = $(".imageViewerViewport").index(viewport);
this.data.viewportIndex = viewportIndex;
var data = {
viewport: viewport
};
if (this.data.seriesInstanceUid !== undefined && this.data.studyInstanceUid !== undefined) {
var studyInstanceUid = this.data.studyInstanceUid;
var seriesInstanceUid = this.data.seriesInstanceUid;
studies.every(function(study) {
if (study.studyInstanceUid === studyInstanceUid) {
data.study = study;
study.seriesList.every(function(series) {
if (series.seriesInstanceUid === seriesInstanceUid) {
data.series = series;
return false;
}
return true;
});
return false;
}
return true;
});
} else {
var stacks = [];
studies.forEach(function(study) {
study.seriesList.forEach(function(series) {
var stack = {
series: series,
study: study
};
stacks.push(stack);
});
});
if (viewportIndex >= stacks.length) {
viewport.classList.add('empty');
$(viewport).siblings('.viewportInstructions').show();
return;
}
data.series = stacks[viewportIndex].series;
data.study = stacks[viewportIndex].study;
}
loadSeriesIntoViewport(data);
});
Template.imageViewerViewport.onDestroyed(function() {
var element = this.find(".imageViewerViewport");
cornerstone.disable(element);
});
Template.imageViewerViewport.events({
'ActivateViewport .imageViewerViewport': function(e) {
console.log('ActivateViewport: ' + e.viewportIndex);
Session.set('ActiveViewport', e.viewportIndex);
enablePrefetchOnElement();
displayReferenceLines();
},
});
@@ -0,0 +1,23 @@
.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
@@ -0,0 +1,11 @@
<template name="imageViewerViewports">
<div id='imageViewerViewports'>
{{ #each viewportArray }}
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
<div class="removable">
{{ > imageViewerViewport}}
</div>
</div>
{{ /each }}
</div>
</template>
@@ -0,0 +1,30 @@
function getNumViewports() {
var viewportRows = Session.get('viewportRows');
var viewportColumns = Session.get('viewportColumns');
return viewportRows * viewportColumns;
}
Template.imageViewerViewports.helpers({
height: function() {
var viewportRows = Session.get('viewportRows');
return 100 / viewportRows;
},
width: function() {
var viewportColumns = Session.get('viewportColumns');
return 100 / viewportColumns;
},
viewportArray: function() {
// This is a really annoying thing to have to do, but Meteor
// doesn't want to let me use another type of helper.
var numViewports = getNumViewports();
var array = [];
for (var i=0; i < numViewports; ++i) {
var data = {
viewportIndex: i
};
array.push(data);
}
return array;
}
});
@@ -0,0 +1,12 @@
#imageViewerViewports
height: calc(100% - 30px)
width: 100%
.viewportContainer
float: left
border: 2px solid #777
.removable
width: 100%
height: 100%
position: relative // Necessary so that the viewportOverlay is on top of the viewports
@@ -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,41 @@
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 container = $(".viewerMain").get(0);
UI.render(Template.imageViewerViewports, container);
var currentCell = $(evt.currentTarget);
var rowIndex = currentCell.closest('tr').index();
var columnIndex = currentCell.index();
// Add 1 because the indices start from zero
Session.set('viewportRows', rowIndex + 1);
Session.set('viewportColumns', columnIndex + 1);
}
});
@@ -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...</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,61 @@
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);
}
};
@@ -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,8 @@
<template name="viewportOrientationMarkers">
<div class="viewportOrientationMarkers noselect">
<div class="topMid orientationMarker">
</div>
<div class="leftMid orientationMarker">
</div>
</div>
</template>
@@ -0,0 +1,49 @@
updateOrientationMarkers = function(element, viewport) {
// Updates the orientation labels on the viewport
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>{{patientName}}</div>
<div>{{patientId}}</div>
<div></div>
</div>
<div class="topright dicomTag">
<div>{{studyDescription}}</div>
<div>{{studyDate}} {{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,155 @@
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, 'date');
},
studyTime : function() {
return getStudy.call(this, 'time');
},
studyDescription : function() {
return getStudy.call(this, 'description');
},
seriesDescription : function() {
return getSeries.call(this, 'description');
},
seriesNumber : function() {
return getSeries.call(this, 'number');
},
imageNumber : function() {
return getInstance.call(this, 'number');
},
imageIndex : function() {
return getInstance.call(this, 'index');
},
numImages : function() {
return getSeries.call(this, 'numImages');
}
});
@@ -0,0 +1,27 @@
.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