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,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
@@ -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
@@ -0,0 +1,5 @@
/*AccountsTemplates.configure({
forbidClientAccountCreation: true // do not allow account creation
});
*/
+18
View File
@@ -0,0 +1,18 @@
createStacks = function(study) {
var stacks = [];
// TODO: Split by multi-frame, modality, image size, etc
study.seriesList.forEach(function(series) {
if (!series.instances) {
return;
}
// Don't display thumbnails for non-image modalities
// All imaging modalities must have a valid value for rows (or columns)
var anInstance = series.instances[0];
if (!anInstance || !anInstance.rows) {
return;
}
stacks.push(series);
});
return stacks;
};
@@ -0,0 +1,17 @@
/**
* Converts the properties to URL query parameters. Based on:
* http://stackoverflow.com/questions/111529/create-query-parameters-in-javascript
*
* @param data
* @returns {string}
*/
encodeQueryData = function(data)
{
var ret = [];
for (var d in data) {
if(data[d]) {
ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
}
}
return ret.join("&");
};
@@ -0,0 +1,39 @@
function checkToken(token, data, dataOffset) {
if(dataOffset + token.length > data.length) {
//console.log('dataOffset >> ', dataOffset);
return false;
}
var endIndex = dataOffset;
for(var i = 0; i < token.length; i++) {
if(token[i] !== data[endIndex++]) {
if(endIndex > 520000) {
console.log('token=',uint8ArrayToString(token));
console.log('data=', uint8ArrayToString(data, dataOffset, endIndex-dataOffset));
console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(data[endIndex]), endIndex);
console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(token[endIndex]), endIndex);
}
return false;
}
}
return true;
}
findIndexOfString = function(data, str, offset) {
offset = offset || 0;
var token = stringToUint8Array(str);
for(var i=offset; i < data.length; i++) {
if(token[0] === data[i]) {
//console.log('match @', i);
if(checkToken(token, data, i)) {
return i;
}
}
}
return -1;
}
@@ -0,0 +1,5 @@
getActiveViewportElement = function() {
// TODO: Fix this, just hardcoded for now
var element = $('.imageViewerViewport').get(0);
return element;
};
+10
View File
@@ -0,0 +1,10 @@
getImageId = function(instance) {
if (!instance) {
return;
}
if (instance.wadouri) {
return 'dicomweb:' + instance.wadouri; // WADO-URI
} else {
return getWADORSImageId(instance); // WADO-RS Retrieve Frame
}
};
@@ -0,0 +1,45 @@
getWADORSImageId = function(instance) {
var columnPixelSpacing = 1.0;
var rowPixelSpacing = 1.0;
if (instance.pixelSpacing) {
var split = instance.pixelSpacing.split('\\');
rowPixelSpacing = parseFloat(split[0]);
columnPixelSpacing = parseFloat(split[1]);
}
var windowWidth;
var windowCenter;
if (instance.windowWidth && instance.windowCenter) {
windowWidth = parseFloat(instance.windowWidth.split('\\')[0]);
windowCenter = parseFloat(instance.windowCenter.split('\\')[0]);
}
var image = {
uri: instance.wadorsuri,
//imageId : '',
//minPixelValue : 0,
//maxPixelValue : 255,
slope: instance.rescaleSlope,
intercept: instance.rescaleIntercept,
windowCenter : windowCenter,
windowWidth : windowWidth,
//render: cornerstone.renderColorImage,
//getPixelData: getPixelData,
//getImageData: getImageData,
//getCanvas: getCanvas,
rows: instance.rows,
columns: instance.columns,
height: instance.rows,
width: instance.columns,
color: false,
columnPixelSpacing: columnPixelSpacing,
rowPixelSpacing: rowPixelSpacing,
invert: false,
sizeInBytes: instance.rows * instance.columns * (instance.bitsAllocated / 8),
instance: instance
};
var imageId = cornerstoneWADORSImageLoader.addImage(image);
return imageId;
};
@@ -0,0 +1,16 @@
UI.registerHelper('formatDA', function (context, format, options) {
if (!context) {
return undefined;
}
/*var date = DICOMUtilities.toDate(context, '', false);
if(!date) {
return context;
}*/
var date = context;
var dateAsMoment = moment(date);
var strFormat = "MMM D, YYYY";
if (options) {
strFormat = format;
}
return dateAsMoment.format(strFormat);
});
@@ -0,0 +1,5 @@
UI.registerHelper('formatNumberPrecision', function(context, precision, options) {
if(context) {
return parseFloat(context).toFixed(precision);
}
});
@@ -0,0 +1,20 @@
UI.registerHelper('formatDA', function (context, format, options) {
if (!context) {
return undefined;
}
var pn = context;
var str = '';
if(pn.prefix) {
str = pn.prefix + ' ';
}
str += (pn.familyName || '') + ', ';
str += (pn.givenName || '');
if(pn.middleName) {
str += ' ' + pn.middleName;
}
if(pn.suffix) {
str += ' ' + pn.suffix;
}
return str;
});
@@ -0,0 +1,13 @@
UI.registerHelper('formatTM', function(context, format, options) {
if (!context) {
return undefined;
}
var date = context;
var dateAsMoment = moment(date);
var strFormat = "HH:mm:ss";
if (options) {
strFormat = format;
}
return dateAsMoment.format(strFormat);
});
+5
View File
@@ -0,0 +1,5 @@
isTouchDevice = function() {
return (('ontouchstart' in window) ||
(navigator.MaxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0));
};
@@ -0,0 +1,82 @@
var metaDataLookup = {};
addMetaData = function(imageId, data) {
var instanceMetaData = data.instance;
var seriesMetaData = data.series;
var studyMetaData = data.study;
var imageIndex = data.imageIndex;
var numImages = data.numImages;
var metaData = {};
metaData.study = {
date: instanceMetaData.studyDate,
time: instanceMetaData.studyTime,
description: instanceMetaData.studyDescription
};
metaData.series = {
description: seriesMetaData.seriesDescription,
number: seriesMetaData.seriesNumber,
modality: seriesMetaData.modality,
instanceUid: seriesMetaData.instanceUid,
numImages: numImages
};
metaData.instance = {
number: instanceMetaData.instanceNumber,
index: imageIndex
};
metaData.patient = {
name: instanceMetaData.patientName,
id: instanceMetaData.patientId
};
// If there is sufficient information, populate
// the imagePlane object for easier use in the Viewer
if (instanceMetaData.frameOfReferenceUID &&
instanceMetaData.imageOrientationPatient &&
instanceMetaData.imagePositionPatient) {
var imageOrientation = instanceMetaData.imageOrientationPatient.split('\\');
var imagePosition = instanceMetaData.imagePositionPatient.split('\\');
var columnPixelSpacing = 1.0;
var rowPixelSpacing = 1.0;
if (instanceMetaData.pixelSpacing) {
var split = instanceMetaData.pixelSpacing.split('\\');
rowPixelSpacing = parseFloat(split[0]);
columnPixelSpacing = parseFloat(split[1]);
}
metaData.imagePlane = {
frameOfReferenceUID: instanceMetaData.frameOfReferenceUID,
rows: instanceMetaData.rows,
columns: instanceMetaData.columns,
rowCosines: new cornerstoneMath.Vector3(parseFloat(imageOrientation[0]), parseFloat(imageOrientation[1]), parseFloat(imageOrientation[2])),
columnCosines: new cornerstoneMath.Vector3(parseFloat(imageOrientation[3]), parseFloat(imageOrientation[4]), parseFloat(imageOrientation[5])),
imagePositionPatient: new cornerstoneMath.Vector3(parseFloat(imagePosition[0]), parseFloat(imagePosition[1]), parseFloat(imagePosition[2])),
rowPixelSpacing: rowPixelSpacing,
columnPixelSpacing: columnPixelSpacing,
};
}
// Add the metaData to the imageId lookup object
metaDataLookup[imageId] = metaData;
};
function provider(type, imageId) {
var imageMetaData = metaDataLookup[imageId];
if (!imageMetaData) {
return;
}
if (imageMetaData.hasOwnProperty(type)) {
return imageMetaData[type];
}
}
Meteor.startup(function() {
cornerstoneTools.metaData.addProvider(provider);
});
@@ -0,0 +1,42 @@
/**
* This function destroys and rerenders the imageViewerViewport template.
* It uses the data provided (object containing seriesIndex and studyIndex) to
* load a new series into the produced viewport.
*
* @param element
* @param data An object containing a seriesIndex and studyIndex for a study to load into this viewport
*/
rerenderViewportWithNewSeries = function(element, data) {
var container = $(element).parents('.viewportContainer').get(0);
var viewportIndex = $(container).index();
data.viewportIndex = viewportIndex;
// Update the dictionary of loaded series for the specified viewport
OHIF.viewer.imageViewerLoadedSeriesDictionary[viewportIndex] = {
seriesInstanceUid: data.seriesInstanceUid,
studyInstanceUid: data.studyInstanceUid
};
// Remove the hover styling
$(element).find('canvas').not('.magnifyTool').removeClass("faded");
// Reset the current image index for this viewport
delete OHIF.viewer.imageViewerCurrentImageIdIndexDictionary[viewportIndex];
// Remove the whole template, add in the new one
var viewportContainer = $(element).parents('.removable');
var newViewportContainer = document.createElement('div');
newViewportContainer.className = 'removable';
// Remove the parent element of the template
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
viewportContainer.remove();
container.appendChild(newViewportContainer);
// Render and insert the template
UI.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
Session.set('loadedSeriesDictionary', OHIF.viewer.imageViewerLoadedSeriesDictionary);
};
+15
View File
@@ -0,0 +1,15 @@
sortStudy = function(study) {
if (!study) {
return;
}
study.seriesList.sort(function(a, b) {
return a.seriesNumber - b.seriesNumber;
});
study.seriesList.forEach(function(series){
series.instances.sort(function(a, b) {
return a.instanceNumber - b.instanceNumber;
});
});
};
@@ -0,0 +1,7 @@
stringToUint8Array = function(str) {
var uint=new Uint8Array(str.length);
for(var i=0,j=str.length;i<j;i++){
uint[i]=str.charCodeAt(i);
}
return uint;
};
+150
View File
@@ -0,0 +1,150 @@
var activeTool;
var tools = {};
var initialized = false;
function configureTools() {
// Set the configuration for the multitouch pan tool
var multiTouchPanConfig = {
testPointers: function(eventData) {
return (eventData.numPointers >= 3);
}
};
cornerstoneTools.panMultiTouch.setConfiguration(multiTouchPanConfig);
}
toolManager = {
init: function() {
toolManager.addTool('wwwc', {
mouse: cornerstoneTools.wwwc,
touch: cornerstoneTools.wwwcTouchDrag
});
toolManager.addTool('zoom', {
mouse: cornerstoneTools.zoom,
touch: cornerstoneTools.zoomTouchDrag
});
toolManager.addTool('wwwcRegion', {
mouse: cornerstoneTools.wwwcRegion,
touch: cornerstoneTools.wwwcRegionTouch
});
toolManager.addTool('dragProbe', {
mouse: cornerstoneTools.dragProbe,
touch: cornerstoneTools.dragProbeTouch
});
toolManager.addTool('pan', {
mouse: cornerstoneTools.pan,
touch: cornerstoneTools.panTouchDrag
});
toolManager.addTool('stackScroll', {
mouse: cornerstoneTools.stackScroll,
touch: cornerstoneTools.stackScrollTouchDrag
});
toolManager.addTool('length', {
mouse: cornerstoneTools.length,
touch: cornerstoneTools.lengthTouch
});
toolManager.addTool('angle', {
mouse: cornerstoneTools.simpleAngle,
touch: cornerstoneTools.simpleAngleTouch
});
toolManager.addTool('magnify', {
mouse: cornerstoneTools.magnify,
touch: cornerstoneTools.magnifyTouchDrag
});
toolManager.addTool('ellipticalRoi', {
mouse: cornerstoneTools.ellipticalRoi,
touch: cornerstoneTools.ellipticalRoiTouch
});
toolManager.addTool('rectangleRoi', {
mouse: cornerstoneTools.rectangleRoi,
touch: cornerstoneTools.rectangleRoiTouch
});
toolManager.addTool('annotate', {
mouse: cornerstoneTools.arrowAnnotate,
touch: cornerstoneTools.arrowAnnotateTouch
});
activeTool = OHIF.viewer.defaultTool;
configureTools();
initialized = true;
},
addTool: function(name, base) {
tools[name] = base;
},
setActiveTool: function(tool) {
if (!initialized) {
toolManager.init();
}
$('#toolRow .btn-group button').removeClass('active');
var toolButton = document.getElementById(tool);
toolButton.classList.add('active');
$('.imageViewerViewport').each(function(index, element) {
var canvases = $(element).find('canvas');
if (!element.classList.contains('empty') && canvases.length > 0) {
// First, deactivate the current active tool
tools[activeTool].mouse.deactivate(element, 1);
tools[activeTool].touch.deactivate(element);
// First, get the stack toolData
var toolData = cornerstoneTools.getToolState(element, 'stack');
if (toolData === undefined || toolData.data === undefined || toolData.data.length === 0) {
return;
}
// Get the imageIds for this element
var imageIds = toolData.data[0].imageIds;
// Deactivate all the middle mouse, right click, and scroll wheel tools
cornerstoneTools.pan.deactivate(element);
cornerstoneTools.zoom.deactivate(element);
cornerstoneTools.zoomWheel.deactivate(element);
cornerstoneTools.stackScrollWheel.deactivate(element);
// Reactivate the relevant scrollwheel tool for this element
if (imageIds.length > 1) {
// scroll is the default tool for middle mouse wheel for stacks
cornerstoneTools.stackScrollWheel.activate(element);
} else {
// zoom is the default tool for middle mouse wheel for single images (non stacks)
cornerstoneTools.zoomWheel.activate(element);
}
// This block ensures that the middle mouse and scroll tools keep working
if (tool === 'pan') {
cornerstoneTools.pan.activate(element, 3); // 3 means left mouse button and middle mouse button
cornerstoneTools.zoom.activate(element, 4); // zoom is the default tool for right mouse button
} else if (tool === 'zoom') {
cornerstoneTools.pan.activate(element, 2); // pan is the default tool for middle mouse button
cornerstoneTools.zoom.activate(element, 5); // 5 means left mouse button and right mouse button
} else {
// Reactivate the middle mouse and right click tools
cornerstoneTools.pan.activate(element, 2); // pan is the default tool for middle mouse button
cornerstoneTools.zoom.activate(element, 4); // zoom is the default tool for right mouse button
// Activate the chosen tool
tools[tool].mouse.activate(element, 1);
}
tools[tool].touch.activate(element);
cornerstoneTools.zoomTouchPinch.activate(element);
cornerstoneTools.panMultiTouch.activate(element);
}
});
activeTool = tool;
},
getActiveTool: function() {
if (!activeTool) {
activeTool = OHIF.viewer.defaultTool;
}
return activeTool;
},
setDefaultTool: function(tool) {
OHIF.viewer.defaultTool = tool;
},
getDefaultTool: function() {
return OHIF.viewer.defaultTool;
}
};
@@ -0,0 +1,9 @@
uint8ArrayToString = function(data, offset, length) {
offset = offset || 0;
length = length || data.length - offset;
var str = "";
for(var i=offset; i < offset + length; i++) {
str += String.fromCharCode(data[i]);
}
return str;
};
@@ -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);
}
};
+115
View File
@@ -0,0 +1,115 @@
Package.describe({
name: "viewerbase",
summary: "Shared components and functions for Meteor DICOM Viewers",
version: '0.0.1'
});
Package.onUse(function (api) {
api.versionsFrom('1.2.0.2');
api.use('standard-app-packages');
api.use('jquery');
api.use('stylus');
// Our custom package
api.use('cornerstone');
// ---------- Components ----------
// Basic components
api.addFiles('components/basic/layout/layout.html', 'client');
api.addFiles('components/basic/layout/layout.styl', 'client');
api.addFiles('components/basic/login/login.html', 'client');
api.addFiles('components/basic/notFound/notFound.html', 'client');
// Study Browser components
api.addFiles('components/studyBrowser/studyBrowser/studyBrowser.html', 'client');
api.addFiles('components/studyBrowser/studyBrowser/studyBrowser.js', 'client');
api.addFiles('components/studyBrowser/studyBrowser/studyBrowser.styl', 'client');
api.addFiles('components/studyBrowser/thumbnailEntry/thumbnailEntry.html', 'client');
api.addFiles('components/studyBrowser/thumbnailEntry/thumbnailEntry.js', 'client');
api.addFiles('components/studyBrowser/thumbnailEntry/thumbnailEntry.styl', 'client');
api.addFiles('components/studyBrowser/thumbnails/thumbnails.html', 'client');
api.addFiles('components/studyBrowser/thumbnails/thumbnails.js', 'client');
api.addFiles('components/studyBrowser/imageThumbnail/imageThumbnail.html', 'client');
api.addFiles('components/studyBrowser/imageThumbnail/imageThumbnail.js', 'client');
api.addFiles('components/studyBrowser/imageThumbnail/imageThumbnail.styl', 'client');
// Viewer components
api.addFiles('components/viewer/imageViewerViewport/imageViewerViewport.html', 'client');
api.addFiles('components/viewer/imageViewerViewport/imageViewerViewport.js', 'client');
api.addFiles('components/viewer/imageViewerViewport/imageViewerViewport.styl', 'client');
api.addFiles('components/viewer/imageViewerViewports/imageViewerViewports.html', 'client');
api.addFiles('components/viewer/imageViewerViewports/imageViewerViewports.js', 'client');
api.addFiles('components/viewer/imageViewerViewports/imageViewerViewports.styl', 'client');
api.addFiles('components/viewer/loadingIndicator/loadingIndicator.html', 'client');
api.addFiles('components/viewer/loadingIndicator/loadingIndicator.js', 'client');
api.addFiles('components/viewer/loadingIndicator/loadingIndicator.styl', 'client');
api.addFiles('components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.html', 'client');
api.addFiles('components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.js', 'client');
api.addFiles('components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.styl', 'client');
api.addFiles('components/viewer/viewportOverlay/viewportOverlay.html', 'client');
api.addFiles('components/viewer/viewportOverlay/viewportOverlay.js', 'client');
api.addFiles('components/viewer/viewportOverlay/viewportOverlay.styl', 'client');
api.addFiles('components/viewer/layoutChooser/layoutChooser.html', 'client');
api.addFiles('components/viewer/layoutChooser/layoutChooser.js', 'client');
api.addFiles('components/viewer/layoutChooser/layoutChooser.styl', 'client');
// Library functions
api.addFiles('lib/accountsConfig.js', 'client');
api.addFiles('lib/createStacks.js', 'client');
api.addFiles('lib/encodeQueryData.js', ['client', 'server']);
api.addFiles('lib/findIndexOfString.js', 'client');
api.addFiles('lib/getActiveViewportElement.js', 'client');
api.addFiles('lib/getImageId.js', 'client');
api.addFiles('lib/getWADORSImageId.js', 'client');
api.addFiles('lib/isTouchDevice.js', 'client');
api.addFiles('lib/metaDataProvider.js', 'client');
api.addFiles('lib/rerenderViewportWithNewSeries.js', 'client');
api.addFiles('lib/sortStudy.js', 'client');
api.addFiles('lib/stringToUint8Array.js', 'client');
api.addFiles('lib/toolManager.js', 'client');
api.addFiles('lib/uint8ArrayToString.js', 'client');
api.addFiles('lib/updateOrientationMarkers.js', 'client');
//api.export('accountsConfig', 'client');
api.export('createStacks', 'client');
api.export('encodeQueryData', ['client', 'server']);
api.export('findIndexOfString', 'client');
api.export('getActiveViewportElement', 'client');
api.export('getImageId', 'client');
api.export('getWADORSImageId', 'client');
api.export('isTouchDevice', 'client');
api.export('metaDataProvider', 'client');
api.export('rerenderViewportWithNewSeries', 'client');
api.export('sortStudy', 'client');
api.export('stringToUint8Array', 'client');
api.export('toolManager', 'client');
api.export('uint8ArrayToString', 'client');
api.export('updateOrientationMarkers', 'client');
// UI Helpers
api.addFiles('lib/helpers/formatDA.js', 'client');
api.addFiles('lib/helpers/formatNumberPrecision.js', 'client');
api.addFiles('lib/helpers/formatPN.js', 'client');
api.addFiles('lib/helpers/formatTM.js', 'client');
// Server-side functions
api.addFiles('server/seed.js', 'server');
api.addFiles('server/lib/namespace.js', 'server');
api.addFiles('server/methods/getStudyMetadata.js', 'server');
api.addFiles('server/methods/worklistSearch.js', 'server');
api.addFiles('server/services/qido/instances.js', 'server');
api.addFiles('server/services/qido/studies.js', 'server');
api.addFiles('server/services/wado/retrieveMetadata.js', 'server');
api.export('Services', 'server');
});
@@ -0,0 +1,3 @@
Services = {};
Services.QIDO = {};
Services.WADO = {};
@@ -0,0 +1,10 @@
Meteor.methods({
'GetStudyMetadata' : function(studyInstanceUid) {
console.log('GetStudyMetadata(%s)', studyInstanceUid);
var server = Meteor.settings.dicomWeb.endpoints[0];
//return Services.QIDO.Instances(server, studyInstanceUid);
return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
}
});
@@ -0,0 +1,6 @@
Meteor.methods({
'WorklistSearch' : function(filter) {
var server = Meteor.settings.dicomWeb.endpoints[0];
return Services.QIDO.Studies(server, filter);
}
});
+18
View File
@@ -0,0 +1,18 @@
var users = [
{
email: 'admin@ohif.org',
//username: "admin",
password: "admin",
profile: {
}
}
];
Meteor.startup(function() {
users.forEach(function(user) {
if(!Meteor.users.findOne({username: user.username})) {
Accounts.createUser(user);
}
});
});
@@ -0,0 +1,59 @@
function buildUrl(server, studyInstanceUid) {
var url = server.qidoRoot + '/studies/' + studyInstanceUid + '/instances';
return url;
}
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
var seriesMap = {};
var seriesList = [];
resultData.forEach(function(instance) {
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
var series = seriesMap[seriesInstanceUid];
if(!series) {
series = {
seriesInstanceUid : seriesInstanceUid,
seriesNumber : DICOMWeb.getString(instance['00200011']),
instances: []
};
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
// The uri for the dicomweb
// NOTE: DCM4CHEE seems to return the data zipped
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
// know how to parse yet
//var uri = DICOMWeb.getString(instance['00081190']);
//uri = uri.replace('wado-rs', 'dicom-web');
// manually create a WADO-URI from the UID's
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
series.instances.push({
sopClassUid: DICOMWeb.getString(instance['00080016']),
sopInstanceUid: sopInstanceUid,
uri: uri,
instanceNumber: DICOMWeb.getString(instance['00200013'])
});
});
return seriesList;
}
Services.QIDO.Instances = function(server, studyInstanceUid) {
var url = buildUrl(server, studyInstanceUid);
var result = DICOMWeb.getJSON(url, server.requestOptions);
var study = {
wadoUriRoot: server.wadoUriRoot,
studyInstanceUid: studyInstanceUid,
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
};
return study;
};
@@ -0,0 +1,38 @@
function filterToQIDOURL(server, filter) {
var parameters = {
PatientName : filter.patientName,
PatientID: filter.patientId,
AccessionNumber : filter.accessionNumber,
limit : filter.limit || 20,
includefield : server.qidoSupportsIncludeField ? 'all' : undefined
};
var url = server.qidoRoot + '/studies?' + encodeQueryData(parameters);
return url;
}
function resultDataToStudies(resultData) {
var studies = [];
resultData.forEach(function(study) {
studies.push({
patientName: DICOMWeb.getName(study['00100010']),
patientId: DICOMWeb.getString(study['00100020']),
accessionNumber : DICOMWeb.getString(study['00080050']),
studyDate: DICOMWeb.getString(study['00080020']),
modalities: DICOMWeb.getString(study['00080061']),
studyDescription: DICOMWeb.getString(study['00081030']),
imageCount: DICOMWeb.getString(study['00201208']),
studyInstanceUid: DICOMWeb.getString(study['0020000D'])
//url : server.wadoRoot + '/studies/' + DICOMWeb.getString(study['0020000D']) + '/metadata'
});
});
return studies;
}
Services.QIDO.Studies = function(server, filter) {
var url = filterToQIDOURL(server, filter);
var result = DICOMWeb.getJSON(url, server.requestOptions);
return resultDataToStudies(result.data);
};
@@ -0,0 +1,88 @@
function buildUrl(server, studyInstanceUid) {
var url = server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
return url;
}
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
var seriesMap = {};
var seriesList = [];
resultData.forEach(function(instance) {
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
var series = seriesMap[seriesInstanceUid];
if(!series) {
series = {
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesInstanceUid : seriesInstanceUid,
seriesNumber : DICOMWeb.getNumber(instance['00200011']),
instances: []
};
seriesMap[seriesInstanceUid] = series;
seriesList.push(series);
}
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
var instanceSummary = {
// -----------
// TODO = Fix this
// This doesn't seem like the best place to put this, but otherwise we have no study info
patientName: DICOMWeb.getName(instance['00100010']),
patientId: DICOMWeb.getString(instance['00100020']),
accessionNumber : DICOMWeb.getString(instance['00080050']),
studyDate: DICOMWeb.getString(instance['00080020']),
modalities: DICOMWeb.getString(instance['00080061']),
studyDescription: DICOMWeb.getString(instance['00081030']),
imageCount: DICOMWeb.getString(instance['00201208']),
studyInstanceUid: DICOMWeb.getString(instance['0020000D']),
// -----------
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
sopInstanceUid: sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
};
if(server.imageRendering === 'wadouri') {
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
} else {
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
}
series.instances.push(instanceSummary);
});
return seriesList;
}
Services.WADO.RetrieveMetadata = function(server, studyInstanceUid) {
var url = buildUrl(server, studyInstanceUid);
var result = DICOMWeb.getJSON(url, server.requestOptions);
var study = {
wadoUriRoot: server.wadoUriRoot,
studyInstanceUid: studyInstanceUid,
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
};
return study;
};