Merged LesionTracker and OHIF Viewer, refactored out common components

This commit is contained in:
Erik Ziegler 2015-10-13 14:14:50 +02:00
parent 34adbe3fbc
commit b774c22593
144 changed files with 27820 additions and 0 deletions

View File

@ -0,0 +1,12 @@
# This file contains information which helps Meteor properly upgrade your
# app when you run 'meteor update'. You should check it into version control
# with your project.
notices-for-0.9.0
notices-for-0.9.1
0.9.4-platform-file
notices-for-facebook-graph-api-2
1.2.0-standard-minifiers-package
1.2.0-meteor-platform-split
1.2.0-cordova-changes
1.2.0-breaking-changes

1
LesionTracker/.meteor/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
local

View File

@ -0,0 +1,7 @@
# This file contains a token that is unique to your project.
# Check it into your repository along with the rest of this directory.
# It can be used for purposes such as:
# - ensuring you don't accidentally deploy one app on top of another
# - providing package authors with aggregated statistics
1q3vtk41m2yk4k1rqj08l

View File

@ -0,0 +1,34 @@
# Meteor packages used by this project, one per line.
# Check this file (and the other files in this directory) into your repository.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
meteor-base # Packages every Meteor app needs to have
mobile-experience # Packages for a great mobile UX
mongo # The database Meteor supports right now
blaze-html-templates # Compile .html files into Meteor Blaze views
session # Client-side reactive dictionary for your app
jquery # Helpful client-side library
tracker # Meteor's client-side reactive programming library
standard-minifiers # JS/CSS minifiers run for production mode
es5-shim # ECMAScript 5 compatibility for older browsers.
ecmascript # Enable ECMAScript2015+ syntax in app code
autopublish # Publish all data to the clients (for prototyping)
insecure # Allow all DB writes from clients (for prototyping)
cornerstone
worklist
viewerbase
dicomweb
accounts-password
ian:accounts-ui-bootstrap-3
iron:router
twbs:bootstrap
http
arsnebula:reactive-promise
promise
stylus
fortawesome:fontawesome

View File

@ -0,0 +1,2 @@
server
browser

View File

@ -0,0 +1 @@
METEOR@1.2.0.2

View File

@ -0,0 +1,96 @@
accounts-base@1.2.1
accounts-password@1.1.3
anti:i18n@0.4.3
arsnebula:reactive-promise@0.9.1
autopublish@1.0.4
autoupdate@1.2.3
babel-compiler@5.8.24_1
babel-runtime@0.1.4
base64@1.0.4
binary-heap@1.0.4
blaze@2.1.3
blaze-html-templates@1.0.1
blaze-tools@1.0.4
boilerplate-generator@1.0.4
caching-compiler@1.0.0
caching-html-compiler@1.0.2
callback-hook@1.0.4
check@1.0.6
cornerstone@0.0.1
ddp@1.2.2
ddp-client@1.2.1
ddp-common@1.2.1
ddp-rate-limiter@1.0.0
ddp-server@1.2.1
deps@1.0.9
dicomweb@0.0.1
diff-sequence@1.0.1
ecmascript@0.1.5
ecmascript-collections@0.1.6
ejson@1.0.7
email@1.0.7
es5-shim@4.1.13
fastclick@1.0.7
fortawesome:fontawesome@4.4.0
geojson-utils@1.0.4
hot-code-push@1.0.0
html-tools@1.0.5
htmljs@1.0.5
http@1.1.1
ian:accounts-ui-bootstrap-3@1.2.82
id-map@1.0.4
insecure@1.0.4
iron:controller@1.0.12
iron:core@1.0.11
iron:dynamic-template@1.0.12
iron:layout@1.0.12
iron:location@1.0.11
iron:middleware-stack@1.0.11
iron:router@1.0.12
iron:url@1.0.11
jquery@1.11.4
launch-screen@1.0.4
livedata@1.0.15
localstorage@1.0.5
logging@1.0.8
meteor@1.1.9
meteor-base@1.0.1
meteor-platform@1.2.3
minifiers@1.1.7
minimongo@1.0.10
mobile-experience@1.0.1
mobile-status-bar@1.0.6
mongo@1.1.2
mongo-id@1.0.1
npm-bcrypt@0.7.8_2
npm-mongo@1.4.39_1
observe-sequence@1.0.7
ordered-dict@1.0.4
promise@0.5.0
random@1.0.4
rate-limit@1.0.0
reactive-dict@1.1.2
reactive-var@1.0.6
reload@1.1.4
retry@1.0.4
routepolicy@1.0.6
service-configuration@1.0.5
session@1.1.1
sha@1.0.4
spacebars@1.0.7
spacebars-compiler@1.0.7
srp@1.0.4
standard-app-packages@1.0.6
standard-minifiers@1.0.1
stylus@2.511.0_2
templating@1.1.4
templating-tools@1.0.0
tracker@1.0.9
twbs:bootstrap@3.3.5
ui@1.0.8
underscore@1.0.4
url@1.0.5
viewerbase@0.0.1
webapp@1.2.2
webapp-hashing@1.0.5
worklist@0.0.1

View File

@ -0,0 +1,2 @@
echo "Starting Meteor server..."
PACKAGE_DIRS="../Packages" meteor --settings ../config/localhostOrthanc.json

View File

@ -0,0 +1 @@
PACKAGE_DIRS="../Packages" meteor --settings ../config/medicalConnections.json

View File

@ -0,0 +1 @@
PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json

View File

@ -0,0 +1 @@
PACKAGE_DIRS="../Packages" meteor --settings ../config/siimDCM4CHEE.json

View File

