Merged LesionTracker and OHIF Viewer, refactored out common components
This commit is contained in:
1 parent
34adbe3fbc
commit
b774c22593
144 files changed
+27820
No files matched your search
@@ -0,0 +1,31 @@
|
||||
<template name="layout">
|
||||
<nav class="navbar navbar-default" 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="/">OHIF Viewer</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,18 @@
|
||||
.navbar
|
||||
margin-bottom: 0
|
||||
border-radius: 0px
|
||||
|
||||
.navbar-form
|
||||
margin: 0
|
||||
padding: 15px 3px
|
||||
|
||||
.navbar-default
|
||||
-webkit-transition: background-color 200ms linear
|
||||
-moz-transition: background-color 200ms linear
|
||||
-o-transition: background-color 200ms linear
|
||||
-ms-transition: background-color 200ms linear
|
||||
transition: background-color 200ms linear
|
||||
|
||||
@media (min-width: 768px)
|
||||
.navbar
|
||||
border-radius: 0px
|
||||
@@ -0,0 +1,3 @@
|
||||
<template name="login">
|
||||
{{> atForm}}
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template name="notFound">
|
||||
Not found
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
<template name="imageThumbnail">
|
||||
<div class="imageThumbnail">
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
Template.imageThumbnail.onRendered(function() {
|
||||
var instance = this.data.instances[0];
|
||||
var element = this.find('.imageThumbnail');
|
||||
cornerstone.enable(element);
|
||||
|
||||
var imageId = getImageId(instance);
|
||||
|
||||
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||
cornerstone.displayImage(element, image);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
.imageThumbnail
|
||||
width: 90px
|
||||
height: 90px
|
||||
background-color: black
|
||||
margin: 0 auto
|
||||
@@ -0,0 +1,7 @@
|
||||
<template name="studyBrowser">
|
||||
<div class="studyBrowser">
|
||||
{{#each studies}}
|
||||
{{> thumbnails}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,6 @@
|
||||
Template.studyBrowser.helpers({
|
||||
studies : function() {
|
||||
var studies = Session.get('studies');
|
||||
return studies;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
.studyBrowser
|
||||
float: left
|
||||
height: 100%
|
||||
width: 120px
|
||||
overflow: auto
|
||||
background-color: black
|
||||
@@ -0,0 +1,6 @@
|
||||
<template name="thumbnailEntry">
|
||||
<div class="thumbnailEntry">
|
||||
{{ > imageThumbnail }}
|
||||
<div class='seriesDescription noselect'>{{seriesDescription}}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,221 @@
|
||||
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) {
|
||||
// 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 = {
|
||||
studyInstanceUid: imageThumbnail.data('studyInstanceUid'),
|
||||
seriesInstanceUid: imageThumbnail.data('seriesInstanceUid')
|
||||
};
|
||||
|
||||
// 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);
|
||||
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) {
|
||||
thumbnailDragStartHandler(e);
|
||||
},
|
||||
'touchmove .thumbnailEntry': function(e) {
|
||||
thumbnailDragHandler(e, e.currentTarget);
|
||||
},
|
||||
'touchend .thumbnailEntry': function(e){
|
||||
thumbnailDragEndHandler(e, e.currentTarget);
|
||||
}
|
||||
});
|
||||
|
||||
Template.thumbnailEntry.helpers({
|
||||
seriesDescription : function() {
|
||||
if (!this.seriesDescription) {
|
||||
return '';
|
||||
}
|
||||
return this.seriesDescription;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
.thumbnailEntry
|
||||
width: 105px
|
||||
padding: 5px
|
||||
background-color: black
|
||||
border: 1px solid #424242
|
||||
margin: 0 auto
|
||||
|
||||
.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,6 @@
|
||||
Template.thumbnails.helpers({
|
||||
thumbnails: function() {
|
||||
var stacks = createStacks(this);
|
||||
return stacks;
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<template name="viewportOrientationMarkers">
|
||||
<div class="viewportOrientationMarkers noselect">
|
||||
<div class="topMid orientationMarker">
|
||||
</div>
|
||||
<div class="leftMid orientationMarker">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+49
@@ -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);
|
||||
}
|
||||
};
|
||||
+18
@@ -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
|
||||
Reference in new issue
Block a user