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,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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
local
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
server
|
||||||
|
browser
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
METEOR@1.2.0.2
|
||||||
@@ -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
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
echo "Starting Meteor server..."
|
||||||
|
PACKAGE_DIRS="../Packages" meteor --settings ../config/localhostOrthanc.json
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
PACKAGE_DIRS="../Packages" meteor --settings ../config/medicalConnections.json
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
PACKAGE_DIRS="../Packages" meteor --settings ../config/siimDCM4CHEE.json
|
||||||
@@ -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"
|
||||||
|
};
|
||||||
@@ -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));
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template name="hidingPanel">
|
||||||
|
<div class="hidingPanel">
|
||||||
|
{{> studyBrowser}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.hidingPanel
|
||||||
|
width: 12%
|
||||||
|
height: 100%
|
||||||
|
margin-top: 35px
|
||||||
|
position: relative
|
||||||
|
float: left
|
||||||
|
overflow: auto
|
||||||
|
background-color: blue
|
||||||
|
color: black
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.viewer{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.viewerMain {
|
||||||
|
width: 88%;
|
||||||
|
height: 100%;
|
||||||
|
margin-left: 12%;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template name="viewerMain">
|
||||||
|
<div class="viewerMain">
|
||||||
|
{{> toolbar}}
|
||||||
|
{{> imageViewerViewports}}
|
||||||
|
<!--TODO: Bottom lesion table container will be here-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -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 |
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Router.onBeforeAction(function () {
|
||||||
|
|
||||||
|
// User is logged in, go ahead and route them
|
||||||
|
this.next();
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.meteor/local
|
||||||
|
.meteor/meteorite
|
||||||
|
node_modules
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
local
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
server
|
||||||
|
browser
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
METEOR@1.2.0.2
|
||||||
@@ -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
|
||||||
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
echo "Starting Meteor server..."
|
||||||
|
PACKAGE_DIRS="../Packages" meteor --settings ../config/localhostOrthanc.json
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
PACKAGE_DIRS="../Packages" meteor --settings ../config/medicalConnections.json
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
PACKAGE_DIRS="../Packages" meteor --settings ../config/medkenOrthanc.json
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
PACKAGE_DIRS="../Packages" meteor --settings ../config/siimDCM4CHEE.json
|
||||||
@@ -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>
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<template name="viewerMain">
|
||||||
|
<div class='viewerMain'>
|
||||||
|
{{ > toolbar }}
|
||||||
|
{{ > imageViewerViewports }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Template.viewerMain.helpers({
|
||||||
|
studies : function() {
|
||||||
|
var studies = Session.get('studies');
|
||||||
|
return studies;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.viewerMain
|
||||||
|
float: left
|
||||||
|
background-color: gray
|
||||||
|
height: 100%
|
||||||
|
width: calc(100% - 120px)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Router.onBeforeAction(function () {
|
||||||
|
|
||||||
|
// User is logged in, go ahead and route them
|
||||||
|
this.next();
|
||||||
|
});
|
||||||
@@ -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
@@ -0,0 +1,77 @@
|
|||||||
|
cornerstoneWADORSImageLoader = {
|
||||||
|
internal : {
|
||||||
|
nextIndex: 0,
|
||||||
|
imageIds : []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cornerstoneWADORSImageLoader.addImage = function(image) {
|
||||||
|
var index = cornerstoneWADORSImageLoader.internal.nextIndex++;
|
||||||
|
cornerstoneWADORSImageLoader.internal.imageIds[index] = image;
|
||||||
|
var imageId = 'wadors:' + index;
|
||||||
|
return imageId;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMinMax(storedPixelData)
|
||||||
|
{
|
||||||
|
// we always calculate the min max values since they are not always
|
||||||
|
// present in DICOM and we don't want to trust them anyway as cornerstone
|
||||||
|
// depends on us providing reliable values for these
|
||||||
|
var min = 65535;
|
||||||
|
var max = -32768;
|
||||||
|
var numPixels = storedPixelData.length;
|
||||||
|
var pixelData = storedPixelData;
|
||||||
|
for(var index = 0; index < numPixels; index++) {
|
||||||
|
var spv = pixelData[index];
|
||||||
|
// TODO: test to see if it is faster to use conditional here rather than calling min/max functions
|
||||||
|
min = Math.min(min, spv);
|
||||||
|
max = Math.max(max, spv);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
min: min,
|
||||||
|
max: max
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cornerstone.registerImageLoader('wadors', function(imageId) {
|
||||||
|
var index = imageId.substring(7);
|
||||||
|
var image = cornerstoneWADORSImageLoader.internal.imageIds[index];
|
||||||
|
|
||||||
|
var deferred = $.Deferred();
|
||||||
|
|
||||||
|
var mediaType;// = 'image/dicom+jp2';
|
||||||
|
|
||||||
|
DICOMWeb.getImageFrame(image.uri, mediaType).then(function(result) {
|
||||||
|
//console.log(result);
|
||||||
|
// TODO: add support for retrieving compressed pixel data
|
||||||
|
var storedPixelData;
|
||||||
|
if(image.instance.bitsAllocated === 16) {
|
||||||
|
if(image.instance.pixelRepresentation === 0) {
|
||||||
|
storedPixelData = new Uint16Array(result.arrayBuffer, result.offset, result.length / 2);
|
||||||
|
} else {
|
||||||
|
storedPixelData = new Int16Array(result.arrayBuffer, result.offset, result.length / 2);
|
||||||
|
}
|
||||||
|
} else if(image.instance.bitsAllocated === 8) {
|
||||||
|
storedPixelData = new Uint8Array(result.arrayBuffer, result.offset, result.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle various color space conversions
|
||||||
|
|
||||||
|
var minMax = getMinMax(storedPixelData);
|
||||||
|
image.imageId = imageId;
|
||||||
|
image.minPixelValue = minMax.min;
|
||||||
|
image.maxPixelValue = minMax.max;
|
||||||
|
image.render = cornerstone.renderGrayscaleImage;
|
||||||
|
image.getPixelData = function() {
|
||||||
|
return storedPixelData;
|
||||||
|
};
|
||||||
|
//console.log(image);
|
||||||
|
deferred.resolve(image);
|
||||||
|
}).catch(function(reason) {
|
||||||
|
deferred.reject(reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred;
|
||||||
|
});
|
||||||
|
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,27 @@
|
|||||||
|
Package.describe({
|
||||||
|
name: "cornerstone",
|
||||||
|
summary: "Cornerstone Web-based Medical Imaging libraries",
|
||||||
|
version: '0.0.1'
|
||||||
|
});
|
||||||
|
|
||||||
|
Package.onUse(function (api) {
|
||||||
|
api.versionsFrom('1.2.0.2');
|
||||||
|
|
||||||
|
api.use('jquery');
|
||||||
|
|
||||||
|
api.addFiles('client/cornerstone.js', 'client', {bare: true});
|
||||||
|
api.addFiles('client/cornerstoneMath.js', 'client', {bare: true});
|
||||||
|
api.addFiles('client/cornerstoneTools.js', 'client', {bare: true});
|
||||||
|
api.addFiles('client/cornerstoneWADOImageLoader.js', 'client', {bare: true});
|
||||||
|
api.addFiles('client/cornerstoneWADORSImageLoader.js', 'client', {bare: true});
|
||||||
|
api.addFiles('client/dicomParser.js', 'client', {bare: true});
|
||||||
|
api.addFiles('client/hammer.js', 'client', {bare: true});
|
||||||
|
|
||||||
|
api.export("cornerstone", 'client');
|
||||||
|
api.export("cornerstoneMath", 'client');
|
||||||
|
api.export("cornerstoneTools", 'client');
|
||||||
|
api.export("cornerstoneWADOImageLoader", 'client');
|
||||||
|
api.export("cornerstoneWADORSImageLoader", 'client');
|
||||||
|
api.export("dicomParser", 'client');
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Package.describe({
|
||||||
|
name: "dicomweb",
|
||||||
|
summary: "DICOM Web Helper Functions",
|
||||||
|
version: '0.0.1'
|
||||||
|
});
|
||||||
|
|
||||||
|
Package.onUse(function (api) {
|
||||||
|
api.addFiles('server/namespace.js', 'server');
|
||||||
|
api.addFiles('server/getImageFrame.js', 'server');
|
||||||
|
api.addFiles('server/getJSON.js', 'server');
|
||||||
|
api.addFiles('server/getName.js', 'server');
|
||||||
|
api.addFiles('server/getNumber.js', 'server');
|
||||||
|
api.addFiles('server/getString.js', 'server');
|
||||||
|
|
||||||
|
api.export("DICOMWeb", 'server');
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
function findBoundary(header) {
|
||||||
|
for(var i=0; i < header.length; i++) {
|
||||||
|
if(header[i].substr(0,2) === '--') {
|
||||||
|
return header[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findContentType(header) {
|
||||||
|
for(var i=0; i < header.length; i++) {
|
||||||
|
if(header[i].substr(0,13) === 'Content-Type:') {
|
||||||
|
return header[i].substr(13).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
DICOMWeb.getImageFrame = function(uri, mediaType) {
|
||||||
|
mediaType = mediaType || 'application/octet-stream';
|
||||||
|
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.responseType = "arraybuffer";
|
||||||
|
xhr.open("get", uri, true);
|
||||||
|
xhr.setRequestHeader('Accept', 'multipart/related;type=' + mediaType);
|
||||||
|
xhr.onreadystatechange = function (oEvent) {
|
||||||
|
// TODO: consider sending out progress messages here as we receive the pixel data
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
// request succeeded, Parse the multi-part mime response
|
||||||
|
var imageFrameAsArrayBuffer = xhr.response;
|
||||||
|
var response = new Uint8Array(xhr.response);
|
||||||
|
// First look for the multipart mime header
|
||||||
|
var tokenIndex = findIndexOfString(response, '\n\r\n');
|
||||||
|
if(tokenIndex === -1) {
|
||||||
|
reject('invalid response - no multipart mime header');
|
||||||
|
}
|
||||||
|
var header = uint8ArrayToString(response, 0, tokenIndex);
|
||||||
|
// Now find the boundary marker
|
||||||
|
var split = header.split('\r\n');
|
||||||
|
var boundary = findBoundary(split);
|
||||||
|
if(!boundary) {
|
||||||
|
reject('invalid response - no boundary marker');
|
||||||
|
}
|
||||||
|
var offset = tokenIndex + 4; // skip over the \n\r\n
|
||||||
|
|
||||||
|
// find the terminal boundary marker
|
||||||
|
var endIndex = findIndexOfString(response, boundary, offset);
|
||||||
|
if(endIndex === -1) {
|
||||||
|
reject('invalid response - terminating boundary not found');
|
||||||
|
}
|
||||||
|
// return the info for this pixel data
|
||||||
|
var length = endIndex - offset - 1;
|
||||||
|
resolve({
|
||||||
|
contentType: findContentType(split),
|
||||||
|
arrayBuffer: imageFrameAsArrayBuffer,
|
||||||
|
offset: offset,
|
||||||
|
length: length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// request failed, reject the deferred
|
||||||
|
reject(xhr.response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
DICOMWeb.getJSON = function(url, options) {
|
||||||
|
var getOptions = {
|
||||||
|
headers: {
|
||||||
|
'Accept' : 'application/json'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.auth) {
|
||||||
|
getOptions.auth = options.auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.logRequests) {
|
||||||
|
console.log(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.logTiming) {
|
||||||
|
console.time(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = HTTP.get(url, getOptions);
|
||||||
|
|
||||||
|
if (options.logTiming) {
|
||||||
|
console.timeEnd(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.logResponses) {
|
||||||
|
console.log(result.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Returns the Alphabetic version of a PN
|
||||||
|
*
|
||||||
|
* @param element - The group/element of the element (e.g. '00200013')
|
||||||
|
* @param [defaultValue] - The default value to return if the element is not found
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
DICOMWeb.getName = function(element, defaultValue) {
|
||||||
|
if(!element) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
// Value is not present if the attribute has a zero length value
|
||||||
|
if(!element.Value) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
// Sanity check to make sure we have at least one entry in the array.
|
||||||
|
if(!element.Value.length) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
// Return the Alphabetic component group
|
||||||
|
if(element.Value[0].Alphabetic) {
|
||||||
|
return element.Value[0].Alphabetic;
|
||||||
|
}
|
||||||
|
// Orthanc does not return PN properly so this is a temporary workaround
|
||||||
|
return element.Value[0];
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Returns the first string value as a Javascript Number
|
||||||
|
* @param element - The group/element of the element (e.g. '00200013')
|
||||||
|
* @param [defaultValue] - The default value to return if the element does not exist
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
DICOMWeb.getNumber = function(element, defaultValue) {
|
||||||
|
if(!element) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
// Value is not present if the attribute has a zero length value
|
||||||
|
if(!element.Value) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
// Sanity check to make sure we have at least one entry in the array.
|
||||||
|
if(!element.Value.length) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return parseFloat(element.Value[0]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Returns the specified element as a string. Multi-valued elements will be separated by a backslash
|
||||||
|
*
|
||||||
|
* @param element - The group/element of the element (e.g. '00200013')
|
||||||
|
* @param [defaultValue] - The value to return if the element is not present
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
DICOMWeb.getString = function(element, defaultValue) {
|
||||||
|
if(!element) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
// Value is not present if the attribute has a zero length value
|
||||||
|
if(!element.Value) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
// Sanity check to make sure we have at least one entry in the array.
|
||||||
|
if(!element.Value.length) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
// Join the array together separated by backslash
|
||||||
|
// NOTE: Orthanc does not correctly split values into an array so the join is a no-op
|
||||||
|
return element.Value.join('\\');
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DICOMWeb = {};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<template name="layout">
|
||||||
|
<nav class="navbar navbar-default" role="navigation">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="/">OHIF Viewer</a>
|
||||||
|
</div>
|
||||||
|
<div id="navbar" class="navbar-collapse collapse">
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
<li>
|
||||||
|
<a href="http://www.ohif.org">About</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form class="navbar-form">
|
||||||
|
<div class="form-group">
|
||||||
|
{{> loginButtons align="right"}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{{> yield}}
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.navbar
|
||||||
|
margin-bottom: 0
|
||||||
|
border-radius: 0px
|
||||||
|
|
||||||
|
.navbar-form
|
||||||
|
margin: 0
|
||||||
|
padding: 15px 3px
|
||||||
|
|
||||||
|
.navbar-default
|
||||||
|
-webkit-transition: background-color 200ms linear
|
||||||
|
-moz-transition: background-color 200ms linear
|
||||||
|
-o-transition: background-color 200ms linear
|
||||||
|
-ms-transition: background-color 200ms linear
|
||||||
|
transition: background-color 200ms linear
|
||||||
|
|
||||||
|
@media (min-width: 768px)
|
||||||
|
.navbar
|
||||||
|
border-radius: 0px
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template name="login">
|
||||||
|
{{> atForm}}
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<template name="notFound">
|
||||||
|
Not found
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<template name="imageThumbnail">
|
||||||
|
<div class="imageThumbnail">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Template.imageThumbnail.onRendered(function() {
|
||||||
|
var instance = this.data.instances[0];
|
||||||
|
var element = this.find('.imageThumbnail');
|
||||||
|
cornerstone.enable(element);
|
||||||
|
|
||||||
|
var imageId = getImageId(instance);
|
||||||
|
|
||||||
|
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||||
|
cornerstone.displayImage(element, image);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.imageThumbnail
|
||||||
|
width: 90px
|
||||||
|
height: 90px
|
||||||
|
background-color: black
|
||||||
|
margin: 0 auto
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<template name="studyBrowser">
|
||||||
|
<div class="studyBrowser">
|
||||||
|
{{#each studies}}
|
||||||
|
{{> thumbnails}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Template.studyBrowser.helpers({
|
||||||
|
studies : function() {
|
||||||
|
var studies = Session.get('studies');
|
||||||
|
return studies;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.studyBrowser
|
||||||
|
float: left
|
||||||
|
height: 100%
|
||||||
|
width: 120px
|
||||||
|
overflow: auto
|
||||||
|
background-color: black
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<template name="thumbnailEntry">
|
||||||
|
<div class="thumbnailEntry">
|
||||||
|
{{ > imageThumbnail }}
|
||||||
|
<div class='seriesDescription noselect'>{{seriesDescription}}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
function cloneElement(element, targetId) {
|
||||||
|
// Clone the DOM element
|
||||||
|
var clone = element.cloneNode(true);
|
||||||
|
|
||||||
|
// Find any canvas children to clone
|
||||||
|
var clonedCanvases = $(clone).find('canvas');
|
||||||
|
clonedCanvases.each(function(canvasIndex, clonedCanvas) {
|
||||||
|
// Draw from the original canvas to the cloned canvas
|
||||||
|
var context = clonedCanvas.getContext('2d');
|
||||||
|
var thumbnailCanvas = $(element).find('canvas').get(canvasIndex);
|
||||||
|
context.drawImage(thumbnailCanvas, 0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the clone with the targetId
|
||||||
|
clone.id = targetId;
|
||||||
|
clone.style.visibility = 'hidden';
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
function thumbnailDragStartHandler(e) {
|
||||||
|
// Prevent any scrolling behaviour normally caused by the original event
|
||||||
|
e.originalEvent.preventDefault();
|
||||||
|
|
||||||
|
// Identify the current study and series index from the thumbnail's DOM position
|
||||||
|
var targetThumbnail = e.currentTarget;
|
||||||
|
var imageThumbnail = $(targetThumbnail);
|
||||||
|
|
||||||
|
// Store this data for use during drag and drop
|
||||||
|
OHIF.viewer.dragAndDropData = {
|
||||||
|
studyInstanceUid: imageThumbnail.data('studyInstanceUid'),
|
||||||
|
seriesInstanceUid: imageThumbnail.data('seriesInstanceUid')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clone the image thumbnail
|
||||||
|
var targetId = targetThumbnail.id + 'DragClone';
|
||||||
|
var clone = cloneElement(targetThumbnail, targetId);
|
||||||
|
clone.classList.add('imageThumbnailClone');
|
||||||
|
|
||||||
|
// Set pointerEvents to pass through the clone DOM element
|
||||||
|
// This is necessary in order to identify what is below it
|
||||||
|
// when using document.elementFromPoint
|
||||||
|
clone.style.pointerEvents = "none";
|
||||||
|
|
||||||
|
// Append the clone to the parent of the target
|
||||||
|
targetThumbnail.parentNode.appendChild(clone);
|
||||||
|
|
||||||
|
// Set the cursor x and y positions from the current touch/mouse coordinates
|
||||||
|
// Handle touchStart cases
|
||||||
|
if (e.type === 'touchstart') {
|
||||||
|
cursorX = e.originalEvent.touches[0].pageX;
|
||||||
|
cursorY = e.originalEvent.touches[0].pageY;
|
||||||
|
} else {
|
||||||
|
cursorX = e.pageX;
|
||||||
|
cursorY = e.pageY;
|
||||||
|
|
||||||
|
// Also hook up event handlers for mouse events
|
||||||
|
$(document).on('mousemove', function(e) {
|
||||||
|
thumbnailDragHandler(e, targetThumbnail);
|
||||||
|
});
|
||||||
|
$(document).on('mouseup', function(e) {
|
||||||
|
thumbnailDragEndHandler(e, targetThumbnail);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This block gets the current position of the touch/mouse
|
||||||
|
// relative to the thumbnail itself
|
||||||
|
//
|
||||||
|
// i.e. Where did the user grab it from?
|
||||||
|
var position = imageThumbnail.offset();
|
||||||
|
var left = position.left;
|
||||||
|
var top = position.top;
|
||||||
|
|
||||||
|
// This difference is saved for later so the element movement looks normal
|
||||||
|
diffX = cursorX - left;
|
||||||
|
diffY = cursorY - top;
|
||||||
|
|
||||||
|
// This sets the default style properties of the cloned element so it is
|
||||||
|
// ready to be dragged around the page
|
||||||
|
$(clone).css({
|
||||||
|
top: top,
|
||||||
|
left: left,
|
||||||
|
position: 'absolute',
|
||||||
|
'z-index': 100,
|
||||||
|
visibility: 'hidden'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function thumbnailDragHandler(e, target) {
|
||||||
|
// Get the touch/mouse coordinates from the event
|
||||||
|
var cursorX,
|
||||||
|
cursorY;
|
||||||
|
if (e.type === 'touchmove') {
|
||||||
|
cursorX = e.originalEvent.changedTouches[0].pageX;
|
||||||
|
cursorY = e.originalEvent.changedTouches[0].pageY;
|
||||||
|
} else {
|
||||||
|
cursorX = e.pageX;
|
||||||
|
cursorY = e.pageY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the clone element and update it's position on the page
|
||||||
|
var clone = $('#' + target.id + 'DragClone');
|
||||||
|
clone.css({
|
||||||
|
top: cursorY - diffY,
|
||||||
|
left: cursorX - diffX,
|
||||||
|
position: 'absolute',
|
||||||
|
'z-index': 100,
|
||||||
|
visibility: 'visible'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Identify the element below the current cursor position
|
||||||
|
var elemBelow = document.elementFromPoint(cursorX + diffX, cursorY + diffY);
|
||||||
|
|
||||||
|
// If none exists, stop here
|
||||||
|
if (!elemBelow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any current faded effects on viewports
|
||||||
|
$('.imageViewerViewport canvas').removeClass('faded');
|
||||||
|
|
||||||
|
// Figure out what to do depending on what we're dragging over
|
||||||
|
var viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
|
||||||
|
if (viewportsDraggedOver.length) {
|
||||||
|
// If we're dragging over a non-empty viewport, fade it and change the cursor style
|
||||||
|
viewportsDraggedOver.find('canvas').not('.magnifyTool').addClass('faded');
|
||||||
|
document.body.style.cursor = "copy";
|
||||||
|
} else if (elemBelow.classList.contains('imageViewerViewport') && elemBelow.classList.contains('empty')) {
|
||||||
|
// If we're dragging over an empty viewport, just change the cursor style
|
||||||
|
document.body.style.cursor = "copy";
|
||||||
|
} else {
|
||||||
|
// Otherwise, keep the cursor as no-drop style
|
||||||
|
document.body.style.cursor = "no-drop";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function thumbnailDragEndHandler(e, target) {
|
||||||
|
// Remove the mouse event listeners
|
||||||
|
$(document).off('mousemove mouseup');
|
||||||
|
|
||||||
|
// Reset the cursor style to the default
|
||||||
|
document.body.style.cursor = "auto";
|
||||||
|
|
||||||
|
// Get the cloned element
|
||||||
|
var clone = $('#' + target.id + 'DragClone');
|
||||||
|
|
||||||
|
// If it doesn't exist, stop here
|
||||||
|
if (!clone.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var top = clone.offset().top;
|
||||||
|
var left = clone.offset().left;
|
||||||
|
|
||||||
|
// Identify the element below the cloned element position
|
||||||
|
var elemBelow = document.elementFromPoint(left + diffX, top + diffY);
|
||||||
|
|
||||||
|
// Remove all cloned elements from the page
|
||||||
|
$('.imageThumbnailClone').remove();
|
||||||
|
|
||||||
|
// Remove any current faded effects on viewports
|
||||||
|
$('.imageViewerViewport canvas').removeClass('faded');
|
||||||
|
|
||||||
|
// If none exists, stop here
|
||||||
|
if (!elemBelow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any fade effects on the element below
|
||||||
|
elemBelow.classList.remove('faded');
|
||||||
|
|
||||||
|
var element;
|
||||||
|
var viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
|
||||||
|
if (viewportsDraggedOver.length) {
|
||||||
|
// If we're dragging over a non-empty viewport, retrieve it
|
||||||
|
element = viewportsDraggedOver.get(0);
|
||||||
|
} else if (elemBelow.classList.contains('imageViewerViewport') &&
|
||||||
|
elemBelow.classList.contains('empty')) {
|
||||||
|
// If we're dragging over an empty viewport, retrieve that instead
|
||||||
|
element = elemBelow;
|
||||||
|
} else {
|
||||||
|
// Otherwise, stop here
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no store drag and drop data, stop here
|
||||||
|
if (!OHIF.viewer.dragAndDropData) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rerender the viewport using the drag and drop data
|
||||||
|
rerenderViewportWithNewSeries(element, OHIF.viewer.dragAndDropData);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Template.thumbnailEntry.onRendered(function() {
|
||||||
|
var entry = this.find('.thumbnailEntry');
|
||||||
|
$(entry).data('seriesInstanceUid', Template.parentData(0).seriesInstanceUid);
|
||||||
|
$(entry).data('studyInstanceUid', Template.parentData(1).studyInstanceUid);
|
||||||
|
});
|
||||||
|
|
||||||
|
Template.thumbnailEntry.events({
|
||||||
|
// Touch drag/drop events
|
||||||
|
'touchstart .thumbnailEntry, mousedown .thumbnailEntry': function(e) {
|
||||||
|
thumbnailDragStartHandler(e);
|
||||||
|
},
|
||||||
|
'touchmove .thumbnailEntry': function(e) {
|
||||||
|
thumbnailDragHandler(e, e.currentTarget);
|
||||||
|
},
|
||||||
|
'touchend .thumbnailEntry': function(e){
|
||||||
|
thumbnailDragEndHandler(e, e.currentTarget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Template.thumbnailEntry.helpers({
|
||||||
|
seriesDescription : function() {
|
||||||
|
if (!this.seriesDescription) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return this.seriesDescription;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
.thumbnailEntry
|
||||||
|
width: 105px
|
||||||
|
padding: 5px
|
||||||
|
background-color: black
|
||||||
|
border: 1px solid #424242
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
.seriesDescription
|
||||||
|
text-align: center
|
||||||
|
color: #555
|
||||||
|
word-wrap: break-word
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: white
|
||||||
|
|
||||||
|
&.active
|
||||||
|
background-color: #424242
|
||||||
|
border-color: #4e4e4e
|
||||||
|
|
||||||
|
.seriesDescription
|
||||||
|
color: white
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template name="thumbnails">
|
||||||
|
{{#each thumbnails}}
|
||||||
|
{{ >thumbnailEntry }}
|
||||||
|
{{/each}}
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Template.thumbnails.helpers({
|
||||||
|
thumbnails: function() {
|
||||||
|
var stacks = createStacks(this);
|
||||||
|
return stacks;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<template name="imageViewerViewport">
|
||||||
|
<div class='imageViewerViewport'
|
||||||
|
oncontextmenu='return false;'
|
||||||
|
unselectable='on'
|
||||||
|
onselectstart='return false;'>
|
||||||
|
</div>
|
||||||
|
<div class='viewportInstructions'>
|
||||||
|
Please drag a stack here to view images.
|
||||||
|
</div>
|
||||||
|
{{ >loadingIndicator }}
|
||||||
|
{{ >viewportOverlay }}
|
||||||
|
{{ >viewportOrientationMarkers }}
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
/**
|
||||||
|
* This function enables stack prefetching for a specified element (viewport)
|
||||||
|
* It first disables any prefetching currently occurring on any other viewports.
|
||||||
|
* Prefetching will not be enabled if the series is a clip.
|
||||||
|
*
|
||||||
|
* @param element
|
||||||
|
*/
|
||||||
|
function enablePrefetchOnElement() {
|
||||||
|
var viewportIndex = Session.get('ActiveViewport');
|
||||||
|
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||||
|
console.log('Enabling prefetch on new element');
|
||||||
|
|
||||||
|
// Loop through all viewports and disable stackPrefetch
|
||||||
|
$('.imageViewerViewport').each(function() {
|
||||||
|
if (!$(this).find('canvas').length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cornerstoneTools.stackPrefetch.disable(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make sure there is a stack to fetch
|
||||||
|
var stack = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
|
||||||
|
cornerstoneTools.stackPrefetch.enable(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayReferenceLines() {
|
||||||
|
console.log("Changing reference line display");
|
||||||
|
var viewportIndex = Session.get('ActiveViewport');
|
||||||
|
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||||
|
|
||||||
|
$('.imageViewerViewport').each(function(index, element) {
|
||||||
|
if (!$(this).find('canvas').length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === viewportIndex) {
|
||||||
|
cornerstoneTools.referenceLines.tool.disable(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cornerstoneTools.referenceLines.tool.enable(element, OHIF.viewer.updateImageSynchronizer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSeriesIntoViewport(data) {
|
||||||
|
if (!data.series || !data.viewport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var study = data.study;
|
||||||
|
var series = data.series;
|
||||||
|
var element = data.viewport;
|
||||||
|
var viewportIndex = $(".imageViewerViewport").index(data.viewport);
|
||||||
|
|
||||||
|
var allEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
|
||||||
|
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
|
||||||
|
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
|
||||||
|
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
|
||||||
|
'CornerstoneToolsDragStartActive CornerstoneToolsMultiTouchDragStart';
|
||||||
|
|
||||||
|
var imageIds = [];
|
||||||
|
var numImages = series.instances.length;
|
||||||
|
series.instances.forEach(function(instance, imageIndex) {
|
||||||
|
var imageId = getImageId(instance);
|
||||||
|
imageIds.push(imageId);
|
||||||
|
var data = {
|
||||||
|
instance: instance,
|
||||||
|
series: series,
|
||||||
|
study: study,
|
||||||
|
numImages: numImages,
|
||||||
|
imageIndex: imageIndex + 1
|
||||||
|
};
|
||||||
|
addMetaData(imageId, data);
|
||||||
|
});
|
||||||
|
|
||||||
|
var stack = {
|
||||||
|
currentImageIdIndex: 0,
|
||||||
|
imageIds: imageIds
|
||||||
|
};
|
||||||
|
|
||||||
|
var imageId = imageIds[stack.currentImageIdIndex];
|
||||||
|
|
||||||
|
var templateData = Template.currentData();
|
||||||
|
templateData.imageId = imageId;
|
||||||
|
|
||||||
|
// NOTE: This uses the experimental WebGL renderer for Cornerstone!
|
||||||
|
cornerstone.enable(element, cornerstone.webGL.renderer.render);
|
||||||
|
// If you have problems, replace it with this line instead:
|
||||||
|
// cornerstone.enable(element);
|
||||||
|
|
||||||
|
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||||
|
cornerstone.displayImage(element, image);
|
||||||
|
element.classList.remove('empty');
|
||||||
|
$(element).siblings('.viewportInstructions').hide();
|
||||||
|
$(element).siblings('.imageViewerViewportOverlay').show();
|
||||||
|
|
||||||
|
cornerstone.resize(element, true);
|
||||||
|
|
||||||
|
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
|
||||||
|
|
||||||
|
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
|
||||||
|
|
||||||
|
// Enable orientation markers, if applicable
|
||||||
|
var viewport = cornerstone.getViewport(element);
|
||||||
|
updateOrientationMarkers(element, viewport);
|
||||||
|
|
||||||
|
// Clear any old stack data
|
||||||
|
cornerstoneTools.clearToolState(element, 'stack');
|
||||||
|
cornerstoneTools.addToolState(element, 'stack', stack);
|
||||||
|
|
||||||
|
// Enable mouse input
|
||||||
|
cornerstoneTools.mouseInput.enable(element);
|
||||||
|
cornerstoneTools.touchInput.enable(element);
|
||||||
|
cornerstoneTools.mouseWheelInput.enable(element);
|
||||||
|
|
||||||
|
var activeTool = toolManager.getActiveTool();
|
||||||
|
toolManager.setActiveTool(activeTool);
|
||||||
|
|
||||||
|
cornerstoneTools.magnify.enable(element);
|
||||||
|
|
||||||
|
function onImageRendered(e, eventData) {
|
||||||
|
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
$(element).off('CornerstoneImageRendered', onImageRendered);
|
||||||
|
$(element).on('CornerstoneImageRendered', onImageRendered);
|
||||||
|
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
|
||||||
|
|
||||||
|
function onNewImage(e, eventData) {
|
||||||
|
// Update the templateData with the new imageId
|
||||||
|
// This allows the template helpers to update reactively
|
||||||
|
templateData.imageId = eventData.enabledElement.image.imageId;
|
||||||
|
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
$(element).off('CornerstoneNewImage', onNewImage);
|
||||||
|
$(element).on('CornerstoneNewImage', onNewImage);
|
||||||
|
|
||||||
|
function sendActivationTrigger(e, eventData) {
|
||||||
|
var activeViewportIndex = Session.get('ActiveViewport');
|
||||||
|
var viewportIndex = $(".imageViewerViewport").index(eventData.element);
|
||||||
|
if (viewportIndex === activeViewportIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
eventData.viewportIndex = viewportIndex;
|
||||||
|
var customEvent = jQuery.Event('ActivateViewport', eventData);
|
||||||
|
customEvent.type = 'ActivateViewport'; // Need to overwrite the type set in the touch tools
|
||||||
|
$(e.target).trigger(customEvent, eventData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(element).off(allEvents, sendActivationTrigger);
|
||||||
|
$(element).on(allEvents, sendActivationTrigger);
|
||||||
|
|
||||||
|
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
||||||
|
|
||||||
|
if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
|
||||||
|
OHIF.viewer.updateImageSynchronizer.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
enablePrefetchOnElement();
|
||||||
|
displayReferenceLines();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Template.imageViewerViewport.onRendered(function() {
|
||||||
|
var studies = Session.get('studies');
|
||||||
|
var viewport = this.find(".imageViewerViewport");
|
||||||
|
var viewportIndex = $(".imageViewerViewport").index(viewport);
|
||||||
|
|
||||||
|
this.data.viewportIndex = viewportIndex;
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
viewport: viewport
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.data.seriesInstanceUid !== undefined && this.data.studyInstanceUid !== undefined) {
|
||||||
|
var studyInstanceUid = this.data.studyInstanceUid;
|
||||||
|
var seriesInstanceUid = this.data.seriesInstanceUid;
|
||||||
|
|
||||||
|
studies.every(function(study) {
|
||||||
|
if (study.studyInstanceUid === studyInstanceUid) {
|
||||||
|
data.study = study;
|
||||||
|
study.seriesList.every(function(series) {
|
||||||
|
if (series.seriesInstanceUid === seriesInstanceUid) {
|
||||||
|
data.series = series;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var stacks = [];
|
||||||
|
studies.forEach(function(study) {
|
||||||
|
study.seriesList.forEach(function(series) {
|
||||||
|
var stack = {
|
||||||
|
series: series,
|
||||||
|
study: study
|
||||||
|
};
|
||||||
|
stacks.push(stack);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (viewportIndex >= stacks.length) {
|
||||||
|
viewport.classList.add('empty');
|
||||||
|
$(viewport).siblings('.viewportInstructions').show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.series = stacks[viewportIndex].series;
|
||||||
|
data.study = stacks[viewportIndex].study;
|
||||||
|
}
|
||||||
|
loadSeriesIntoViewport(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
Template.imageViewerViewport.onDestroyed(function() {
|
||||||
|
var element = this.find(".imageViewerViewport");
|
||||||
|
cornerstone.disable(element);
|
||||||
|
});
|
||||||
|
|
||||||
|
Template.imageViewerViewport.events({
|
||||||
|
'ActivateViewport .imageViewerViewport': function(e) {
|
||||||
|
console.log('ActivateViewport: ' + e.viewportIndex);
|
||||||
|
Session.set('ActiveViewport', e.viewportIndex);
|
||||||
|
enablePrefetchOnElement();
|
||||||
|
displayReferenceLines();
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.imageViewerViewport
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
background-color: black
|
||||||
|
|
||||||
|
// Prevents the entire page from getting larger
|
||||||
|
// when the magnify tool is near the sides/corners of the page
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
.viewportInstructions
|
||||||
|
display: none
|
||||||
|
font-size: 13px
|
||||||
|
color: #e4ad00
|
||||||
|
line-height: 18px
|
||||||
|
pointer-events: none // Necessary for drag/drop through to cornerstone element below
|
||||||
|
text-align: center
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
bottom: 0
|
||||||
|
right: 0
|
||||||
|
left: 0
|
||||||
|
margin: auto
|
||||||
|
height: 20px
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<template name="imageViewerViewports">
|
||||||
|
<div id='imageViewerViewports'>
|
||||||
|
{{ #each viewportArray }}
|
||||||
|
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
|
||||||
|
<div class="removable">
|
||||||
|
{{ > imageViewerViewport}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ /each }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
function getNumViewports() {
|
||||||
|
var viewportRows = Session.get('viewportRows');
|
||||||
|
var viewportColumns = Session.get('viewportColumns');
|
||||||
|
return viewportRows * viewportColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
Template.imageViewerViewports.helpers({
|
||||||
|
height: function() {
|
||||||
|
var viewportRows = Session.get('viewportRows');
|
||||||
|
return 100 / viewportRows;
|
||||||
|
},
|
||||||
|
width: function() {
|
||||||
|
var viewportColumns = Session.get('viewportColumns');
|
||||||
|
return 100 / viewportColumns;
|
||||||
|
},
|
||||||
|
viewportArray: function() {
|
||||||
|
// This is a really annoying thing to have to do, but Meteor
|
||||||
|
// doesn't want to let me use another type of helper.
|
||||||
|
var numViewports = getNumViewports();
|
||||||
|
|
||||||
|
var array = [];
|
||||||
|
for (var i=0; i < numViewports; ++i) {
|
||||||
|
var data = {
|
||||||
|
viewportIndex: i
|
||||||
|
};
|
||||||
|
array.push(data);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#imageViewerViewports
|
||||||
|
height: calc(100% - 30px)
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.viewportContainer
|
||||||
|
float: left
|
||||||
|
border: 2px solid #777
|
||||||
|
|
||||||
|
.removable
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
position: relative // Necessary so that the viewportOverlay is on top of the viewports
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<template name="layoutChooser">
|
||||||
|
<div class="layoutChooser pull-right dropdown-menu" role="menu">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
function highlightCells(currentCell) {
|
||||||
|
var cells = $('.layoutChooser table td');
|
||||||
|
cells.removeClass('hover');
|
||||||
|
|
||||||
|
currentCell = $(currentCell);
|
||||||
|
var table = currentCell.parents('.layoutChooser table').get(0);
|
||||||
|
var rowIndex = currentCell.closest('tr').index();
|
||||||
|
var columnIndex = currentCell.index();
|
||||||
|
|
||||||
|
// Loop through the table row by row
|
||||||
|
// and cell by cell to apply the highlighting
|
||||||
|
for (var i = 0; i < table.rows.length; i++) {
|
||||||
|
row = table.rows[i];
|
||||||
|
if (i <= rowIndex) {
|
||||||
|
for (var j = 0; j < row.cells.length; j++) {
|
||||||
|
if (j <= columnIndex) {
|
||||||
|
cell = row.cells[j];
|
||||||
|
cell.classList.add('hover');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Template.layoutChooser.events({
|
||||||
|
'touchstart .layoutChooser table td, mouseenter .layoutChooser table td': function(evt) {
|
||||||
|
highlightCells(evt.currentTarget);
|
||||||
|
},
|
||||||
|
'click .layoutChooser table td': function(evt) {
|
||||||
|
$('#imageViewerViewports').remove();
|
||||||
|
var container = $(".viewerMain").get(0);
|
||||||
|
UI.render(Template.imageViewerViewports, container);
|
||||||
|
|
||||||
|
var currentCell = $(evt.currentTarget);
|
||||||
|
var rowIndex = currentCell.closest('tr').index();
|
||||||
|
var columnIndex = currentCell.index();
|
||||||
|
|
||||||
|
// Add 1 because the indices start from zero
|
||||||
|
Session.set('viewportRows', rowIndex + 1);
|
||||||
|
Session.set('viewportColumns', columnIndex + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.layoutChooser
|
||||||
|
width: 75px
|
||||||
|
min-width: 75px // to override bootstrap's dropdown-menu class
|
||||||
|
height: 75px
|
||||||
|
|
||||||
|
table
|
||||||
|
margin: 0 auto
|
||||||
|
|
||||||
|
td
|
||||||
|
cursor: pointer
|
||||||
|
border: 2px solid black
|
||||||
|
width: 20px
|
||||||
|
height: 20px
|
||||||
|
transition: background-color 0.1s linear
|
||||||
|
|
||||||
|
&:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave
|
||||||
|
background-color: gray
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<template name="loadingIndicator">
|
||||||
|
<div class="imageViewerLoadingIndicator loadingIndicator">
|
||||||
|
<div class="indicatorContents">
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="imageViewerErrorLoadingIndicator loadingIndicator">
|
||||||
|
<div class="indicatorContents">
|
||||||
|
<h4>Error Loading Image</h4>
|
||||||
|
<p class='description'>An error has occurred.</p>
|
||||||
|
<p class='details'></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
Meteor.startup(function() {
|
||||||
|
cornerstoneTools.loadHandlerManager.setStartLoadHandler(startLoadingHandler);
|
||||||
|
cornerstoneTools.loadHandlerManager.setEndLoadHandler(doneLoadingHandler);
|
||||||
|
cornerstoneTools.loadHandlerManager.setErrorLoadingHandler(errorLoadingHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
var loadHandlerTimeout;
|
||||||
|
|
||||||
|
startLoadingHandler = function(element) {
|
||||||
|
clearTimeout(loadHandlerTimeout);
|
||||||
|
loadHandlerTimeout = setTimeout(function() {
|
||||||
|
var elem = $(element);
|
||||||
|
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
|
||||||
|
elem.find('canvas').not('.magnifyTool').addClass("faded");
|
||||||
|
elem.siblings('.imageViewerLoadingIndicator').css('display', 'block');
|
||||||
|
}, OHIF.viewer.loadIndicatorDelay);
|
||||||
|
};
|
||||||
|
|
||||||
|
doneLoadingHandler = function(element) {
|
||||||
|
clearTimeout(loadHandlerTimeout);
|
||||||
|
var elem = $(element);
|
||||||
|
elem.siblings('.imageViewerErrorLoadingIndicator').css('display', 'none');
|
||||||
|
elem.find('canvas').not('.magnifyTool').removeClass("faded");
|
||||||
|
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
||||||
|
};
|
||||||
|
|
||||||
|
errorLoadingHandler = function(element, imageId, error, source) {
|
||||||
|
clearTimeout(loadHandlerTimeout);
|
||||||
|
var elem = $(element);
|
||||||
|
|
||||||
|
// Could probably chain all of these, but this is more readable
|
||||||
|
elem.find('canvas').not('.magnifyTool').removeClass("faded");
|
||||||
|
elem.siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
||||||
|
|
||||||
|
// Don't display errors from the stackPrefetch tool
|
||||||
|
if (source === "stackPrefetch") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorLoadingIndicator = elem.siblings('.imageViewerErrorLoadingIndicator');
|
||||||
|
errorLoadingIndicator.css('display', 'block');
|
||||||
|
|
||||||
|
var cleanedImageId = imageId;
|
||||||
|
|
||||||
|
// This is just used to expand upon some error messages that are sent
|
||||||
|
// when things fail. An example is a network error throwing the error
|
||||||
|
// which is only described as "network".
|
||||||
|
var errorDetails = {
|
||||||
|
network: "A network error has occurred"
|
||||||
|
// We need to expand this further when we see more obscure error messages
|
||||||
|
};
|
||||||
|
|
||||||
|
if (errorDetails.hasOwnProperty(error)) {
|
||||||
|
error = errorDetails[error];
|
||||||
|
}
|
||||||
|
|
||||||
|
errorLoadingIndicator.find('.description').text("An error has occurred while loading image: " + cleanedImageId);
|
||||||
|
if (error) {
|
||||||
|
errorLoadingIndicator.find('.details').text("Details: " + error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
.imageViewerLoadingIndicator
|
||||||
|
display: none
|
||||||
|
color: #ffa500
|
||||||
|
|
||||||
|
.faded
|
||||||
|
opacity: 0.5
|
||||||
|
|
||||||
|
.imageViewerErrorLoadingIndicator
|
||||||
|
display: none
|
||||||
|
background-color: rgba(0,0,0,0.75)
|
||||||
|
|
||||||
|
p, h4
|
||||||
|
text-align: center
|
||||||
|
color: #ffa500
|
||||||
|
padding: 4px 0px
|
||||||
|
|
||||||
|
p
|
||||||
|
font-size: 11pt
|
||||||
|
|
||||||
|
.loadingIndicator
|
||||||
|
pointer-events: none // Necessary for click-through to cornerstone element below
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
font-size: 18px
|
||||||
|
z-index: 100
|
||||||
|
|
||||||
|
.indicatorContents
|
||||||
|
position: absolute
|
||||||
|
top: 40%
|
||||||
|
width: 100%
|
||||||
|
text-align: center
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<template name="viewportOrientationMarkers">
|
||||||
|
<div class="viewportOrientationMarkers noselect">
|
||||||
|
<div class="topMid orientationMarker">
|
||||||
|
</div>
|
||||||
|
<div class="leftMid orientationMarker">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
updateOrientationMarkers = function(element, viewport) {
|
||||||
|
// Updates the orientation labels on the viewport
|
||||||
|
var enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
|
||||||
|
|
||||||
|
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowString = cornerstoneTools.orientation.getOrientationString(imagePlane.rowCosines);
|
||||||
|
var columnString = cornerstoneTools.orientation.getOrientationString(imagePlane.columnCosines);
|
||||||
|
var oppositeRowString = cornerstoneTools.orientation.invertOrientationString(rowString);
|
||||||
|
var oppositeColumnString = cornerstoneTools.orientation.invertOrientationString(columnString);
|
||||||
|
|
||||||
|
var markers = {
|
||||||
|
top: oppositeColumnString,
|
||||||
|
left: oppositeRowString,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
|
||||||
|
// the rotation applications
|
||||||
|
if (viewport.vflip) {
|
||||||
|
markers.top = cornerstoneTools.orientation.invertOrientationString(markers.top);
|
||||||
|
}
|
||||||
|
if (viewport.hflip) {
|
||||||
|
markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the viewport orientation marker DOM elements
|
||||||
|
var viewportOrientationMarkers = $(element).siblings(".viewportOrientationMarkers");
|
||||||
|
var topMarker = viewportOrientationMarkers.find('.topMid');
|
||||||
|
var leftMarker = viewportOrientationMarkers.find('.leftMid');
|
||||||
|
|
||||||
|
// Swap the labels accordingly if the viewport has been rotated
|
||||||
|
// This could be done in a more complex way for intermediate rotation values (e.g. 45 degrees)
|
||||||
|
if (viewport.rotation === 90 || viewport.rotation === -270) {
|
||||||
|
topMarker.text(markers.left);
|
||||||
|
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
|
||||||
|
} else if (viewport.rotation === -90 || viewport.rotation === 270) {
|
||||||
|
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
|
||||||
|
leftMarker.text(markers.top);
|
||||||
|
} else if (viewport.rotation === 180 || viewport.rotation === -180) {
|
||||||
|
topMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.top));
|
||||||
|
leftMarker.text(cornerstoneTools.orientation.invertOrientationString(markers.left));
|
||||||
|
} else {
|
||||||
|
topMarker.text(markers.top);
|
||||||
|
leftMarker.text(markers.left);
|
||||||
|
}
|
||||||
|
};
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
.viewportOrientationMarkers
|
||||||
|
pointer-events: none // Necessary for click-through to cornerstone element below
|
||||||
|
|
||||||
|
font-size: 15px
|
||||||
|
color: rgb(204, 204, 204)
|
||||||
|
line-height: 18px
|
||||||
|
|
||||||
|
.orientationMarker
|
||||||
|
position: absolute
|
||||||
|
|
||||||
|
.topMid
|
||||||
|
top: 5px
|
||||||
|
left: 50%
|
||||||
|
|
||||||
|
.leftMid
|
||||||
|
top: 47%
|
||||||
|
left: 5px
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<template name="viewportOverlay">
|
||||||
|
<div class="imageViewerViewportOverlay noselect">
|
||||||
|
<div class="topleft dicomTag">
|
||||||
|
<div>{{patientName}}</div>
|
||||||
|
<div>{{patientId}}</div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div class="topright dicomTag">
|
||||||
|
<div>{{studyDescription}}</div>
|
||||||
|
<div>{{studyDate}} {{studyTime}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bottomright dicomTag">
|
||||||
|
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
|
||||||
|
<div>{{compression}}</div>
|
||||||
|
<div>{{wwwc}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="bottomleft dicomTag">
|
||||||
|
<div>Ser: {{seriesNumber}}</div>
|
||||||
|
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
|
||||||
|
<div>{{frameRate}}</div>
|
||||||
|
<div>{{imageDimensions}}</div>
|
||||||
|
<div>{{seriesDescription}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
function getElementIfNotEmpty(viewportIndex) {
|
||||||
|
// Meteor template helpers run more often than expected
|
||||||
|
// They often seem to run just before the whole template is rendered
|
||||||
|
// This meant that the onRendered event hadn't fired yet, so the
|
||||||
|
// element wasn't enabled / set empty yet. The check here
|
||||||
|
// for canvases under the 'enabled' element div is to prevent
|
||||||
|
// 'undefined' errors from the helper functions
|
||||||
|
|
||||||
|
var imageViewerViewports = $('.imageViewerViewport'),
|
||||||
|
element = imageViewerViewports.get(viewportIndex),
|
||||||
|
canvases = imageViewerViewports.eq(viewportIndex).find('canvas');
|
||||||
|
|
||||||
|
if (!element || $(element).hasClass("empty") || canvases.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to make sure the element is enabled.
|
||||||
|
try {
|
||||||
|
var enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
} catch(error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPatient(property) {
|
||||||
|
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||||
|
if (!this.imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var patient = cornerstoneTools.metaData.get('patient', this.imageId);
|
||||||
|
if (!patient) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return patient[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStudy(property) {
|
||||||
|
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||||
|
if (!this.imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var study = cornerstoneTools.metaData.get('study', this.imageId);
|
||||||
|
if (!study) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return study[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeries(property) {
|
||||||
|
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||||
|
if (!this.imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var series = cornerstoneTools.metaData.get('series', this.imageId);
|
||||||
|
if (!series) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return series[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInstance(property) {
|
||||||
|
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||||
|
if (!this.imageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var instance = cornerstoneTools.metaData.get('instance', this.imageId);
|
||||||
|
if (!instance) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return instance[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImage(viewportIndex) {
|
||||||
|
var element = getElementIfNotEmpty(viewportIndex);
|
||||||
|
if (!element) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var enabledElement;
|
||||||
|
try {
|
||||||
|
enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
} catch(error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!enabledElement || !enabledElement.image) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return enabledElement.image;
|
||||||
|
}
|
||||||
|
|
||||||
|
Template.viewportOverlay.helpers({
|
||||||
|
wwwc: function() {
|
||||||
|
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
||||||
|
var element = getElementIfNotEmpty(this.viewportIndex);
|
||||||
|
if (!element) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var viewport = cornerstone.getViewport(element);
|
||||||
|
if (!viewport) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
|
||||||
|
},
|
||||||
|
zoom: function() {
|
||||||
|
Session.get('CornerstoneImageRendered' + this.viewportIndex);
|
||||||
|
var element = getElementIfNotEmpty(this.viewportIndex);
|
||||||
|
if (!element) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
var viewport = cornerstone.getViewport(element);
|
||||||
|
if (!viewport) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return (viewport.scale * 100.0);
|
||||||
|
},
|
||||||
|
imageDimensions: function() {
|
||||||
|
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||||
|
|
||||||
|
var image = getImage(this.viewportIndex);
|
||||||
|
if (!image) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return image.width + ' x ' + image.height;
|
||||||
|
},
|
||||||
|
patientName : function() {
|
||||||
|
return getPatient.call(this, 'name');
|
||||||
|
},
|
||||||
|
patientId : function() {
|
||||||
|
return getPatient.call(this, 'id');
|
||||||
|
},
|
||||||
|
studyDate : function() {
|
||||||
|
return getStudy.call(this, 'date');
|
||||||
|
},
|
||||||
|
studyTime : function() {
|
||||||
|
return getStudy.call(this, 'time');
|
||||||
|
},
|
||||||
|
studyDescription : function() {
|
||||||
|
return getStudy.call(this, 'description');
|
||||||
|
},
|
||||||
|
seriesDescription : function() {
|
||||||
|
return getSeries.call(this, 'description');
|
||||||
|
},
|
||||||
|
seriesNumber : function() {
|
||||||
|
return getSeries.call(this, 'number');
|
||||||
|
},
|
||||||
|
imageNumber : function() {
|
||||||
|
return getInstance.call(this, 'number');
|
||||||
|
},
|
||||||
|
imageIndex : function() {
|
||||||
|
return getInstance.call(this, 'index');
|
||||||
|
},
|
||||||
|
numImages : function() {
|
||||||
|
return getSeries.call(this, 'numImages');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
.imageViewerViewportOverlay
|
||||||
|
font-size: 13px
|
||||||
|
color: #e4ad00
|
||||||
|
line-height: 18px
|
||||||
|
display: none // Shown when an image is loaded
|
||||||
|
|
||||||
|
.dicomTag
|
||||||
|
pointer-events: none // Necessary for drag/drop through to cornerstone element below
|
||||||
|
position: absolute
|
||||||
|
|
||||||
|
.topleft
|
||||||
|
top: 3px
|
||||||
|
left: 3px
|
||||||
|
|
||||||
|
.topright
|
||||||
|
top: 3px
|
||||||
|
right: 3px
|
||||||
|
text-align: right
|
||||||
|
|
||||||
|
.bottomleft
|
||||||
|
bottom: 3px
|
||||||
|
left: 3px
|
||||||
|
|
||||||
|
.bottomright
|
||||||
|
bottom: 3px
|
||||||
|
right: 3px
|
||||||
|
text-align: right
|
||||||
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user