@ -0,0 +1,33 @@
//Collapse the container
function collapseContainer ( container ) {
container.toggleClass("collapseHorizontal");
}
//function add collapse button
function addButtonCollapse ( container ) {
var btnCollapse = document.createElement("button");
btnCollapse.innerHTML = ">";
btnCollapse.classList.add('btnCollapse');
btnCollapse.onclick = function() { // Note this is a function
collapseContainer(container);
};
container.append(btnCollapse);
}
$.fn.dockingContainer = function(options) {
var self = this;
var opts = $.extend( {}, $.fn.dockingContainer.defaults, options);
//Set container class
this.addClass("dockingContainer");
//Add btnCollapse
addButtonCollapse(this);
return this;
};
//Defaults
$.fn.dockingContainer.defaults = {
background: "black"
};

View File

@ -0,0 +1,285 @@
var lineIndex = 0; //This holds drawn line index
var cornerstoneTools = (function ($, cornerstone, cornerstoneMath, cornerstoneTools) {
"use strict";
if(cornerstoneTools === undefined) {
cornerstoneTools = {};
}
var toolType = "lesion";
///////// BEGIN ACTIVE TOOL ///////
function createNewMeasurement(mouseEventData)
{
var element = mouseEventData.element;
var lesionCounter = "";
//setting name of lesion
$(element).on("LesionNameSet", function(e,counter){
lesionCounter = counter;
});
//Listens LesionToolModified event and calls measurementModified function when lesion measurement is changed or updated.
$(element).on("LesionToolModified", measurementModified);
$(element).trigger("LesionMeasurementCreated");
// create the measurement data for this tool with the end handle activated
var measurementData = {
visible : true,
active : true,
handles : {
start : {
x : mouseEventData.currentPoints.image.x,
y : mouseEventData.currentPoints.image.y,
highlight: true,
active: false
},
end: {
x : mouseEventData.currentPoints.image.x,
y : mouseEventData.currentPoints.image.y,
highlight: true,
active: true
}
},
index: lineIndex,
measurementText: "",
linkedTextCoords: {
start : {
x : mouseEventData.currentPoints.image.x,
y : mouseEventData.currentPoints.image.y,
highlight: true,
active: false
},
end: {
x : mouseEventData.currentPoints.image.x,
y : mouseEventData.currentPoints.image.y,
highlight: true,
active: true
},
init: false
},
lesionName: "Target "+lesionCounter,
isDeleted: false,
lineNumber:"" //Indicates line number on image
};
lineIndex++;
return measurementData;
}
///////// END ACTIVE TOOL ///////
function pointNearTool(element, data, coords)
{
var lineSegment = {
start: cornerstone.pixelToCanvas(element, data.handles.start), end: cornerstone.pixelToCanvas(element, data.handles.end)
};
var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
return (distanceToPoint < 25);
}
function pointNearToolForText(element, data, coords) {
var lineSegment = {
start: cornerstone.pixelToCanvas(element, data.linkedTextCoords.start), end: cornerstone.pixelToCanvas(element, data.linkedTextCoords.end)
};
var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
return (distanceToPoint < 30);
}
///////// BEGIN IMAGE RENDERING ///////
function onImageRendered(e, eventData) {
// if we have no toolData for this element, return immediately as there is nothing to do
var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType);
if (toolData === undefined) {
return;
}
// we have tool data for this element - iterate over each one and draw it
var context = eventData.canvasContext.canvas.getContext('2d');
context.setTransform(1, 0, 0, 1, 0, 0);
var color;
var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
var font = cornerstoneTools.textStyle.getFont();
var config = cornerstoneTools.length.getConfiguration();
for (var i = 0; i < toolData.data.length; i++) {
context.save();
// configurable shadow
if (config && config.shadow) {
context.shadowColor = '#000000';
context.shadowOffsetX = +1;
context.shadowOffsetY = +1;
}
var data = toolData.data[i];
if (data.active) {
color = cornerstoneTools.toolColors.getActiveColor();
} else {
color = cornerstoneTools.toolColors.getToolColor();
}
// draw the line
var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.start);
var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.handles.end);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
context.lineTo(handleEndCanvas.x, handleEndCanvas.y);
context.stroke();
// draw the handles
cornerstoneTools.drawHandles(context, eventData, data.handles, color);
//Set coordinates of text
var linkedTextStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.start);
var linkedTextEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.end);
if(!data.linkedTextCoords.init){
data.linkedTextCoords.start.x = data.handles.start.x + 50;
data.linkedTextCoords.start.y = data.handles.start.y + 40;
linkedTextStartCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.start);
//Set end point of linkedTextCoords
data.linkedTextCoords.end.x = data.linkedTextCoords.start.x + 50;
data.linkedTextCoords.end.y = data.linkedTextCoords.start.y + 30;
linkedTextEndCanvas = cornerstone.pixelToCanvas(eventData.element, data.linkedTextCoords.end);
//initialized coordinates of text
data.linkedTextCoords.init = true;
}
//Draw linked line as dashed
context.setLineDash([2,3]);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = 1 / eventData.viewport.scale;
var mid = {
x:(handleStartCanvas.x + handleEndCanvas.x) / 2, y:(handleStartCanvas.y + handleEndCanvas.y) / 2
};
context.moveTo(mid.x, mid.y);
context.lineTo(linkedTextStartCanvas.x + 20, linkedTextStartCanvas.y + 20);
context.stroke();
// Draw the text
context.fillStyle = color;
context.font = font;
var dx = (data.handles.start.x - data.handles.end.x) * (eventData.image.columnPixelSpacing || 1);
var dy = (data.handles.start.y - data.handles.end.y) * (eventData.image.rowPixelSpacing || 1);
var length = Math.sqrt(dx * dx + dy * dy);
var suffix = ' mm';
if (!eventData.image.rowPixelSpacing || !eventData.image.columnPixelSpacing) {
suffix = ' pixels';
}
var text = '' + length.toFixed(2) + suffix;
var textCoords = {
x: linkedTextStartCanvas.x, y: linkedTextStartCanvas.y
};
cornerstoneTools.drawTextBox(context, data.lesionName, textCoords.x, textCoords.y, color);
cornerstoneTools.drawTextBox(context, text, textCoords.x, textCoords.y + 20, color);
//Set measurement text to show lesion table
data.measurementText = length.toFixed(2);
$(eventData.enabledElement.element).trigger("LesionTextChanged",data);
context.restore();
}
}
///////// END IMAGE RENDERING ///////
//This function is called from cornerstone-viewport.html and updates lesion measurement and makes the lesion active
function measurementModified(e, eventObject){
var start = new Date();
var enabledElement = eventObject.enabledElement;
var lineIndex = eventObject.lineIndex;
var type = eventObject.type;
// if we have no toolData for this element, return immediately as there is nothing to do
var toolData = cornerstoneTools.getToolState(e.currentTarget, toolType);
if (toolData === undefined) {
return;
}
//If type is delete, remove measurement
if(type === "delete") {
var deletedDataIndex = -1;
for (var i = 0; i < toolData.data.length; i++) {
var data = toolData.data[i];
//When click a row of table measurements, measurement will be active and color will be green
if (data.index === eventObject.lineIndex && eventObject.type !== "active") {
data.visible = false;
deletedDataIndex = i;
}
}
if (deletedDataIndex >= 0 && deletedDataIndex < toolData.data.length) {
toolData.data.splice(deletedDataIndex, 1);
}
} else {
for (var i = 0; i < toolData.data.length; i++) {
var data = toolData.data[i];
//When click a row of table measurements, measurement will be active and color will be green
if (data.index === eventObject.lineIndex && eventObject.type === "active") {
data.active = true;
} else{
data.active = false;
}
}
}
enabledElement.image.render(enabledElement, true);
var context = enabledElement.canvas.getContext('2d');
var end = new Date();
var diff = end - start;
//console.log(diff + ' ms');
var eventData = {
viewport : enabledElement.viewport,
element : enabledElement.element,
image : enabledElement.image,
enabledElement : enabledElement,
canvasContext: context,
measurementText: "",
renderTimeInMs : diff,
lineIndex : lineIndex,
type : type //Holds image will be deleted or active
};
enabledElement.invalid = false;
onImageRendered(e, eventData);
}
// module exports
cornerstoneTools.lesion = cornerstoneTools.mouseButtonTool({
createNewMeasurement : createNewMeasurement,
onImageRendered: onImageRendered,
pointNearTool : pointNearTool,
pointNearToolForText: pointNearToolForText,
toolType : toolType
});
cornerstoneTools.lesionTouch = cornerstoneTools.touchTool({
createNewMeasurement: createNewMeasurement,
onImageRendered: onImageRendered,
pointNearTool: pointNearTool,
pointNearToolForText: pointNearToolForText,
toolType: toolType
});
return cornerstoneTools;
}($, cornerstone, cornerstoneMath, cornerstoneTools));

