Merged LesionTracker and OHIF Viewer, refactored out common components
This commit is contained in:
1 parent
34adbe3fbc
commit
b774c22593
144 files changed
+27820
No files matched your search
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
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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('\\');
|
||||
};
|
||||
@@ -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
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<template name="viewportOrientationMarkers">
|
||||
<div class="viewportOrientationMarkers noselect">
|
||||
<div class="topMid orientationMarker">
|
||||
</div>
|
||||
<div class="leftMid orientationMarker">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
updateOrientationMarkers = function(element, viewport) {
|
||||
// Updates the orientation labels on the viewport
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
|
||||
|
||||
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
|
||||
var columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
|
||||
var oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
|
||||
var oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
|
||||
|
||||
var markers = {
|
||||
top: oppositeColumnString,
|
||||
left: oppositeRowString,
|
||||
};
|
||||
|
||||
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
|
||||
// the rotation applications
|
||||
if (viewport.vflip) {
|
||||
markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
|
||||
}
|
||||
if (viewport.hflip) {
|
||||
markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
|
||||
}
|
||||
|
||||
// Get the viewport orientation marker DOM elements
|
||||
var viewportOrientationMarkers = $(element).siblings(".viewportOrientationMarkers");
|
||||
var topMarker = viewportOrientationMarkers.find('.topMid');
|
||||
var leftMarker = viewportOrientationMarkers.find('.leftMid');
|
||||
|
||||
// Swap the labels accordingly if the viewport has been rotated
|
||||
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
|
||||
if (viewport.rotation === 90 || viewport.rotation === -270) {
|
||||
topMarker.text(markers.left);
|
||||
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
|
||||
} else if (viewport.rotation === -90 || viewport.rotation === 270) {
|
||||
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
|
||||
leftMarker.text(markers.top);
|
||||
} else if (viewport.rotation === 180 || viewport.rotation === -180) {
|
||||
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
|
||||
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
|
||||
} else {
|
||||
topMarker.text(markers.top);
|
||||
leftMarker.text(markers.left);
|
||||
}
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
.viewportOrientationMarkers
|
||||
pointer-events: none // Necessary for click-through to cornerstone element below
|
||||
|
||||
font-size: 15px
|
||||
color: rgb(204, 204, 204)
|
||||
line-height: 18px
|
||||
|
||||
.orientationMarker
|
||||
position: absolute
|
||||
|
||||
.topMid
|
||||
top: 5px
|
||||
left: 50%
|
||||
|
||||
.leftMid
|
||||
top: 47%
|
||||
left: 5px
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<template name="viewportOverlay">
|
||||
<div class="imageViewerViewportOverlay noselect">
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="topright dicomTag">
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{studyDate}} {{studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomright dicomTag">
|
||||
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
|
||||
<div>{{compression}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
<div class="bottomleft dicomTag">
|
||||
<div>Ser: {{seriesNumber}}</div>
|
||||
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
|
||||
<div>{{frameRate}}</div>
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,155 @@
|
||||
function getElementIfNotEmpty(viewportIndex) {
|
||||
// Meteor template helpers run more often than expected
|
||||
// They often seem to run just before the whole template is rendered
|
||||
// This meant that the onRendered event hadn't fired yet, so the
|
||||
// element wasn't enabled / set empty yet. The check here
|
||||
// for canvases under the 'enabled' element div is to prevent
|
||||
// 'undefined' errors from the helper functions
|
||||
|
||||
var imageViewerViewports = $('.imageViewerViewport'),
|
||||
element = imageViewerViewports.get(viewportIndex),
|
||||
canvases = imageViewerViewports.eq(viewportIndex).find('canvas');
|
||||
|
||||
if (!element || $(element).hasClass("empty") || canvases.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to make sure the element is enabled.
|
||||
try {
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function getPatient(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var patient = cornerstoneTools.metaData.get('patient', this.imageId);
|
||||
if (!patient) {
|
||||
return '';
|
||||
}
|
||||
return patient[property];
|
||||
}
|
||||
|
||||
function getStudy(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var study = cornerstoneTools.metaData.get('study', this.imageId);
|
||||
if (!study) {
|
||||
return '';
|
||||
}
|
||||
return study[property];
|
||||
}
|
||||
|
||||
function getSeries(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var series = cornerstoneTools.metaData.get('series', this.imageId);
|
||||
if (!series) {
|
||||
return '';
|
||||
}
|
||||
return series[property];
|
||||
}
|
||||
|
||||
function getInstance(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var instance = cornerstoneTools.metaData.get('instance', this.imageId);
|
||||
if (!instance) {
|
||||
return '';
|
||||
}
|
||||
return instance[property];
|
||||
}
|
||||
|
||||
function getImage(viewportIndex) {
|
||||
var element = getElementIfNotEmpty(viewportIndex);
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
var enabledElement;
|
||||
try {
|
||||
enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return false;
|
||||
}
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return false;
|
||||
}
|
||||
return enabledElement.image;
|
||||
}
|
||||
|
||||
Template.viewportOverlay.helpers({
|
||||
wwwc: function() {
|
||||
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
||||
var element = getElementIfNotEmpty(this.viewportIndex);
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) {
|
||||
return '';
|
||||
}
|
||||
return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
|
||||
},
|
||||
zoom: function() {
|
||||
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
||||
var element = getElementIfNotEmpty(this.viewportIndex);
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) {
|
||||
return '';
|
||||
}
|
||||
return (viewport.scale * 100.0);
|
||||
},
|
||||
imageDimensions: function() {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
|
||||
var image = getImage(this.viewportIndex);
|
||||
if (!image) {
|
||||
return '';
|
||||
}
|
||||
return image.width + ' x ' + image.height;
|
||||
},
|
||||
patientName : function() {
|
||||
return getPatient.call(this, 'name');
|
||||
},
|
||||
patientId : function() {
|
||||
return getPatient.call(this, 'id');
|
||||
},
|
||||
studyDate : function() {
|
||||
return getStudy.call(this, 'date');
|
||||
},
|
||||
studyTime : function() {
|
||||
return getStudy.call(this, 'time');
|
||||
},
|
||||
studyDescription : function() {
|
||||
return getStudy.call(this, 'description');
|
||||
},
|
||||
seriesDescription : function() {
|
||||
return getSeries.call(this, 'description');
|
||||
},
|
||||
seriesNumber : function() {
|
||||
return getSeries.call(this, 'number');
|
||||
},
|
||||
imageNumber : function() {
|
||||
return getInstance.call(this, 'number');
|
||||
},
|
||||
imageIndex : function() {
|
||||
return getInstance.call(this, 'index');
|
||||
},
|
||||
numImages : function() {
|
||||
return getSeries.call(this, 'numImages');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
.imageViewerViewportOverlay
|
||||
font-size: 13px
|
||||
color: #e4ad00
|
||||
line-height: 18px
|
||||
display: none // Shown when an image is loaded
|
||||
|
||||
.dicomTag
|
||||
pointer-events: none // Necessary for drag/drop through to cornerstone element below
|
||||
position: absolute
|
||||
|
||||
.topleft
|
||||
top: 3px
|
||||
left: 3px
|
||||
|
||||
.topright
|
||||
top: 3px
|
||||
right: 3px
|
||||
text-align: right
|
||||
|
||||
.bottomleft
|
||||
bottom: 3px
|
||||
left: 3px
|
||||
|
||||
.bottomright
|
||||
bottom: 3px
|
||||
right: 3px
|
||||
text-align: right
|
||||
@@ -0,0 +1,5 @@
|
||||
/*AccountsTemplates.configure({
|
||||
forbidClientAccountCreation: true // do not allow account creation
|
||||
});
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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 @@
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user