Merged LesionTracker and OHIF Viewer, refactored out common components
This commit is contained in:
1 parent
34adbe3fbc
commit
b774c22593
144 files changed
+27820
No files matched your search
@@ -0,0 +1,31 @@
|
||||
<template name="layout">
|
||||
<nav class="navbar navbar-default" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/">OHIF Viewer</a>
|
||||
</div>
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li>
|
||||
<a href="http://www.ohif.org">About</a>
|
||||
</li>
|
||||
<li>
|
||||
<form class="navbar-form">
|
||||
<div class="form-group">
|
||||
{{> loginButtons align="right"}}
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{{> yield}}
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
.navbar
|
||||
margin-bottom: 0
|
||||
border-radius: 0px
|
||||
|
||||
.navbar-form
|
||||
margin: 0
|
||||
padding: 15px 3px
|
||||
|
||||
.navbar-default
|
||||
-webkit-transition: background-color 200ms linear
|
||||
-moz-transition: background-color 200ms linear
|
||||
-o-transition: background-color 200ms linear
|
||||
-ms-transition: background-color 200ms linear
|
||||
transition: background-color 200ms linear
|
||||
|
||||
@media (min-width: 768px)
|
||||
.navbar
|
||||
border-radius: 0px
|
||||
@@ -0,0 +1,3 @@
|
||||
<template name="login">
|
||||
{{> atForm}}
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
<template name="notFound">
|
||||
Not found
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
<template name="imageThumbnail">
|
||||
<div class="imageThumbnail">
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
Template.imageThumbnail.onRendered(function() {
|
||||
var instance = this.data.instances[0];
|
||||
var element = this.find('.imageThumbnail');
|
||||
cornerstone.enable(element);
|
||||
|
||||
var imageId = getImageId(instance);
|
||||
|
||||
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||
cornerstone.displayImage(element, image);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
.imageThumbnail
|
||||
width: 90px
|
||||
height: 90px
|
||||
background-color: black
|
||||
margin: 0 auto
|
||||
@@ -0,0 +1,7 @@
|
||||
<template name="studyBrowser">
|
||||
<div class="studyBrowser">
|
||||
{{#each studies}}
|
||||
{{> thumbnails}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,6 @@
|
||||
Template.studyBrowser.helpers({
|
||||
studies : function() {
|
||||
var studies = Session.get('studies');
|
||||
return studies;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
.studyBrowser
|
||||
float: left
|
||||
height: 100%
|
||||
width: 120px
|
||||
overflow: auto
|
||||
background-color: black
|
||||
@@ -0,0 +1,6 @@
|
||||
<template name="thumbnailEntry">
|
||||
<div class="thumbnailEntry">
|
||||
{{ > imageThumbnail }}
|
||||
<div class='seriesDescription noselect'>{{seriesDescription}}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,221 @@
|
||||
function cloneElement(element, targetId) {
|
||||
// Clone the DOM element
|
||||
var clone = element.cloneNode(true);
|
||||
|
||||
// Find any canvas children to clone
|
||||
var clonedCanvases = $(clone).find('canvas');
|
||||
clonedCanvases.each(function(canvasIndex, clonedCanvas) {
|
||||
// Draw from the original canvas to the cloned canvas
|
||||
var context = clonedCanvas.getContext('2d');
|
||||
var thumbnailCanvas = $(element).find('canvas').get(canvasIndex);
|
||||
context.drawImage(thumbnailCanvas, 0, 0);
|
||||
});
|
||||
|
||||
// Update the clone with the targetId
|
||||
clone.id = targetId;
|
||||
clone.style.visibility = 'hidden';
|
||||
return clone;
|
||||
}
|
||||
|
||||
function thumbnailDragStartHandler(e) {
|
||||
// Prevent any scrolling behaviour normally caused by the original event
|
||||
e.originalEvent.preventDefault();
|
||||
|
||||
// Identify the current study and series index from the thumbnail's DOM position
|
||||
var targetThumbnail = e.currentTarget;
|
||||
var imageThumbnail = $(targetThumbnail);
|
||||
|
||||
// Store this data for use during drag and drop
|
||||
OHIF.viewer.dragAndDropData = {
|
||||
studyInstanceUid: imageThumbnail.data('studyInstanceUid'),
|
||||
seriesInstanceUid: imageThumbnail.data('seriesInstanceUid')
|
||||
};
|
||||
|
||||
// Clone the image thumbnail
|
||||
var targetId = targetThumbnail.id + 'DragClone';
|
||||
var clone = cloneElement(targetThumbnail, targetId);
|
||||
clone.classList.add('imageThumbnailClone');
|
||||
|
||||
// Set pointerEvents to pass through the clone DOM element
|
||||
// This is necessary in order to identify what is below it
|
||||
// when using document.elementFromPoint
|
||||
clone.style.pointerEvents = "none";
|
||||
|
||||
// Append the clone to the parent of the target
|
||||
targetThumbnail.parentNode.appendChild(clone);
|
||||
|
||||
// Set the cursor x and y positions from the current touch/mouse coordinates
|
||||
// Handle touchStart cases
|
||||
if (e.type === 'touchstart') {
|
||||
cursorX = e.originalEvent.touches[0].pageX;
|
||||
cursorY = e.originalEvent.touches[0].pageY;
|
||||
} else {
|
||||
cursorX = e.pageX;
|
||||
cursorY = e.pageY;
|
||||
|
||||
// Also hook up event handlers for mouse events
|
||||
$(document).on('mousemove', function(e) {
|
||||
thumbnailDragHandler(e, targetThumbnail);
|
||||
});
|
||||
$(document).on('mouseup', function(e) {
|
||||
thumbnailDragEndHandler(e, targetThumbnail);
|
||||
});
|
||||
}
|
||||
|
||||
// This block gets the current position of the touch/mouse
|
||||
// relative to the thumbnail itself
|
||||
//
|
||||
// i.e. Where did the user grab it from?
|
||||
var position = imageThumbnail.offset();
|
||||
var left = position.left;
|
||||
var top = position.top;
|
||||
|
||||
// This difference is saved for later so the element movement looks normal
|
||||
diffX = cursorX - left;
|
||||
diffY = cursorY - top;
|
||||
|
||||
// This sets the default style properties of the cloned element so it is
|
||||
// ready to be dragged around the page
|
||||
$(clone).css({
|
||||
top: top,
|
||||
left: left,
|
||||
position: 'absolute',
|
||||
'z-index': 100,
|
||||
visibility: 'hidden'
|
||||
});
|
||||
}
|
||||
|
||||
function thumbnailDragHandler(e, target) {
|
||||
// Get the touch/mouse coordinates from the event
|
||||
var cursorX,
|
||||
cursorY;
|
||||
if (e.type === 'touchmove') {
|
||||
cursorX = e.originalEvent.changedTouches[0].pageX;
|
||||
cursorY = e.originalEvent.changedTouches[0].pageY;
|
||||
} else {
|
||||
cursorX = e.pageX;
|
||||
cursorY = e.pageY;
|
||||
}
|
||||
|
||||
// Find the clone element and update it's position on the page
|
||||
var clone = $('#' + target.id + 'DragClone');
|
||||
clone.css({
|
||||
top: cursorY - diffY,
|
||||
left: cursorX - diffX,
|
||||
position: 'absolute',
|
||||
'z-index': 100,
|
||||
visibility: 'visible'
|
||||
});
|
||||
|
||||
// Identify the element below the current cursor position
|
||||
var elemBelow = document.elementFromPoint(cursorX + diffX, cursorY + diffY);
|
||||
|
||||
// If none exists, stop here
|
||||
if (!elemBelow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any current faded effects on viewports
|
||||
$('.imageViewerViewport canvas').removeClass('faded');
|
||||
|
||||
// Figure out what to do depending on what we're dragging over
|
||||
var viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
|
||||
if (viewportsDraggedOver.length) {
|
||||
// If we're dragging over a non-empty viewport, fade it and change the cursor style
|
||||
viewportsDraggedOver.find('canvas').not('.magnifyTool').addClass('faded');
|
||||
document.body.style.cursor = "copy";
|
||||
} else if (elemBelow.classList.contains('imageViewerViewport') && elemBelow.classList.contains('empty')) {
|
||||
// If we're dragging over an empty viewport, just change the cursor style
|
||||
document.body.style.cursor = "copy";
|
||||
} else {
|
||||
// Otherwise, keep the cursor as no-drop style
|
||||
document.body.style.cursor = "no-drop";
|
||||
}
|
||||
}
|
||||
|
||||
function thumbnailDragEndHandler(e, target) {
|
||||
// Remove the mouse event listeners
|
||||
$(document).off('mousemove mouseup');
|
||||
|
||||
// Reset the cursor style to the default
|
||||
document.body.style.cursor = "auto";
|
||||
|
||||
// Get the cloned element
|
||||
var clone = $('#' + target.id + 'DragClone');
|
||||
|
||||
// If it doesn't exist, stop here
|
||||
if (!clone.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var top = clone.offset().top;
|
||||
var left = clone.offset().left;
|
||||
|
||||
// Identify the element below the cloned element position
|
||||
var elemBelow = document.elementFromPoint(left + diffX, top + diffY);
|
||||
|
||||
// Remove all cloned elements from the page
|
||||
$('.imageThumbnailClone').remove();
|
||||
|
||||
// Remove any current faded effects on viewports
|
||||
$('.imageViewerViewport canvas').removeClass('faded');
|
||||
|
||||
// If none exists, stop here
|
||||
if (!elemBelow) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any fade effects on the element below
|
||||
elemBelow.classList.remove('faded');
|
||||
|
||||
var element;
|
||||
var viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
|
||||
if (viewportsDraggedOver.length) {
|
||||
// If we're dragging over a non-empty viewport, retrieve it
|
||||
element = viewportsDraggedOver.get(0);
|
||||
} else if (elemBelow.classList.contains('imageViewerViewport') &&
|
||||
elemBelow.classList.contains('empty')) {
|
||||
// If we're dragging over an empty viewport, retrieve that instead
|
||||
element = elemBelow;
|
||||
} else {
|
||||
// Otherwise, stop here
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is no store drag and drop data, stop here
|
||||
if (!OHIF.viewer.dragAndDropData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rerender the viewport using the drag and drop data
|
||||
rerenderViewportWithNewSeries(element, OHIF.viewer.dragAndDropData);
|
||||
return false;
|
||||
}
|
||||
|
||||
Template.thumbnailEntry.onRendered(function() {
|
||||
var entry = this.find('.thumbnailEntry');
|
||||
$(entry).data('seriesInstanceUid', Template.parentData(0).seriesInstanceUid);
|
||||
$(entry).data('studyInstanceUid', Template.parentData(1).studyInstanceUid);
|
||||
});
|
||||
|
||||
Template.thumbnailEntry.events({
|
||||
// Touch drag/drop events
|
||||
'touchstart .thumbnailEntry, mousedown .thumbnailEntry': function(e) {
|
||||
thumbnailDragStartHandler(e);
|
||||
},
|
||||
'touchmove .thumbnailEntry': function(e) {
|
||||
thumbnailDragHandler(e, e.currentTarget);
|
||||
},
|
||||
'touchend .thumbnailEntry': function(e){
|
||||
thumbnailDragEndHandler(e, e.currentTarget);
|
||||
}
|
||||
});
|
||||
|
||||
Template.thumbnailEntry.helpers({
|
||||
seriesDescription : function() {
|
||||
if (!this.seriesDescription) {
|
||||
return '';
|
||||
}
|
||||
return this.seriesDescription;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
.thumbnailEntry
|
||||
width: 105px
|
||||
padding: 5px
|
||||
background-color: black
|
||||
border: 1px solid #424242
|
||||
margin: 0 auto
|
||||
|
||||
.seriesDescription
|
||||
text-align: center
|
||||
color: #555
|
||||
word-wrap: break-word
|
||||
|
||||
&:hover
|
||||
background-color: white
|
||||
|
||||
&.active
|
||||
background-color: #424242
|
||||
border-color: #4e4e4e
|
||||
|
||||
.seriesDescription
|
||||
color: white
|
||||
@@ -0,0 +1,5 @@
|
||||
<template name="thumbnails">
|
||||
{{#each thumbnails}}
|
||||
{{ >thumbnailEntry }}
|
||||
{{/each}}
|
||||
</template>
|
||||
@@ -0,0 +1,6 @@
|
||||
Template.thumbnails.helpers({
|
||||
thumbnails: function() {
|
||||
var stacks = createStacks(this);
|
||||
return stacks;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<template name="imageViewerViewport">
|
||||
<div class='imageViewerViewport'
|
||||
oncontextmenu='return false;'
|
||||
unselectable='on'
|
||||
onselectstart='return false;'>
|
||||
</div>
|
||||
<div class='viewportInstructions'>
|
||||
Please drag a stack here to view images.
|
||||
</div>
|
||||
{{ >loadingIndicator }}
|
||||
{{ >viewportOverlay }}
|
||||
{{ >viewportOrientationMarkers }}
|
||||
</template>
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* This function enables stack prefetching for a specified element (viewport)
|
||||
* It first disables any prefetching currently occurring on any other viewports.
|
||||
* Prefetching will not be enabled if the series is a clip.
|
||||
*
|
||||
* @param element
|
||||
*/
|
||||
function enablePrefetchOnElement() {
|
||||
var viewportIndex = Session.get('ActiveViewport');
|
||||
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||
console.log('Enabling prefetch on new element');
|
||||
|
||||
// Loop through all viewports and disable stackPrefetch
|
||||
$('.imageViewerViewport').each(function() {
|
||||
if (!$(this).find('canvas').length) {
|
||||
return;
|
||||
}
|
||||
cornerstoneTools.stackPrefetch.disable(this);
|
||||
});
|
||||
|
||||
// Make sure there is a stack to fetch
|
||||
var stack = cornerstoneTools.getToolState(element, 'stack');
|
||||
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
|
||||
cornerstoneTools.stackPrefetch.enable(element);
|
||||
}
|
||||
}
|
||||
|
||||
function displayReferenceLines() {
|
||||
console.log("Changing reference line display");
|
||||
var viewportIndex = Session.get('ActiveViewport');
|
||||
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||
|
||||
$('.imageViewerViewport').each(function(index, element) {
|
||||
if (!$(this).find('canvas').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index === viewportIndex) {
|
||||
cornerstoneTools.referenceLines.tool.disable(element);
|
||||
return;
|
||||
}
|
||||
cornerstoneTools.referenceLines.tool.enable(element, OHIF.viewer.updateImageSynchronizer);
|
||||
});
|
||||
}
|
||||
|
||||
function loadSeriesIntoViewport(data) {
|
||||
if (!data.series || !data.viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
var study = data.study;
|
||||
var series = data.series;
|
||||
var element = data.viewport;
|
||||
var viewportIndex = $(".imageViewerViewport").index(data.viewport);
|
||||
|
||||
var allEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
|
||||
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
|
||||
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
|
||||
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
|
||||
'CornerstoneToolsDragStartActive CornerstoneToolsMultiTouchDragStart';
|
||||
|
||||
var imageIds = [];
|
||||
var numImages = series.instances.length;
|
||||
series.instances.forEach(function(instance, imageIndex) {
|
||||
var imageId = getImageId(instance);
|
||||
imageIds.push(imageId);
|
||||
var data = {
|
||||
instance: instance,
|
||||
series: series,
|
||||
study: study,
|
||||
numImages: numImages,
|
||||
imageIndex: imageIndex + 1
|
||||
};
|
||||
addMetaData(imageId, data);
|
||||
});
|
||||
|
||||
var stack = {
|
||||
currentImageIdIndex: 0,
|
||||
imageIds: imageIds
|
||||
};
|
||||
|
||||
var imageId = imageIds[stack.currentImageIdIndex];
|
||||
|
||||
var templateData = Template.currentData();
|
||||
templateData.imageId = imageId;
|
||||
|
||||
// NOTE: This uses the experimental WebGL renderer for Cornerstone!
|
||||
cornerstone.enable(element, cornerstone.webGL.renderer.render);
|
||||
// If you have problems, replace it with this line instead:
|
||||
// cornerstone.enable(element);
|
||||
|
||||
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||
cornerstone.displayImage(element, image);
|
||||
element.classList.remove('empty');
|
||||
$(element).siblings('.viewportInstructions').hide();
|
||||
$(element).siblings('.imageViewerViewportOverlay').show();
|
||||
|
||||
cornerstone.resize(element, true);
|
||||
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
|
||||
|
||||
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
|
||||
|
||||
// Enable orientation markers, if applicable
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
updateOrientationMarkers(element, viewport);
|
||||
|
||||
// Clear any old stack data
|
||||
cornerstoneTools.clearToolState(element, 'stack');
|
||||
cornerstoneTools.addToolState(element, 'stack', stack);
|
||||
|
||||
// Enable mouse input
|
||||
cornerstoneTools.mouseInput.enable(element);
|
||||
cornerstoneTools.touchInput.enable(element);
|
||||
cornerstoneTools.mouseWheelInput.enable(element);
|
||||
|
||||
var activeTool = toolManager.getActiveTool();
|
||||
toolManager.setActiveTool(activeTool);
|
||||
|
||||
cornerstoneTools.magnify.enable(element);
|
||||
|
||||
function onImageRendered(e, eventData) {
|
||||
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
|
||||
}
|
||||
|
||||
$(element).off('CornerstoneImageRendered', onImageRendered);
|
||||
$(element).on('CornerstoneImageRendered', onImageRendered);
|
||||
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
|
||||
|
||||
function onNewImage(e, eventData) {
|
||||
// Update the templateData with the new imageId
|
||||
// This allows the template helpers to update reactively
|
||||
templateData.imageId = eventData.enabledElement.image.imageId;
|
||||
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
||||
}
|
||||
|
||||
$(element).off('CornerstoneNewImage', onNewImage);
|
||||
$(element).on('CornerstoneNewImage', onNewImage);
|
||||
|
||||
function sendActivationTrigger(e, eventData) {
|
||||
var activeViewportIndex = Session.get('ActiveViewport');
|
||||
var viewportIndex = $(".imageViewerViewport").index(eventData.element);
|
||||
if (viewportIndex === activeViewportIndex) {
|
||||
return;
|
||||
}
|
||||
eventData.viewportIndex = viewportIndex;
|
||||
var customEvent = jQuery.Event('ActivateViewport', eventData);
|
||||
customEvent.type = 'ActivateViewport'; // Need to overwrite the type set in the touch tools
|
||||
$(e.target).trigger(customEvent, eventData);
|
||||
}
|
||||
|
||||
$(element).off(allEvents, sendActivationTrigger);
|
||||
$(element).on(allEvents, sendActivationTrigger);
|
||||
|
||||
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
||||
|
||||
if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
|
||||
OHIF.viewer.updateImageSynchronizer.add(element);
|
||||
}
|
||||
|
||||
enablePrefetchOnElement();
|
||||
displayReferenceLines();
|
||||
});
|
||||
}
|
||||
|
||||
Template.imageViewerViewport.onRendered(function() {
|
||||
var studies = Session.get('studies');
|
||||
var viewport = this.find(".imageViewerViewport");
|
||||
var viewportIndex = $(".imageViewerViewport").index(viewport);
|
||||
|
||||
this.data.viewportIndex = viewportIndex;
|
||||
|
||||
var data = {
|
||||
viewport: viewport
|
||||
};
|
||||
|
||||
if (this.data.seriesInstanceUid !== undefined && this.data.studyInstanceUid !== undefined) {
|
||||
var studyInstanceUid = this.data.studyInstanceUid;
|
||||
var seriesInstanceUid = this.data.seriesInstanceUid;
|
||||
|
||||
studies.every(function(study) {
|
||||
if (study.studyInstanceUid === studyInstanceUid) {
|
||||
data.study = study;
|
||||
study.seriesList.every(function(series) {
|
||||
if (series.seriesInstanceUid === seriesInstanceUid) {
|
||||
data.series = series;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
var stacks = [];
|
||||
studies.forEach(function(study) {
|
||||
study.seriesList.forEach(function(series) {
|
||||
var stack = {
|
||||
series: series,
|
||||
study: study
|
||||
};
|
||||
stacks.push(stack);
|
||||
});
|
||||
});
|
||||
|
||||
if (viewportIndex >= stacks.length) {
|
||||
viewport.classList.add('empty');
|
||||
$(viewport).siblings('.viewportInstructions').show();
|
||||
return;
|
||||
}
|
||||
data.series = stacks[viewportIndex].series;
|
||||
data.study = stacks[viewportIndex].study;
|
||||
}
|
||||
loadSeriesIntoViewport(data);
|
||||
});
|
||||
|
||||
Template.imageViewerViewport.onDestroyed(function() {
|
||||
var element = this.find(".imageViewerViewport");
|
||||
cornerstone.disable(element);
|
||||
});
|
||||
|
||||
Template.imageViewerViewport.events({
|
||||
'ActivateViewport .imageViewerViewport': function(e) {
|
||||
console.log('ActivateViewport: ' + e.viewportIndex);
|
||||
Session.set('ActiveViewport', e.viewportIndex);
|
||||
enablePrefetchOnElement();
|
||||
displayReferenceLines();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
.imageViewerViewport
|
||||
width: 100%
|
||||
height: 100%
|
||||
background-color: black
|
||||
|
||||
// Prevents the entire page from getting larger
|
||||
// when the magnify tool is near the sides/corners of the page
|
||||
overflow: hidden
|
||||
|
||||
.viewportInstructions
|
||||
display: none
|
||||
font-size: 13px
|
||||
color: #e4ad00
|
||||
line-height: 18px
|
||||
pointer-events: none // Necessary for drag/drop through to cornerstone element below
|
||||
text-align: center
|
||||
position: absolute
|
||||
top: 0
|
||||
bottom: 0
|
||||
right: 0
|
||||
left: 0
|
||||
margin: auto
|
||||
height: 20px
|
||||
@@ -0,0 +1,11 @@
|
||||
<template name="imageViewerViewports">
|
||||
<div id='imageViewerViewports'>
|
||||
{{ #each viewportArray }}
|
||||
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
|
||||
<div class="removable">
|
||||
{{ > imageViewerViewport}}
|
||||
</div>
|
||||
</div>
|
||||
{{ /each }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
function getNumViewports() {
|
||||
var viewportRows = Session.get('viewportRows');
|
||||
var viewportColumns = Session.get('viewportColumns');
|
||||
return viewportRows * viewportColumns;
|
||||
}
|
||||
|
||||
Template.imageViewerViewports.helpers({
|
||||
height: function() {
|
||||
var viewportRows = Session.get('viewportRows');
|
||||
return 100 / viewportRows;
|
||||
},
|
||||
width: function() {
|
||||
var viewportColumns = Session.get('viewportColumns');
|
||||
return 100 / viewportColumns;
|
||||
},
|
||||
viewportArray: function() {
|
||||
// This is a really annoying thing to have to do, but Meteor
|
||||
// doesn't want to let me use another type of helper.
|
||||
var numViewports = getNumViewports();
|
||||
|
||||
var array = [];
|
||||
for (var i=0; i < numViewports; ++i) {
|
||||
var data = {
|
||||
viewportIndex: i
|
||||
};
|
||||
array.push(data);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
#imageViewerViewports
|
||||
height: calc(100% - 30px)
|
||||
width: 100%
|
||||
|
||||
.viewportContainer
|
||||
float: left
|
||||
border: 2px solid #777
|
||||
|
||||
.removable
|
||||
width: 100%
|
||||
height: 100%
|
||||
position: relative // Necessary so that the viewportOverlay is on top of the viewports
|
||||
@@ -0,0 +1,21 @@
|
||||
<template name="layoutChooser">
|
||||
<div class="layoutChooser pull-right dropdown-menu" role="menu">
|
||||
<table>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,41 @@
|
||||
function highlightCells(currentCell) {
|
||||
var cells = $('.layoutChooser table td');
|
||||
cells.removeClass('hover');
|
||||
|
||||
currentCell = $(currentCell);
|
||||
var table = currentCell.parents('.layoutChooser table').get(0);
|
||||
var rowIndex = currentCell.closest('tr').index();
|
||||
var columnIndex = currentCell.index();
|
||||
|
||||
// Loop through the table row by row
|
||||
// and cell by cell to apply the highlighting
|
||||
for (var i = 0; i < table.rows.length; i++) {
|
||||
row = table.rows[i];
|
||||
if (i <= rowIndex) {
|
||||
for (var j = 0; j < row.cells.length; j++) {
|
||||
if (j <= columnIndex) {
|
||||
cell = row.cells[j];
|
||||
cell.classList.add('hover');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Template.layoutChooser.events({
|
||||
'touchstart .layoutChooser table td, mouseenter .layoutChooser table td': function(evt) {
|
||||
highlightCells(evt.currentTarget);
|
||||
},
|
||||
'click .layoutChooser table td': function(evt) {
|
||||
$('#imageViewerViewports').remove();
|
||||
var container = $(".viewerMain").get(0);
|
||||
UI.render(Template.imageViewerViewports, container);
|
||||
|
||||
var currentCell = $(evt.currentTarget);
|
||||
var rowIndex = currentCell.closest('tr').index();
|
||||
var columnIndex = currentCell.index();
|
||||
|
||||
// Add 1 because the indices start from zero
|
||||
Session.set('viewportRows', rowIndex + 1);
|
||||
Session.set('viewportColumns', columnIndex + 1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
.layoutChooser
|
||||
width: 75px
|
||||
min-width: 75px // to override bootstrap's dropdown-menu class
|
||||
height: 75px
|
||||
|
||||
table
|
||||
margin: 0 auto
|
||||
|
||||
td
|
||||
cursor: pointer
|
||||
border: 2px solid black
|
||||
width: 20px
|
||||
height: 20px
|
||||
transition: background-color 0.1s linear
|
||||
|
||||
&:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave
|
||||
background-color: gray
|
||||
@@ -0,0 +1,14 @@
|
||||
<template name="loadingIndicator">
|
||||
<div class="imageViewerLoadingIndicator loadingIndicator">
|
||||
<div class="indicatorContents">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="imageViewerErrorLoadingIndicator loadingIndicator">
|
||||
<div class="indicatorContents">
|
||||
<h4>Error Loading Image</h4>
|
||||
<p class='description'>An error has occurred.</p>
|
||||
<p class='details'></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,61 @@
|
||||
Meteor.startup(function() {
|
||||
cornerstoneTools.loadHandlerManager.setStartLoadHandler(startLoadingHandler);
|
||||
cornerstoneTools.loadHandlerManager.setEndLoadHandler(doneLoadingHandler);
|
||||
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(errorLoadingHandler);
|
||||
});
|
||||
|
||||
var loadHandlerTimeout;
|
||||
|
||||
startLoadingHandler = function(element) {
|
||||
clearTimeout(loadHandlerTimeout);
|
||||
loadHandlerTimeout = setTimeout(function() {
|
||||
var elem = $(element);
|
||||
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
|
||||
elem.find('canvas').not('.magnifyTool').addClass("faded");
|
||||
elem.siblings('.imageViewerLoadingIndicator').css('display', 'block');
|
||||
}, OHIF.viewer.loadIndicatorDelay);
|
||||
};
|
||||
|
||||
doneLoadingHandler = function(element) {
|
||||
clearTimeout(loadHandlerTimeout);
|
||||
var elem = $(element);
|
||||
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
|
||||
elem.find('canvas').not('.magnifyTool').removeClass("faded");
|
||||
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
||||
};
|
||||
|
||||
errorLoadingHandler = function(element, imageId, error, source) {
|
||||
clearTimeout(loadHandlerTimeout);
|
||||
var elem = $(element);
|
||||
|
||||
// Could probably chain all of these, but this is more readable
|
||||
elem.find('canvas').not('.magnifyTool').removeClass("faded");
|
||||
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
||||
|
||||
// Don't display errors from the stackPrefetch tool
|
||||
if (source === "stackPrefetch") {
|
||||
return;
|
||||
}
|
||||
|
||||
var errorLoadingIndicator = elem.siblings('.imageViewerErrorLoadingIndicator');
|
||||
errorLoadingIndicator.css('display', 'block');
|
||||
|
||||
var cleanedImageId = imageId;
|
||||
|
||||
// This is just used to expand upon some error messages that are sent
|
||||
// when things fail. An example is a network error throwing the error
|
||||
// which is only described as "network".
|
||||
var errorDetails = {
|
||||
network: "A network error has occurred"
|
||||
// We need to expand this further when we see more obscure error messages
|
||||
};
|
||||
|
||||
if (errorDetails.hasOwnProperty(error)) {
|
||||
error = errorDetails[error];
|
||||
}
|
||||
|
||||
errorLoadingIndicator.find('.description').text("An error has occurred while loading image: " + cleanedImageId);
|
||||
if (error) {
|
||||
errorLoadingIndicator.find('.details').text("Details: " + error);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
.imageViewerLoadingIndicator
|
||||
display: none
|
||||
color: #ffa500
|
||||
|
||||
.faded
|
||||
opacity: 0.5
|
||||
|
||||
.imageViewerErrorLoadingIndicator
|
||||
display: none
|
||||
background-color: rgba(0,0,0,0.75)
|
||||
|
||||
p, h4
|
||||
text-align: center
|
||||
color: #ffa500
|
||||
padding: 4px 0px
|
||||
|
||||
p
|
||||
font-size: 11pt
|
||||
|
||||
.loadingIndicator
|
||||
pointer-events: none // Necessary for click-through to cornerstone element below
|
||||
width: 100%
|
||||
height: 100%
|
||||
font-size: 18px
|
||||
z-index: 100
|
||||
|
||||
.indicatorContents
|
||||
position: absolute
|
||||
top: 40%
|
||||
width: 100%
|
||||
text-align: center
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<template name="viewportOrientationMarkers">
|
||||
<div class="viewportOrientationMarkers noselect">
|
||||
<div class="topMid orientationMarker">
|
||||
</div>
|
||||
<div class="leftMid orientationMarker">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
updateOrientationMarkers = function(element, viewport) {
|
||||
// Updates the orientation labels on the viewport
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
|
||||
|
||||
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
|
||||
var columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
|
||||
var oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
|
||||
var oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
|
||||
|
||||
var markers = {
|
||||
top: oppositeColumnString,
|
||||
left: oppositeRowString,
|
||||
};
|
||||
|
||||
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
|
||||
// the rotation applications
|
||||
if (viewport.vflip) {
|
||||
markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
|
||||
}
|
||||
if (viewport.hflip) {
|
||||
markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
|
||||
}
|
||||
|
||||
// Get the viewport orientation marker DOM elements
|
||||
var viewportOrientationMarkers = $(element).siblings(".viewportOrientationMarkers");
|
||||
var topMarker = viewportOrientationMarkers.find('.topMid');
|
||||
var leftMarker = viewportOrientationMarkers.find('.leftMid');
|
||||
|
||||
// Swap the labels accordingly if the viewport has been rotated
|
||||
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
|
||||
if (viewport.rotation === 90 || viewport.rotation === -270) {
|
||||
topMarker.text(markers.left);
|
||||
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
|
||||
} else if (viewport.rotation === -90 || viewport.rotation === 270) {
|
||||
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
|
||||
leftMarker.text(markers.top);
|
||||
} else if (viewport.rotation === 180 || viewport.rotation === -180) {
|
||||
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
|
||||
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
|
||||
} else {
|
||||
topMarker.text(markers.top);
|
||||
leftMarker.text(markers.left);
|
||||
}
|
||||
};
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
.viewportOrientationMarkers
|
||||
pointer-events: none // Necessary for click-through to cornerstone element below
|
||||
|
||||
font-size: 15px
|
||||
color: rgb(204, 204, 204)
|
||||
line-height: 18px
|
||||
|
||||
.orientationMarker
|
||||
position: absolute
|
||||
|
||||
.topMid
|
||||
top: 5px
|
||||
left: 50%
|
||||
|
||||
.leftMid
|
||||
top: 47%
|
||||
left: 5px
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<template name="viewportOverlay">
|
||||
<div class="imageViewerViewportOverlay noselect">
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="topright dicomTag">
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{studyDate}} {{studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomright dicomTag">
|
||||
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
|
||||
<div>{{compression}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
<div class="bottomleft dicomTag">
|
||||
<div>Ser: {{seriesNumber}}</div>
|
||||
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
|
||||
<div>{{frameRate}}</div>
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,155 @@
|
||||
function getElementIfNotEmpty(viewportIndex) {
|
||||
// Meteor template helpers run more often than expected
|
||||
// They often seem to run just before the whole template is rendered
|
||||
// This meant that the onRendered event hadn't fired yet, so the
|
||||
// element wasn't enabled / set empty yet. The check here
|
||||
// for canvases under the 'enabled' element div is to prevent
|
||||
// 'undefined' errors from the helper functions
|
||||
|
||||
var imageViewerViewports = $('.imageViewerViewport'),
|
||||
element = imageViewerViewports.get(viewportIndex),
|
||||
canvases = imageViewerViewports.eq(viewportIndex).find('canvas');
|
||||
|
||||
if (!element || $(element).hasClass("empty") || canvases.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to make sure the element is enabled.
|
||||
try {
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function getPatient(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var patient = cornerstoneTools.metaData.get('patient', this.imageId);
|
||||
if (!patient) {
|
||||
return '';
|
||||
}
|
||||
return patient[property];
|
||||
}
|
||||
|
||||
function getStudy(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var study = cornerstoneTools.metaData.get('study', this.imageId);
|
||||
if (!study) {
|
||||
return '';
|
||||
}
|
||||
return study[property];
|
||||
}
|
||||
|
||||
function getSeries(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var series = cornerstoneTools.metaData.get('series', this.imageId);
|
||||
if (!series) {
|
||||
return '';
|
||||
}
|
||||
return series[property];
|
||||
}
|
||||
|
||||
function getInstance(property) {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
var instance = cornerstoneTools.metaData.get('instance', this.imageId);
|
||||
if (!instance) {
|
||||
return '';
|
||||
}
|
||||
return instance[property];
|
||||
}
|
||||
|
||||
function getImage(viewportIndex) {
|
||||
var element = getElementIfNotEmpty(viewportIndex);
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
var enabledElement;
|
||||
try {
|
||||
enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return false;
|
||||
}
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return false;
|
||||
}
|
||||
return enabledElement.image;
|
||||
}
|
||||
|
||||
Template.viewportOverlay.helpers({
|
||||
wwwc: function() {
|
||||
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
||||
var element = getElementIfNotEmpty(this.viewportIndex);
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) {
|
||||
return '';
|
||||
}
|
||||
return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
|
||||
},
|
||||
zoom: function() {
|
||||
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
||||
var element = getElementIfNotEmpty(this.viewportIndex);
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) {
|
||||
return '';
|
||||
}
|
||||
return (viewport.scale * 100.0);
|
||||
},
|
||||
imageDimensions: function() {
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
|
||||
var image = getImage(this.viewportIndex);
|
||||
if (!image) {
|
||||
return '';
|
||||
}
|
||||
return image.width + ' x ' + image.height;
|
||||
},
|
||||
patientName : function() {
|
||||
return getPatient.call(this, 'name');
|
||||
},
|
||||
patientId : function() {
|
||||
return getPatient.call(this, 'id');
|
||||
},
|
||||
studyDate : function() {
|
||||
return getStudy.call(this, 'date');
|
||||
},
|
||||
studyTime : function() {
|
||||
return getStudy.call(this, 'time');
|
||||
},
|
||||
studyDescription : function() {
|
||||
return getStudy.call(this, 'description');
|
||||
},
|
||||
seriesDescription : function() {
|
||||
return getSeries.call(this, 'description');
|
||||
},
|
||||
seriesNumber : function() {
|
||||
return getSeries.call(this, 'number');
|
||||
},
|
||||
imageNumber : function() {
|
||||
return getInstance.call(this, 'number');
|
||||
},
|
||||
imageIndex : function() {
|
||||
return getInstance.call(this, 'index');
|
||||
},
|
||||
numImages : function() {
|
||||
return getSeries.call(this, 'numImages');
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
.imageViewerViewportOverlay
|
||||
font-size: 13px
|
||||
color: #e4ad00
|
||||
line-height: 18px
|
||||
display: none // Shown when an image is loaded
|
||||
|
||||
.dicomTag
|
||||
pointer-events: none // Necessary for drag/drop through to cornerstone element below
|
||||
position: absolute
|
||||
|
||||
.topleft
|
||||
top: 3px
|
||||
left: 3px
|
||||
|
||||
.topright
|
||||
top: 3px
|
||||
right: 3px
|
||||
text-align: right
|
||||
|
||||
.bottomleft
|
||||
bottom: 3px
|
||||
left: 3px
|
||||
|
||||
.bottomright
|
||||
bottom: 3px
|
||||
right: 3px
|
||||
text-align: right
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user