View File

@ -0,0 +1,15 @@
//Holds functions to help managing tabs
//Returns activeTabId
function activeTabId() {
var activeTab = Session.get("activeTabId");
return activeTab;
}
//Returns activeTabContentId
function activeTabContentId () {
var activeTabId = activeTabId();
var splitTabId = activeTabId.split("tab");
var contentId = "content"+splitTabId[1];
return contentId;
}

View File

@ -0,0 +1,5 @@
<template name="hidingPanel">
<div class="hidingPanel">
{{> studyBrowser}}
</div>
</template>

View File

@ -0,0 +1,9 @@
.hidingPanel
width: 12%
height: 100%
margin-top: 35px
position: relative
float: left
overflow: auto
background-color: blue
color: black

View File

@ -0,0 +1,48 @@
.toolbar {
width: 100%;
height: 35px;
text-align: center;
}
#toolbar-group button {
text-align: center;
margin-top: 2px;
margin-right: 3px;
margin-left: 3px;
color: #ffffff;
background-color: black;
width: 30px;
height: 30px;
border: 1px solid white;
}
#toolbar-group button > span {
width: 12px;
height: 12px;
position: absolute;
top: 9px;
left: 9px;
right: 9px;
bottom: 9px;
}
#toolbar-group .btn:hover {
border-width: 3px;
background-color: transparent;
color: white;
}
#toolbar-group .btn:focus, .btn:active {
outline: 0 !important;
border-width: 5px;
background-color: transparent;
color:white;
text-align: center;
}
#toolbar-group .btn:focus > span{
top: 6px;
left: 6px;
right: 6px;
bottom: 6px;
}

View File

