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

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,77 @@
cornerstoneWADORSImageLoader = {
internal : {
nextIndex: 0,
imageIds : []
}
};
cornerstoneWADORSImageLoader.addImage = function(image) {
var index = cornerstoneWADORSImageLoader.internal.nextIndex++;
cornerstoneWADORSImageLoader.internal.imageIds[index] = image;
var imageId = 'wadors:' + index;
return imageId;
};
function getMinMax(storedPixelData)
{
// we always calculate the min max values since they are not always
// present in DICOM and we don't want to trust them anyway as cornerstone
// depends on us providing reliable values for these
var min = 65535;
var max = -32768;
var numPixels = storedPixelData.length;
var pixelData = storedPixelData;
for(var index = 0; index < numPixels; index++) {
var spv = pixelData[index];
// TODO: test to see if it is faster to use conditional here rather than calling min/max functions
min = Math.min(min, spv);
max = Math.max(max, spv);
}
return {
min: min,
max: max
};
}
cornerstone.registerImageLoader('wadors', function(imageId) {
var index = imageId.substring(7);
var image = cornerstoneWADORSImageLoader.internal.imageIds[index];
var deferred = $.Deferred();
var mediaType;// = 'image/dicom+jp2';
DICOMWeb.getImageFrame(image.uri, mediaType).then(function(result) {
//console.log(result);
// TODO: add support for retrieving compressed pixel data
var storedPixelData;
if(image.instance.bitsAllocated === 16) {
if(image.instance.pixelRepresentation === 0) {
storedPixelData = new Uint16Array(result.arrayBuffer, result.offset, result.length / 2);
} else {
storedPixelData = new Int16Array(result.arrayBuffer, result.offset, result.length / 2);
}
} else if(image.instance.bitsAllocated === 8) {
storedPixelData = new Uint8Array(result.arrayBuffer, result.offset, result.length);
}
// TODO: handle various color space conversions
var minMax = getMinMax(storedPixelData);
image.imageId = imageId;
image.minPixelValue = minMax.min;
image.maxPixelValue = minMax.max;
image.render = cornerstone.renderGrayscaleImage;
image.getPixelData = function() {
return storedPixelData;
};
//console.log(image);
deferred.resolve(image);
}).catch(function(reason) {
deferred.reject(reason);
});
return deferred;
});
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+27
View File
@@ -0,0 +1,27 @@
Package.describe({
name: "cornerstone",
summary: "Cornerstone Web-based Medical Imaging libraries",
version: '0.0.1'
});
Package.onUse(function (api) {
api.versionsFrom('1.2.0.2');
api.use('jquery');
api.addFiles('client/cornerstone.js', 'client', {bare: true});
api.addFiles('client/cornerstoneMath.js', 'client', {bare: true});
api.addFiles('client/cornerstoneTools.js', 'client', {bare: true});
api.addFiles('client/cornerstoneWADOImageLoader.js', 'client', {bare: true});
api.addFiles('client/cornerstoneWADORSImageLoader.js', 'client', {bare: true});
api.addFiles('client/dicomParser.js', 'client', {bare: true});
api.addFiles('client/hammer.js', 'client', {bare: true});
api.export("cornerstone", 'client');
api.export("cornerstoneMath", 'client');
api.export("cornerstoneTools", 'client');
api.export("cornerstoneWADOImageLoader", 'client');
api.export("cornerstoneWADORSImageLoader", 'client');
api.export("dicomParser", 'client');
});
+17
View File
@@ -0,0 +1,17 @@
Package.describe({
name: "dicomweb",
summary: "DICOM Web Helper Functions",
version: '0.0.1'
});
Package.onUse(function (api) {
api.addFiles('server/namespace.js', 'server');
api.addFiles('server/getImageFrame.js', 'server');
api.addFiles('server/getJSON.js', 'server');
api.addFiles('server/getName.js', 'server');
api.addFiles('server/getNumber.js', 'server');
api.addFiles('server/getString.js', 'server');
api.export("DICOMWeb", 'server');
});
+70
View File
@@ -0,0 +1,70 @@
function findBoundary(header) {
for(var i=0; i < header.length; i++) {
if(header[i].substr(0,2) === '--') {
return header[i];
}
}
return undefined;
}
function findContentType(header) {
for(var i=0; i < header.length; i++) {
if(header[i].substr(0,13) === 'Content-Type:') {
return header[i].substr(13).trim();
}
}
return undefined;
}
DICOMWeb.getImageFrame = function(uri, mediaType) {
mediaType = mediaType || 'application/octet-stream';
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.responseType = "arraybuffer";
xhr.open("get", uri, true);
xhr.setRequestHeader('Accept', 'multipart/related;type=' + mediaType);
xhr.onreadystatechange = function (oEvent) {
// TODO: consider sending out progress messages here as we receive the pixel data
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// request succeeded, Parse the multi-part mime response
var imageFrameAsArrayBuffer = xhr.response;
var response = new Uint8Array(xhr.response);
// First look for the multipart mime header
var tokenIndex = findIndexOfString(response, '\n\r\n');
if(tokenIndex === -1) {
reject('invalid response - no multipart mime header');
}
var header = uint8ArrayToString(response, 0, tokenIndex);
// Now find the boundary marker
var split = header.split('\r\n');
var boundary = findBoundary(split);
if(!boundary) {
reject('invalid response - no boundary marker');
}
var offset = tokenIndex + 4; // skip over the \n\r\n
// find the terminal boundary marker
var endIndex = findIndexOfString(response, boundary, offset);
if(endIndex === -1) {
reject('invalid response - terminating boundary not found');
}
// return the info for this pixel data
var length = endIndex - offset - 1;
resolve({
contentType: findContentType(split),
arrayBuffer: imageFrameAsArrayBuffer,
offset: offset,
length: length
});
}
else {
// request failed, reject the deferred
reject(xhr.response);
}
}
};
xhr.send();
});
};
+31
View File
@@ -0,0 +1,31 @@
DICOMWeb.getJSON = function(url, options) {
var getOptions = {
headers: {
'Accept' : 'application/json'
},
};
if (options.auth) {
getOptions.auth = options.auth;
}
if (options.logRequests) {
console.log(url);
}
if (options.logTiming) {
console.time(url);
}
var result = HTTP.get(url, getOptions);
if (options.logTiming) {
console.timeEnd(url);
}
if (options.logResponses) {
console.log(result.data);
}
return result;
};
+26
View File
@@ -0,0 +1,26 @@
/**
* Returns the Alphabetic version of a PN
*
* @param element - The group/element of the element (e.g. '00200013')
* @param [defaultValue] - The default value to return if the element is not found
* @returns {*}
*/
DICOMWeb.getName = function(element, defaultValue) {
if(!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if(!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if(!element.Value.length) {
return defaultValue;
}
// Return the Alphabetic component group
if(element.Value[0].Alphabetic) {
return element.Value[0].Alphabetic;
}
// Orthanc does not return PN properly so this is a temporary workaround
return element.Value[0];
};
+20
View File
@@ -0,0 +1,20 @@
/**
* Returns the first string value as a Javascript Number
* @param element - The group/element of the element (e.g. '00200013')
* @param [defaultValue] - The default value to return if the element does not exist
* @returns {*}
*/
DICOMWeb.getNumber = function(element, defaultValue) {
if(!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if(!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if(!element.Value.length) {
return defaultValue;
}
return parseFloat(element.Value[0]);
};
+23
View File
@@ -0,0 +1,23 @@
/**
* Returns the specified element as a string. Multi-valued elements will be separated by a backslash
*
* @param element - The group/element of the element (e.g. '00200013')
* @param [defaultValue] - The value to return if the element is not present
* @returns {*}
*/
DICOMWeb.getString = function(element, defaultValue) {
if(!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if(!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if(!element.Value.length) {
return defaultValue;
}
// Join the array together separated by backslash
// NOTE: Orthanc does not correctly split values into an array so the join is a no-op
return element.Value.join('\\');
};
+1
View File
@@ -0,0 +1 @@
DICOMWeb = {};
@@ -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;
};
@@ -0,0 +1,15 @@
<template name="worklist">
<div class="tabbable">
<ul id="tabs" class="nav nav-tabs" style="width:100%;">
<li class="active">
<a id="tabList" data-toggle="tab">Study List</a>
</li>
</ul>
<div id="tabs-content" class="tab-content">
<div id="contentList" class="tab-pane active">
{{> worklistSearch }}
{{> worklistResult }}
</div>
</div>
</div>
</template>
+110
View File
@@ -0,0 +1,110 @@
var contentID = "";
//Generate UUID to create unique tabs
function generateUUID () {
var d = new Date().getTime();
var uuid = 'xxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*8)%8 | 0;
d = Math.floor(d/8);
return (c=='x' ? r : (r&0x3|0x8)).toString(8);
});
return uuid;
}
//Remove tab
function removeTab (uuid) {
var removedTabIndex = $("#tab"+uuid).index();
//Remove tab
$("#tab"+uuid).remove();
//Remove Content
$("#content"+uuid).remove();
//TODO:Activate previous tab
}
//Add new tab
function addNewTab (data) {
//Make inactive all tabs
$("#tabs > li").removeClass("active");
$("#tabs-content > div").removeClass("active");
//Create new li element for tab
var tabli = document.createElement("li");
$(tabli).addClass("active");
$("#tabs").append(tabli);
//a element inside li
var taba = document.createElement("a");
var uuid = generateUUID();
var tabId = "tab"+uuid;
//activeTabId provides unique tabs content and encapsulates templates in tabs
Session.set("activeTabId", tabId);
console.log(Session.get("activeTabId"));
//Create tab
taba.setAttribute("data-toggle","tab");
taba.setAttribute("id",tabId);
taba.innerHTML = data.patientName;
tabli.appendChild(taba);
//Create close button
var btnClose = document.createElement('button');
$(btnClose).addClass("btnClose");
btnClose.innerHTML = "x";
btnClose.onclick = function() { // Note this is a function
removeTab(uuid);
};
taba.appendChild(btnClose);
//Create div content
var tabContent = document.createElement("div");
var contentId = "content"+uuid;
tabContent.setAttribute("id",contentId);
$(tabContent).css("background-color", "black");
$(tabContent).addClass("tab-pane");
$(tabContent).addClass("active");
$("#tabs-content").append(tabContent);
contentID = contentId;
}
function insertContent () {
//Include imageViewer template
UI.insert( UI.render( Template.viewer ), $( '#'+contentID ).get(0) );
}
Template.worklist.events({
'click ul#tabs>li>a': function (event){
var tabId = $(event.target).attr("id");
if ( tabId !== undefined ) {
//Save activeTabId to load correct study for each tab
Session.set("activeTabId", tabId);
var splitTabId = tabId.split("tab");
var contentId = "content"+splitTabId[1];
//Make inactive all tab-pane divs
$("#tabs-content>div").removeClass("active");
$("#"+contentId).addClass("active");
}
}
});
Tracker.autorun(function () {
var data = Session.get('openNewTabEvent');
var content = Session.get('showContentInTab');
if ( data ) {
addNewTab(data);
}
if (content) {
insertContent();
}
Session.set('openNewTabEvent', false);
Session.set('showContentInTab', false);
});
@@ -0,0 +1,50 @@
html, body
height: 100%
margin: 0
padding: 0
background-color: white
color: black
div
margin: 0
padding: 0
/***********************************************/
/* CSS3 Close button */
/***********************************************/
.btnClose
top: 0
right: 0
bottom: 0
width: 20px
height: 20px
text-decoration: none
border: none
background-color: #fcfcfc
-moz-border-radius: 10px
-webkit-border-radius: 10px
border-radius: 10px
margin-left: 5px
font-weight: bold
.tabbable
cursor: pointer
position: absolute
top:0
left:0
bottom: 0
right: 0
width: 100%
height: 100%
color: black
.tab-content
position: absolute
top: 42px
left: 0
right: 0
bottom: 3px
width: 100%
@@ -0,0 +1,20 @@
<template name="worklistResult">
<table id="tblStudyList" class="table table-striped">
<thead>
<tr>
<th>Patient Name</th>
<th>Patient ID</th>
<th>Accession#</th>
<th>Study Date</th>
<th>Modality</th>
<th>Study Description</th>
<th># Images</th>
</tr>
</thead>
<tbody id="studyListData">
{{#each studies}}
{{> worklistStudy }}
{{/each}}
</tbody>
</table>
</template>
@@ -0,0 +1,7 @@
Studies = new Mongo.Collection(null);
Template.worklistResult.helpers({
studies : function() {
return Studies.find();
}
});
@@ -0,0 +1,6 @@
table#tblStudyList
color: black
tbody tr:hover
background-color: #009BD2
color: white
@@ -0,0 +1,88 @@
<template name="worklistSearch">
<!--Search Panel-->
<div class="panel panel-default">
<div class="panel-heading" id="search-panel-head">Search</div>
<div class="panel-body">
<div style="padding: 10px;">
<div class="row">
<div class="col-lg-8 col-md-8 col-xs-12">
<div class="row">
<div class="col-lg-3 col-md-3 col-xs-10">
<div class="form-group">
<label for="patientId">Patient ID</label>
<input type="text" class="form-control" id="patientId" placeholder="Patient ID">
</div>
</div>
<div class="col-lg-3 col-md-3 col-xs-10">
<div class="form-group">
<label for="patientName">Name</label>
<input type="text" class="form-control" id="patientName" placeholder="Name">
</div>
</div>
<div class="col-lg-3 col-md-3 col-xs-10">
<div class="form-group">
<label for="patientAccessionNumber">Accession #</label>
<input type="text" class="form-control" id="patientAccessionNumber" placeholder="Accession #">
</div>
</div>
<div class="col-lg-3 col-md-3 col-xs-10">
<div class="form-group">
<label for="studyDescription">Study Description</label>
<input type="text" class="form-control" id="studyDescription" placeholder="Study Description">
</div>
</div>
</div>
<div class="row">
<div class="col-lg-3 col-md-3 col-xs-10">
<!--Add calendar ui-->
<div class="form-group">
<label for="">Study Date(from)</label>
<p><input type="checkbox" id="checkFrom"><input id="studyDateFrom" type="date" class="form-control" style="padding-left: 30px;"></p>
</div>
</div>
<div class="col-lg-3 col-md-3 col-xs-10">
<div class="form-group">
<label for="">Study Date(to)</label>
<p><input type="checkbox" id="checkTo"><input id="studyDateTo" type="date" class="form-control" style="padding-left:30px;"></p>
</div>
</div>
<div class="col-lg-3 col-md-3 col-xs-10">
<div class="form-group">
<label for="referringPhysician">Referring Physician</label>
<input type="text" class="form-control" id="referringPhysician" placeholder="Referring Physician">
</div>
</div>
<div class="col-lg-3 col-md-3 col-xs-10">
<div class="form-group">
<label for="modality">Modality</label>
<input type="text" class="form-control" id="modality" placeholder="Modality">
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-md-4 col-xs-12">
<div class="row" style="margin-top: 25px;margin-bottom: 25px;">
<div class="col-lg-4 col-md-6 col-xs-6">
<button class="btn btn-primary" id="btnSearch">Search</button>
</div>
<div class="col-lg-4 col-md-6 col-xs-6">
<button class="btn btn-primary" id="btnToday">Today</button>
</div>
</div>
<div class="row">
<div class="col-lg-4 col-md-6 col-xs-6">
<button class="btn btn-primary" id="btnClear">Clear</button>
</div>
<div class="col-lg-4 col-md-6 col-xs-6">
<button class="btn btn-primary" id="btnLastSevenDays">Last 7 Days</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--Search panel ends-->
</template>
@@ -0,0 +1,124 @@
//Get all studies
search();
var studyDateFrom;
var studyDateTo;
var checkFrom = false;
var checkTo = false;
function getFilter(filter) {
if(filter && filter.length && filter.substr(filter.length - 1) !== '*') {
filter += '*';
}
return filter;
}
//Search value
function isIndexOf(mainVal, searchVal) {
if(mainVal === undefined || mainVal === '' || mainVal.indexOf(searchVal) > -1){
return true;
}
return false;
}
//Replace object is undefined
function replaceUndefinedColumnValue (text) {
if (text == undefined || text === "undefined") {
return "";
} else {
return text;
}
}
//Convert string to study date
function convertStringToStudyDate (dateStr) {
var y = dateStr.substring(0,4);
var m = dateStr.substring(4,6);
var d = dateStr.substring(6,8);
var newDateStr = y+"/"+m+"/"+d;
var date_ = new Date(newDateStr);
return date_;
}
function search() {
var filter = {
patientName: getFilter($('#patientName').val()),
patientId: getFilter($('#patientId').val()),
accessionNumber: getFilter($('#patientAccessionNumber').val())
};
var modality = replaceUndefinedColumnValue($('#modality').val());
Studies.remove({});
Meteor.call('WorklistSearch', filter, function(error, studies) {
if (studies) {
studies.forEach(function(study) {
if(isIndexOf(study.modalities, modality) &&
(new Date(studyDateFrom).setHours(0,0,0,0) <= convertStringToStudyDate(study.studyDate) || !checkFrom) &&
(convertStringToStudyDate(study.studyDate) <= new Date(studyDateTo).setHours(0,0,0,0) || !checkTo)) {
Studies.insert(study);
}
});
}
});
}
Template.worklistSearch.events({
//Search
'click button#btnSearch' :function(event) {
studyDateFrom = $('#studyDateFrom').val();
studyDateTo = $('#studyDateTo').val();
checkFrom = $('#checkFrom').is(':checked');
checkTo = $('#checkTo').is(':checked');
search();
return false;
},
//Today
'click button#btnToday' :function(event) {
studyDateTo = new Date();
studyDateFrom = studyDateTo;
checkFrom = true;
checkTo = true;
search();
return false;
},
//Clear
'click button#btnClear' :function(event) {
$("#patientId").val("");
$("#patientName").val("");
$("#patientAccessionNumber").val("");
$("#studyDescription").val("");
$("#referringPhysician").val("");
$("#studyDateFrom").val("");
$("#studyDateTo").val("");
$("#modality").val("");
$('#checkFrom').prop('checked', false);
$('#checkTo').prop('checked', false);
},
//Last 7 Days
'click button#btnLastSevenDays' :function(event) {
studyDateTo = new Date();
studyDateFrom = new Date();
studyDateFrom.setDate(studyDateFrom.getDate()-7);
checkFrom = true;
checkTo = true;
search();
return false;
}
});
@@ -0,0 +1,35 @@
/***********************************************/
/* Search Panel */
/***********************************************/
#search-panel-head{
background-color: #337ab7;
color: white;
font-weight: 500;
font-size: 20px;
line-height: 20px;
box-shadow: 2px 2px 2px #888888;
}
#btnSearch, #btnToday, #btnClear, #btnLastSevenDays{
width: 100%;
}
#checkFrom, #checkTo{
width:20px;
height:20px;
position:absolute;
left:20px;
bottom: 22px;
}
/***********************************************/
/* Tabs */
/***********************************************/
.tab-pane{
position:absolute;
width: 100%;
height:100%;
}
.nav > li > a {
padding: 10px 5px;
}
@@ -0,0 +1,25 @@
<template name="worklistStudy">
<tr>
<td>
{{patientName}}
</td>
<td>
{{patientId}}
</td>
<td>
{{accessionNumber}}
</td>
<td>
{{studyDate}}
</td>
<td>
{{modalities}}
</td>
<td>
{{studyDescription}}
</td>
<td>
{{imageCount}}
</td>
</tr>
</template>
@@ -0,0 +1,5 @@
Template.worklistStudy.events({
'click': function (){
Router.go('viewer', {_id: this.studyInstanceUid});
}
});
@@ -0,0 +1 @@
+31
View File
@@ -0,0 +1,31 @@
Package.describe({
name: "worklist",
summary: "Basic worklist for web-based 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');
// Components
api.addFiles('components/worklist.html', 'client');
api.addFiles('components/worklist.js', 'client');
api.addFiles('components/worklist.styl', 'client');
api.addFiles('components/worklistStudy/worklistStudy.html', 'client');
api.addFiles('components/worklistStudy/worklistStudy.js', 'client');
api.addFiles('components/worklistStudy/worklistStudy.styl', 'client');
api.addFiles('components/worklistResult/worklistResult.html', 'client');
api.addFiles('components/worklistResult/worklistResult.js', 'client');
api.addFiles('components/worklistResult/worklistResult.styl', 'client');
api.addFiles('components/worklistSearch/worklistSearch.html', 'client');
api.addFiles('components/worklistSearch/worklistSearch.js', 'client');
api.addFiles('components/worklistSearch/worklistSearch.styl', 'client');
});