@ -0,0 +1,36 @@
<template name="toolbar">
<div id="toolbar" class="toolbar">
<div id="toolbar-group" class="btn-group">
<!-- WW/WL -->
<button id="wwwc" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="WW/WC"><span class="fa fa-sun-o"></span></button>
<!-- Invert -->
<button id="invert" type="button" class="imageViewerCommand btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Invert"><span class="fa fa-adjust"></span></button>
<!-- Zoom -->
<button id="zoom" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Zoom"><span class="fa fa-search"></span></button>
<!-- Pan -->
<button id="pan" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Pan"><span class="fa fa-arrows"></span></button>
<!-- Stack scroll -->
<button id="stackScroll" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Stack Scroll"><span class="fa fa-bars"></span></button>
<!-- Play clip -->
<button id="playClip" type="button" class="imageViewerCommand btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Play/Stop Clip">
{{ #if isPlaying }}
<span class="fa fa-stop"></span>
{{ else }}
<span class="fa fa-play"></span>
{{ /if }}
</button>
<!-- Angle measurement -->
<!--<button id="angle" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Angle Measurement"><span class="fa fa-angle-left"></span></button>-->
<!-- Pixel probe -->
<button id="dragProbe" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Pixel Probe"><span class="fa fa-dot-circle-o"></span></button>
<!-- Length measurement -->
<button id="length" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Length Measurement"><span class="fa fa-arrows-v"></span></button>
<!-- Elliptical ROI -->
<button id="ellipticalRoi" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Elliptical ROI"><span class="fa fa-circle-o"></span></button>
<!-- Rectangle ROI -->
<button id="rectangleRoi" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Rectangle ROI"><span class="fa fa-square-o"></span></button>
<!-- Lesion Tool -->
<button id="lesion" type="button" class="imageViewerTool btn btn-sm btn-toolbar" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Lesion Tool"><span class="fa fa-arrows-v"></span></button>
</div>
</div>
</template>

View File

@ -0,0 +1,34 @@
Template.toolbar.events({
'click .imageViewerTool': function(e) {
var tool = e.currentTarget.id;
console.log('Setting active tool to: ' + tool);
toolManager.setActiveTool(tool);
},
'click .imageViewerCommand': function(e) {
var command = e.currentTarget.id;
if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
return;
}
// TODO = Add support for active viewports
//var element = getActiveViewportElement();
$('.imageViewer').each(function() {
var element = this;
OHIF.viewer.functionList[command](element);
});
}
});
Template.toolbar.onRendered(function() {
var tooltipButtons = $('[data-toggle="tooltip"]');
tooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
// Enable tooltips for the layout button
var extraTooltipButtons = $('[rel="tooltip"]');
extraTooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
toolManager.addTool('lesion', {
mouse: cornerstoneTools.lesion,
touch: cornerstoneTools.lesionTouch
});
});

View File

@ -0,0 +1,7 @@
.viewer{
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background-color: black;
}

View File

@ -0,0 +1,9 @@
<template name="viewer">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no">
</head>
<div id="viewer" class="viewer">
{{ >hidingPanel }}
{{ >viewerMain }}
</div>
</template>

View File

@ -0,0 +1,89 @@
OHIF = {
viewer: {}
};
OHIF.viewer.imageViewerLoadedSeriesDictionary = {};
OHIF.viewer.imageViewerCurrentImageIdIndexDictionary = {};
OHIF.viewer.loadIndicatorDelay = 3000;
OHIF.viewer.defaultTool = 'wwwc';
OHIF.viewer.refLinesEnabled = true;
OHIF.viewer.isPlaying = {};
Meteor.startup(function() {
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
OHIF.viewer.functionList = {
invert: function(element) {
var viewport = cornerstone.getViewport(element);
viewport.invert = !viewport.invert;
cornerstone.setViewport(element, viewport);
},
playClip: function(element) {
var viewportIndex = $('.imageViewerViewport').index(element);
var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
if (isPlaying === true) {
cornerstoneTools.stopClip(element);
} else {
cornerstoneTools.playClip(element);
}
OHIF.viewer.isPlaying[viewportIndex] = !OHIF.viewer.isPlaying[viewportIndex];
Session.set('UpdateCINE', Random.id());
}
};
if (isTouchDevice()) {
OHIF.viewer.tooltipConfig = {
trigger: 'manual'
};
} else {
OHIF.viewer.tooltipConfig = {
trigger: 'hover'
};
}
});
function resizeViewports() {
// Handle resizing of image viewer viewports
// For some reason, this seems to need to be on
// another delay, or the resizing won't work properly
viewportResizeTimer = setTimeout(function() {
var elements = $('.imageViewerViewport');
elements.each(function(index) {
var element = this;
if (!element) {
return;
}
cornerstone.resize(element, true);
});
}, 1);
}
Session.setDefault('ActiveViewport', 0);
Session.setDefault('viewportRows', 1);
Session.setDefault('viewportColumns', 1);
// Avoid doing DOM manipulation during the resize handler
// because it is fired very often.
// Resizing is therefore performed 100 ms after the resize event stops.
var resizeTimer;
$(window).on('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
resizeViewports();
}, 100);
});
Template.viewer.onRendered(function() {
var imageViewer = $("#viewer");
if (imageViewer) {
$('.navbar-default').css({
'background-color': '#000000',
'border-color': '#101010'
});
document.body.style.overflow = "hidden";
document.body.style.height = '100%';
document.body.style.width = '100%';
document.body.style.minWidth = 0;
document.body.style.position = 'fixed'; // Prevent overscroll on mobile devices
}
});

View File

@ -0,0 +1,5 @@
.viewerMain {
width: 88%;
height: 100%;
margin-left: 12%;
}

View File

@ -0,0 +1,7 @@
<template name="viewerMain">
<div class="viewerMain">
{{> toolbar}}
{{> imageViewerViewports}}
<!--TODO: Bottom lesion table container will be here-->
</div>
</template>

View File

@ -0,0 +1,12 @@
<head>
<meta charset="utf-8">
<title>Lesion Tracker</title>
<meta name="description" content="Prototype viewer application for oncology">
<meta name="viewport" content="user-scalable=no, initial-scale=1, minimal-ui, maximum-scale=1, minimum-scale=1" />
<link rel="shortcut icon" type="image/png" href="images/logo.png" sizes="16x16 32x32 64x64">
<!--Best display for mobile devices-->
<meta http-equiv="cleartype" content="on">
<meta name="MobileOptimized" content="320">
<meta name="HandheldFriendly" content="True">
<meta name="apple-mobile-web-app-capable" content="yes">
</head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,34 @@
Router.configure({
layoutTemplate: 'layout',
//loadingTemplate: '',
notFoundTemplate: 'notFound'
});
Router.route('/', function () {
this.render('worklist', {
});
});
ViewerStudies = new Mongo.Collection(null);
Router.route('/viewer/:_id', {
layoutTemplate: 'layout',
name: 'viewer',
onBeforeAction: function() {
var self = this;
//Session.set('openNewTabEvent', self);
Meteor.call('GetStudyMetadata', this.params._id, function(error, study) {
//console.log(study);
sortStudy(study);
var studies = [study];
Session.set('studies', studies);
//Session.set(activeTabId, studies);
//Session.set('showContentInTab', true);
self.render('viewer');
});
}
});

View File

@ -0,0 +1,5 @@
Router.onBeforeAction(function () {
// User is logged in, go ahead and route them
this.next();
});

View File

@ -0,0 +1,52 @@
/*****************************/
/* Toggle */
/*****************************/
* {
box-sizing: border-box;
}
.dockingContainer, .collapseVertical, .collapseHorizontal {
-moz-transition: all 0.3s ease-out;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.dockingContainer {
-moz-transition: all 0.3s ease-out;
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.collapseVertical {
opacity: 0;
height: 0;
border: none;
}
.collapseHorizontal {
width: 30px;
}
.btnCollapse{
position: absolute;
top: 0;
left: 0;
display: block;
width: 25px;
height: 25px;
line-height: 25px;
padding: 0;
border: 0;
border-top: 1px solid #e6e6e6;
background: #f6f6f6;
color: #666;
font-size: 0.875rem;
text-align: center;
cursor: pointer;
z-index: 100;
}
.btnCollapseIcon-collapse{
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}

3
OHIFViewer/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.meteor/local
.meteor/meteorite
node_modules

View File

@ -0,0 +1,12 @@
# This file contains information which helps Meteor properly upgrade your
# app when you run 'meteor update'. You should check it into version control
# with your project.
notices-for-0.9.0
notices-for-0.9.1
0.9.4-platform-file
notices-for-facebook-graph-api-2
1.2.0-standard-minifiers-package
1.2.0-meteor-platform-split
1.2.0-cordova-changes
1.2.0-breaking-changes

1
OHIFViewer/.meteor/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
local

7
OHIFViewer/.meteor/.id Normal file
View File

@ -0,0 +1,7 @@
# This file contains a token that is unique to your project.
# Check it into your repository along with the rest of this directory.
# It can be used for purposes such as:
# - ensuring you don't accidentally deploy one app on top of another
# - providing package authors with aggregated statistics
1jfkhvr1ljfqodmpn32m

View File

@ -0,0 +1,34 @@
# Meteor packages used by this project, one per line.
# Check this file (and the other files in this directory) into your repository.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password
ian:accounts-ui-bootstrap-3
iron:router
twbs:bootstrap
http
arsnebula:reactive-promise
promise
stylus
fortawesome:fontawesome
mrt:moment
standard-minifiers
meteor-base
mobile-experience
mongo
blaze-html-templates
session
jquery
tracker
logging
reload
random
ejson
spacebars
check
cornerstone
dicomweb
viewerbase
worklist

View File

@ -0,0 +1,2 @@
server
browser

View File

@ -0,0 +1 @@
METEOR@1.2.0.2

View File

@ -0,0 +1,94 @@
accounts-base@1.2.1
accounts-password@1.1.3
anti:i18n@0.4.3
arsnebula:reactive-promise@0.9.1
autoupdate@1.2.3
babel-compiler@5.8.24_1
babel-runtime@0.1.4
base64@1.0.4
binary-heap@1.0.4
blaze@2.1.3
blaze-html-templates@1.0.1
blaze-tools@1.0.4
boilerplate-generator@1.0.4
caching-compiler@1.0.0
caching-html-compiler@1.0.2
callback-hook@1.0.4
check@1.0.6
cornerstone@0.0.1
ddp@1.2.2
ddp-client@1.2.1
ddp-common@1.2.1
ddp-rate-limiter@1.0.0
ddp-server@1.2.1
deps@1.0.9
dicomweb@0.0.1
diff-sequence@1.0.1
ecmascript@0.1.5
ecmascript-collections@0.1.6
ejson@1.0.7
email@1.0.7
fastclick@1.0.7
fortawesome:fontawesome@4.4.0
geojson-utils@1.0.4
hot-code-push@1.0.0
html-tools@1.0.5
htmljs@1.0.5
http@1.1.1
ian:accounts-ui-bootstrap-3@1.2.81
id-map@1.0.4
iron:controller@1.0.8
iron:core@1.0.8
iron:dynamic-template@1.0.8
iron:layout@1.0.8
iron:location@1.0.9
iron:middleware-stack@1.0.9
iron:router@1.0.9
iron:url@1.0.9
jquery@1.11.4
launch-screen@1.0.4
livedata@1.0.15
localstorage@1.0.5
logging@1.0.8
meteor@1.1.9
meteor-base@1.0.1
meteor-platform@1.2.3
minifiers@1.1.7
minimongo@1.0.10
mobile-experience@1.0.1
mobile-status-bar@1.0.6
mongo@1.1.2
mongo-id@1.0.1
mrt:moment@2.8.1
npm-bcrypt@0.7.8_2
npm-mongo@1.4.39_1
observe-sequence@1.0.7
ordered-dict@1.0.4
promise@0.5.0
random@1.0.4
rate-limit@1.0.0
reactive-dict@1.1.2
reactive-var@1.0.6
reload@1.1.4
retry@1.0.4
routepolicy@1.0.6
service-configuration@1.0.5
session@1.1.1
sha@1.0.4
spacebars@1.0.7
spacebars-compiler@1.0.7
srp@1.0.4
standard-app-packages@1.0.6
standard-minifiers@1.0.1
stylus@2.511.0_2
templating@1.1.4
templating-tools@1.0.0
tracker@1.0.9
twbs:bootstrap@3.3.5
ui@1.0.8
underscore@1.0.4
url@1.0.5
viewerbase@0.0.1
webapp@1.2.2
webapp-hashing@1.0.5
worklist@0.0.1

View File

@ -0,0 +1,2 @@
echo "Starting Meteor server..."
PACKAGE_DIRS="../Packages" meteor --settings ../config/localhostOrthanc.json

View File

@ -0,0 +1 @@
PACKAGE_DIRS="../Packages" meteor --settings ../config/medicalConnections.json

View File

@ -0,0 +1 @@
PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json

1
OHIFViewer/bin/siimDCM4CHEE.sh Executable file
View File

@ -0,0 +1 @@
PACKAGE_DIRS="../Packages" meteor --settings ../config/siimDCM4CHEE.json

View File

@ -0,0 +1,36 @@
<template name="toolbar">
<div id='toolbar'>
<div class="btn-group">
<!-- WW/WL -->
<button id="wwwc" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="WW/WC"><span class="fa fa-sun-o"></span></button>
<!-- Invert -->
<button id="invert" type="button" class="imageViewerCommand btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Invert"><span class="fa fa-adjust"></span></button>
<!-- Zoom -->
<button id="zoom" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Zoom"><span class="fa fa-search"></span></button>
<!-- Pan -->
<button id="pan" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Pan"><span class="fa fa-arrows"></span></button>
<!-- Stack scroll -->
<button id="stackScroll" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Stack Scroll"><span class="fa fa-bars"></span></button>
<!-- Length measurement -->
<button id="length" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Length Measurement"><span class="fa fa-arrows-v"></span></button>
<!-- Angle measurement -->
<button id="angle" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Angle Measurement"><span class="fa fa-angle-left"></span></button>
<!-- Pixel probe -->
<button id="dragProbe" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Pixel Probe"><span class="fa fa-dot-circle-o"></span></button>
<!-- Elliptical ROI -->
<button id="ellipticalRoi" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Elliptical ROI"><span class="fa fa-circle-o"></span></button>
<!-- Rectangle ROI -->
<button id="rectangleRoi" type="button" class="imageViewerTool btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Rectangle ROI"><span class="fa fa-square-o"></span></button>
<!-- Play clip -->
<button id="playClip" type="button" class="imageViewerCommand btn btn-sm btn-default" data-container="body" data-toggle="tooltip" data-placement="bottom" title="Play/Stop Clip">
{{ #if isPlaying }}
<span class="fa fa-stop"></span>
{{ else }}
<span class="fa fa-play"></span>
{{ /if }}
</button>
<button id="layout" type="button" class="btn btn-sm btn-default dropdown-toggle" data-container="body" data-toggle="dropdown" aria-expanded="false" data-placement="right" title="Layout" rel="tooltip"><span class="fa fa-th-large"></span></button>
{{> layoutChooser}}
</div>
</div>
</template>

View File

@ -0,0 +1,40 @@
Template.toolbar.helpers({
studies : function() {
var studies = Session.get('studies');
return studies;
}
});
Template.toolbar.events({
'click .imageViewerTool': function(e) {
var tool = e.currentTarget.id;
console.log('Setting active tool to: ' + tool);
toolManager.setActiveTool(tool);
},
'click .imageViewerCommand': function(e) {
var command = e.currentTarget.id;
if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
return;
}
var viewportIndex = Session.get('ActiveViewport');
var element = $('.imageViewerViewport').get(viewportIndex);
OHIF.viewer.functionList[command](element);
}
});
Template.toolbar.onRendered(function() {
var tooltipButtons = $('[data-toggle="tooltip"]');
tooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
// Enable tooltips for the layout button
var extraTooltipButtons = $('[rel="tooltip"]');
extraTooltipButtons.tooltip(OHIF.viewer.tooltipConfig);
});
Template.toolbar.helpers({
'isPlaying': function() {
Session.get('UpdateCINE');
var viewportIndex = Session.get('ActiveViewport');
return !!OHIF.viewer.isPlaying[viewportIndex];
}
});

View File

@ -0,0 +1,15 @@
#toolbar
background-color: black
text-align: center
.btn-group
display: inline-block
button
text-align: center
color: #ffffff
background-color: #424242
border-color: #424242
&.active, &:active
background-color: #3B678E

View File

@ -0,0 +1,9 @@
<template name="viewer">
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no">
</head>
<div id="viewer">
{{> studyBrowser}}
{{> viewerMain}}
</div>
</template>

View File

@ -0,0 +1,89 @@
OHIF = {
viewer: {}
};
OHIF.viewer.imageViewerLoadedSeriesDictionary = {};
OHIF.viewer.imageViewerCurrentImageIdIndexDictionary = {};
OHIF.viewer.loadIndicatorDelay = 3000;
OHIF.viewer.defaultTool = 'wwwc';
OHIF.viewer.refLinesEnabled = true;
OHIF.viewer.isPlaying = {};
Meteor.startup(function() {
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer("CornerstoneNewImage", cornerstoneTools.updateImageSynchronizer);
OHIF.viewer.functionList = {
invert: function(element) {
var viewport = cornerstone.getViewport(element);
viewport.invert = !viewport.invert;
cornerstone.setViewport(element, viewport);
},
playClip: function(element) {
var viewportIndex = $('.imageViewerViewport').index(element);
var isPlaying = OHIF.viewer.isPlaying[viewportIndex] || false;
if (isPlaying === true) {
cornerstoneTools.stopClip(element);
} else {
cornerstoneTools.playClip(element);
}
OHIF.viewer.isPlaying[viewportIndex] = !OHIF.viewer.isPlaying[viewportIndex];
Session.set('UpdateCINE', Random.id());
}
};
if (isTouchDevice()) {
OHIF.viewer.tooltipConfig = {
trigger: 'manual'
};
} else {
OHIF.viewer.tooltipConfig = {
trigger: 'hover'
};
}
});
function resizeViewports() {
// Handle resizing of image viewer viewports
// For some reason, this seems to need to be on
// another delay, or the resizing won't work properly
viewportResizeTimer = setTimeout(function() {
var elements = $('.imageViewerViewport');
elements.each(function(index) {
var element = this;
if (!element) {
return;
}
cornerstone.resize(element, true);
});
}, 1);
}
Session.setDefault('ActiveViewport', 0);
Session.setDefault('viewportRows', 1);
Session.setDefault('viewportColumns', 1);
// Avoid doing DOM manipulation during the resize handler
// because it is fired very often.
// Resizing is therefore performed 100 ms after the resize event stops.
var resizeTimer;
$(window).on('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
resizeViewports();
}, 100);
});
Template.viewer.onRendered(function() {
var imageViewer = $("#viewer");
if (imageViewer) {
$('.navbar-default').css({
'background-color': '#000000',
'border-color': '#101010'
});
document.body.style.overflow = "hidden";
document.body.style.height = '100%';
document.body.style.width = '100%';
document.body.style.minWidth = 0;
document.body.style.position = 'fixed'; // Prevent overscroll on mobile devices
}
});

View File

@ -0,0 +1,12 @@
#viewer
width: 100%
height: calc(100% - 53px)
border-top: 1px solid #525252
.noselect
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
user-select: none

View File

@ -0,0 +1,6 @@
<template name="viewerMain">
<div class='viewerMain'>
{{ > toolbar }}
{{ > imageViewerViewports }}
</div>
</template>

View File

@ -0,0 +1,6 @@
Template.viewerMain.helpers({
studies : function() {
var studies = Session.get('studies');
return studies;
}
});

View File

@ -0,0 +1,5 @@
.viewerMain
float: left
background-color: gray
height: 100%
width: calc(100% - 120px)

View File

@ -0,0 +1,5 @@
Router.onBeforeAction(function () {
// User is logged in, go ahead and route them
this.next();
});

View File

@ -0,0 +1,33 @@
Router.configure({
layoutTemplate: 'layout',
//loadingTemplate: '',
notFoundTemplate: 'notFound'
});
Router.route('/', function () {
this.render('worklist', {
});
});
ViewerStudies = new Mongo.Collection(null);
Router.route('/viewer/:_id', {
layoutTemplate: 'layout',
name: 'viewer',
onBeforeAction: function() {
var self = this;
Meteor.call('GetStudyMetadata', this.params._id, function(error, study) {
//console.log(study);
sortStudy(study);
var studies = [study];
Session.set('studies', studies);
self.render('viewer');
});
}
});

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

View File

@ -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

View File

@ -0,0 +1,27 @@
Package.describe({
name: "cornerstone",
summary: "Cornerstone Web-based Medical Imaging libraries",
version: '0.0.1'
});
Package.onUse(function (api) {
api.versionsFrom('1.2.0.2');
api.use('jquery');
api.addFiles('client/cornerstone.js', 'client', {bare: true});
api.addFiles('client/cornerstoneMath.js', 'client', {bare: true});
api.addFiles('client/cornerstoneTools.js', 'client', {bare: true});
api.addFiles('client/cornerstoneWADOImageLoader.js', 'client', {bare: true});
api.addFiles('client/cornerstoneWADORSImageLoader.js', 'client', {bare: true});
api.addFiles('client/dicomParser.js', 'client', {bare: true});
api.addFiles('client/hammer.js', 'client', {bare: true});
api.export("cornerstone", 'client');
api.export("cornerstoneMath", 'client');
api.export("cornerstoneTools", 'client');
api.export("cornerstoneWADOImageLoader", 'client');
api.export("cornerstoneWADORSImageLoader", 'client');
api.export("dicomParser", 'client');
});

View File

@ -0,0 +1,17 @@
Package.describe({
name: "dicomweb",
summary: "DICOM Web Helper Functions",
version: '0.0.1'
});
Package.onUse(function (api) {
api.addFiles('server/namespace.js', 'server');
api.addFiles('server/getImageFrame.js', 'server');
api.addFiles('server/getJSON.js', 'server');
api.addFiles('server/getName.js', 'server');
api.addFiles('server/getNumber.js', 'server');
api.addFiles('server/getString.js', 'server');
api.export("DICOMWeb", 'server');
});

View File

@ -0,0 +1,70 @@
function findBoundary(header) {
for(var i=0; i < header.length; i++) {
if(header[i].substr(0,2) === '--') {
return header[i];
}
}
return undefined;
}
function findContentType(header) {
for(var i=0; i < header.length; i++) {
if(header[i].substr(0,13) === 'Content-Type:') {
return header[i].substr(13).trim();
}
}
return undefined;
}
DICOMWeb.getImageFrame = function(uri, mediaType) {
mediaType = mediaType || 'application/octet-stream';
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.responseType = "arraybuffer";
xhr.open("get", uri, true);
xhr.setRequestHeader('Accept', 'multipart/related;type=' + mediaType);
xhr.onreadystatechange = function (oEvent) {
// TODO: consider sending out progress messages here as we receive the pixel data
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// request succeeded, Parse the multi-part mime response
var imageFrameAsArrayBuffer = xhr.response;
var response = new Uint8Array(xhr.response);
// First look for the multipart mime header
var tokenIndex = findIndexOfString(response, '\n\r\n');
if(tokenIndex === -1) {
reject('invalid response - no multipart mime header');
}
var header = uint8ArrayToString(response, 0, tokenIndex);
// Now find the boundary marker
var split = header.split('\r\n');
var boundary = findBoundary(split);
if(!boundary) {
reject('invalid response - no boundary marker');
}
var offset = tokenIndex + 4; // skip over the \n\r\n
// find the terminal boundary marker
var endIndex = findIndexOfString(response, boundary, offset);
if(endIndex === -1) {
reject('invalid response - terminating boundary not found');
}
// return the info for this pixel data
var length = endIndex - offset - 1;
resolve({
contentType: findContentType(split),
arrayBuffer: imageFrameAsArrayBuffer,
offset: offset,
length: length
});
}
else {
// request failed, reject the deferred
reject(xhr.response);
}
}
};
xhr.send();
});
};

View File

@ -0,0 +1,31 @@
DICOMWeb.getJSON = function(url, options) {
var getOptions = {
headers: {
'Accept' : 'application/json'
},
};
if (options.auth) {
getOptions.auth = options.auth;
}
if (options.logRequests) {
console.log(url);
}
if (options.logTiming) {
console.time(url);
}
var result = HTTP.get(url, getOptions);
if (options.logTiming) {
console.timeEnd(url);
}
if (options.logResponses) {
console.log(result.data);
}
return result;
};

View File

@ -0,0 +1,26 @@
/**
* Returns the Alphabetic version of a PN
*
* @param element - The group/element of the element (e.g. '00200013')
* @param [defaultValue] - The default value to return if the element is not found
* @returns {*}
*/
DICOMWeb.getName = function(element, defaultValue) {
if(!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if(!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if(!element.Value.length) {
return defaultValue;
}
// Return the Alphabetic component group
if(element.Value[0].Alphabetic) {
return element.Value[0].Alphabetic;
}
// Orthanc does not return PN properly so this is a temporary workaround
return element.Value[0];
};

View File

@ -0,0 +1,20 @@
/**
* Returns the first string value as a Javascript Number
* @param element - The group/element of the element (e.g. '00200013')
* @param [defaultValue] - The default value to return if the element does not exist
* @returns {*}
*/
DICOMWeb.getNumber = function(element, defaultValue) {
if(!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if(!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if(!element.Value.length) {
return defaultValue;
}
return parseFloat(element.Value[0]);
};

View File

@ -0,0 +1,23 @@
/**
* Returns the specified element as a string. Multi-valued elements will be separated by a backslash
*
* @param element - The group/element of the element (e.g. '00200013')
* @param [defaultValue] - The value to return if the element is not present
* @returns {*}
*/
DICOMWeb.getString = function(element, defaultValue) {
if(!element) {
return defaultValue;
}
// Value is not present if the attribute has a zero length value
if(!element.Value) {
return defaultValue;
}
// Sanity check to make sure we have at least one entry in the array.
if(!element.Value.length) {
return defaultValue;
}
// Join the array together separated by backslash
// NOTE: Orthanc does not correctly split values into an array so the join is a no-op
return element.Value.join('\\');
};

View File

@ -0,0 +1 @@
DICOMWeb = {};

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,3 @@
<template name="login">
{{> atForm}}
</template>

View File

@ -0,0 +1,3 @@
<template name="notFound">
Not found
</template>

View File

@ -0,0 +1,4 @@
<template name="imageThumbnail">
<div class="imageThumbnail">
</div>
</template>

View File

@ -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);
});
});

View File

@ -0,0 +1,5 @@
.imageThumbnail
width: 90px
height: 90px
background-color: black
margin: 0 auto

View File

@ -0,0 +1,7 @@
<template name="studyBrowser">
<div class="studyBrowser">
{{#each studies}}
{{> thumbnails}}
{{/each}}
</div>
</template>

View File

@ -0,0 +1,6 @@
Template.studyBrowser.helpers({
studies : function() {
var studies = Session.get('studies');
return studies;
}
});

View File

@ -0,0 +1,6 @@
.studyBrowser
float: left
height: 100%
width: 120px
overflow: auto
background-color: black

View File

@ -0,0 +1,6 @@
<template name="thumbnailEntry">
<div class="thumbnailEntry">
{{ > imageThumbnail }}
<div class='seriesDescription noselect'>{{seriesDescription}}</div>
</div>
</template>

View File

@ -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;
}
});

View File

@ -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

View File

@ -0,0 +1,5 @@
<template name="thumbnails">
{{#each thumbnails}}
{{ >thumbnailEntry }}
{{/each}}
</template>

View File

@ -0,0 +1,6 @@
Template.thumbnails.helpers({
thumbnails: function() {
var stacks = createStacks(this);
return stacks;
}
});

View File

@ -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>

View File

@ -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();
},
});

View File

@ -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

View File

@ -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>

View File

@ -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;
}
});

View File

@ -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

View File

@ -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>

View File

@ -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);
}
});

View File

@ -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

View File

@ -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>

View File

@ -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);
}
};

View File

@ -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

View File

@ -0,0 +1,8 @@
<template name="viewportOrientationMarkers">
<div class="viewportOrientationMarkers noselect">
<div class="topMid orientationMarker">
</div>
<div class="leftMid orientationMarker">
</div>
</div>
</template>

View File

@ -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);
}
};

View File

@ -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

View File

@ -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>

View File

@ -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');
}
});

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More