Merge branch 'master' of https://github.com/OHIF/Viewers
This commit is contained in:
commit
4f1fdfab3e
142 files changed
+6190
-3331
No files matched your search
@@ -2148,7 +2148,7 @@ if(typeof cornerstone === 'undefined'){
|
||||
|
||||
function initShaders() {
|
||||
for (var id in cornerstone.webGL.shaders) {
|
||||
console.log("WEBGL: Loading shader", id);
|
||||
//console.log("WEBGL: Loading shader", id);
|
||||
var shader = cornerstone.webGL.shaders[ id ];
|
||||
shader.attributes = {};
|
||||
shader.uniforms = {};
|
||||
@@ -2168,13 +2168,13 @@ if(typeof cornerstone === 'undefined'){
|
||||
|
||||
function initRenderer() {
|
||||
if (cornerstone.webGL.isWebGLInitialized === true) {
|
||||
console.log("WEBGL Renderer already initialized");
|
||||
//console.log("WEBGL Renderer already initialized");
|
||||
return;
|
||||
}
|
||||
if ( initWebGL( renderCanvas ) ) {
|
||||
initBuffers();
|
||||
initShaders();
|
||||
console.log("WEBGL Renderer initialized!");
|
||||
//console.log("WEBGL Renderer initialized!");
|
||||
cornerstone.webGL.isWebGLInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+1572
-1523
File diff suppressed because it is too large.
Load diff
@@ -1,14 +1,38 @@
|
||||
var hangingProtocol;
|
||||
|
||||
/**
|
||||
* This is an example of a hanging protocol
|
||||
* It takes in a set of studies, as well as the
|
||||
* number of rows and columns in the layout.
|
||||
*
|
||||
* It returns an array of objects, one for each viewport, detailing
|
||||
* which series should be loaded in the viewport.
|
||||
*
|
||||
* @param inputData
|
||||
* @returns {Array}
|
||||
*/
|
||||
function defaultHangingProtocol(inputData) {
|
||||
var studies = inputData.studies;
|
||||
var viewportRows = inputData.viewportRows;
|
||||
var viewportColumns = inputData.viewportColumns;
|
||||
|
||||
if (!studies.length) {
|
||||
log.warn("No studies provided to Hanging Protocol");
|
||||
return;
|
||||
}
|
||||
|
||||
// This is the most basic hanging protocol.
|
||||
var stacks = [];
|
||||
studies.forEach(function(study) {
|
||||
study.seriesList.forEach(function(series) {
|
||||
|
||||
// Ensure that the series has image data
|
||||
// (All images have rows)
|
||||
var anInstance = series.instances[0];
|
||||
if (!anInstance || !anInstance.rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
var stack = {
|
||||
series: series,
|
||||
study: study
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Converts a String to a UInt8 array of character codes
|
||||
* @param {String} str Input string
|
||||
* @returns {Uint8Array} Uint8 Array of character codes
|
||||
*/
|
||||
function stringToUint8Array(str) {
|
||||
var uint=new Uint8Array(str.length);
|
||||
for(var i=0,j=str.length;i<j;i++){
|
||||
uint[i]=str.charCodeAt(i);
|
||||
}
|
||||
return uint;
|
||||
}
|
||||
|
||||
function checkToken(token, data, dataOffset) {
|
||||
if(dataOffset + token.length > data.length) {
|
||||
//console.log('dataOffset >> ', dataOffset);
|
||||
return false;
|
||||
}
|
||||
|
||||
for(var i = 0; i < token.length; i++) {
|
||||
if(token[i] !== data[endIndex++]) {
|
||||
if(endIndex > 520000) {
|
||||
console.log('token=',uint8ArrayToString(token));
|
||||
console.log('data=', uint8ArrayToString(data, dataOffset, endIndex-dataOffset));
|
||||
console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(data[endIndex]), endIndex);
|
||||
console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(token[endIndex]), endIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
findIndexOfString = function(data, str, offset) {
|
||||
offset = offset || 0;
|
||||
|
||||
var token = stringToUint8Array(str);
|
||||
|
||||
for(var i=offset; i < data.length; i++) {
|
||||
if(data[i] === token[0]) {
|
||||
//console.log('match @', i);
|
||||
if(checkToken(token, data, i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Converts a Uint8 array to a string
|
||||
* @param data
|
||||
* @param offset
|
||||
* @param length
|
||||
* @returns {string}
|
||||
*/
|
||||
uint8ArrayToString = function(data, offset, length) {
|
||||
offset = offset || 0;
|
||||
length = length || data.length - offset;
|
||||
var str = "";
|
||||
|
||||
for(var i=offset; i < offset + length; i++) {
|
||||
str += String.fromCharCode(data[i]);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
@@ -5,6 +5,9 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use('http');
|
||||
|
||||
// DICOMWeb API functions
|
||||
api.addFiles('server/namespace.js', 'server');
|
||||
api.addFiles('server/getImageFrame.js', 'server');
|
||||
api.addFiles('server/getJSON.js', 'server');
|
||||
@@ -12,6 +15,10 @@ Package.onUse(function (api) {
|
||||
api.addFiles('server/getNumber.js', 'server');
|
||||
api.addFiles('server/getString.js', 'server');
|
||||
|
||||
// Helper functions
|
||||
api.addFiles('lib/findIndexOfString.js', 'server');
|
||||
api.addFiles('lib/uint8ArrayToString.js', 'server');
|
||||
|
||||
api.export("DICOMWeb", 'server');
|
||||
});
|
||||
|
||||
@@ -1,298 +1,408 @@
|
||||
var lineIndex = 0; //This holds drawn line index
|
||||
var cornerstoneTools = (function ($, cornerstone, cornerstoneMath, cornerstoneTools) {
|
||||
var activeLesionMeasurementData;
|
||||
var timepointID;
|
||||
var cornerstoneTools = (function($, cornerstone, cornerstoneMath, cornerstoneTools) {
|
||||
|
||||
"use strict";
|
||||
|
||||
if(cornerstoneTools === undefined) {
|
||||
if (cornerstoneTools === undefined) {
|
||||
cornerstoneTools = {};
|
||||
}
|
||||
|
||||
var configuration = {
|
||||
getLesionLocationCallback: getLesionLocationCallback,
|
||||
changeLesionLocationCallback: changeLesionLocationCallback,
|
||||
};
|
||||
|
||||
// Define a callback to get your text annotation
|
||||
// This could be used, e.g. to open a modal
|
||||
function getLesionLocationCallback(measurementData, eventData, doneCallback) {
|
||||
doneCallback(prompt('Enter your lesion location:'));
|
||||
}
|
||||
|
||||
function changeLesionLocationCallback(measurementData, eventData, doneCallback) {
|
||||
doneCallback(prompt('Change your lesion location:'));
|
||||
}
|
||||
|
||||
var toolType = "lesion";
|
||||
|
||||
///////// BEGIN ACTIVE TOOL ///////
|
||||
function createNewMeasurement(mouseEventData)
|
||||
{
|
||||
|
||||
function addNewMeasurement(mouseEventData) {
|
||||
var element = mouseEventData.element;
|
||||
var lesionCounter = "";
|
||||
|
||||
// Subscribe CornerstoneMouseup event, when mouse is up, call lesionDialog
|
||||
$(element).on("CornerstoneToolsMouseUp", function (e) {
|
||||
function doneCallback(lesionNumber) {
|
||||
measurementData.lesionName = "Target " + lesionNumber;
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
measurementData.active = false;
|
||||
cornerstone.updateImage(element);
|
||||
}
|
||||
|
||||
// Unsubscribe CornerstoneToolsMouseUp event
|
||||
$(element).off("CornerstoneToolsMouseUp");
|
||||
var measurementData = createNewMeasurement(mouseEventData);
|
||||
|
||||
// Show LesionDialog
|
||||
$(document).trigger("ShowLesionDialog", e);
|
||||
var eventData = {
|
||||
mouseButtonMask: mouseEventData.which,
|
||||
};
|
||||
|
||||
// associate this data with this imageId so we can render it and manipulate it
|
||||
cornerstoneTools.addToolState(element, toolType, measurementData);
|
||||
|
||||
// since we are dragging to another place to drop the end point, we can just activate
|
||||
// the end point and let the moveHandle move it for us.
|
||||
$(element).off('CornerstoneToolsMouseMove', cornerstoneTools.lesion.mouseMoveCallback);
|
||||
$(element).off('CornerstoneToolsMouseDown', cornerstoneTools.lesion.mouseDownCallback);
|
||||
$(element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.lesion.mouseDownActivateCallback);
|
||||
|
||||
cornerstone.updateImage(element);
|
||||
cornerstoneTools.moveNewHandle(mouseEventData, measurementData.handles.end, function() {
|
||||
if (cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
|
||||
// delete the measurement
|
||||
cornerstoneTools.removeToolState(element, toolType, measurementData);
|
||||
} else {
|
||||
// Set lesionMeasurementData Session
|
||||
var config = cornerstoneTools.lesion.getConfiguration();
|
||||
config.getLesionLocationCallback(measurementData, mouseEventData, doneCallback);
|
||||
}
|
||||
|
||||
$(element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.lesion.mouseMoveCallback);
|
||||
$(element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.lesion.mouseDownCallback);
|
||||
$(element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.lesion.mouseDownActivateCallback);
|
||||
cornerstone.updateImage(element);
|
||||
});
|
||||
}
|
||||
|
||||
// Set Lesion Name
|
||||
$(element).on("LesionNameSet", function(e,lesionName){
|
||||
lesionCounter = lesionName;
|
||||
function addNewMeasurementTouch(touchEventData) {
|
||||
var element = touchEventData.element;
|
||||
|
||||
function doneCallback(lesionNumber) {
|
||||
measurementData.lesionName = "Target " + lesionNumber;
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
measurementData.active = false;
|
||||
cornerstone.updateImage(element);
|
||||
}
|
||||
|
||||
var measurementData = createNewMeasurement(touchEventData);
|
||||
|
||||
// associate this data with this imageId so we can render it and manipulate it
|
||||
cornerstoneTools.addToolState(element, toolType, measurementData);
|
||||
|
||||
// since we are dragging to another place to drop the end point, we can just activate
|
||||
// the end point and let the moveHandle move it for us.
|
||||
$(element).off('CornerstoneToolsTouchDrag', cornerstoneTools.lesion.touchMoveHandle);
|
||||
$(element).off('CornerstoneToolsTap', cornerstoneTools.lesion.tapCallback);
|
||||
$(element).off('CornerstoneToolsDragStartActive', cornerstoneTools.lesion.touchDownActivateCallback);
|
||||
|
||||
cornerstone.updateImage(element);
|
||||
cornerstoneTools.moveNewHandleTouch(touchEventData, measurementData.handles.end, function() {
|
||||
if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
|
||||
// delete the measurement
|
||||
cornerstoneTools.removeToolState(element, toolType, measurementData);
|
||||
} else {
|
||||
// Set lesionMeasurementData Session
|
||||
var config = cornerstoneTools.lesion.getConfiguration();
|
||||
config.getLesionLocationCallback(measurementData, touchEventData, doneCallback);
|
||||
}
|
||||
|
||||
$(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.lesion.touchMoveHandle);
|
||||
$(element).on('CornerstoneToolsTap', cornerstoneTools.lesion.tapCallback);
|
||||
$(element).on('CornerstoneToolsDragStartActive', cornerstoneTools.lesion.touchDownActivateCallback);
|
||||
cornerstone.updateImage(element);
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe LesionToolModified and calls measurementModified function when lesion measurement is changed or updated.
|
||||
$(element).on("LesionToolModified", measurementModified);
|
||||
|
||||
// Subscribe LesionMeasurementCreated
|
||||
$(element).trigger("LesionMeasurementCreated");
|
||||
|
||||
// create the measurement data for this tool with the end handle activated
|
||||
function createNewMeasurement(mouseEventData) {
|
||||
// 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,
|
||||
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,
|
||||
x: mouseEventData.currentPoints.image.x,
|
||||
y: mouseEventData.currentPoints.image.y,
|
||||
highlight: true,
|
||||
active: true
|
||||
},
|
||||
textBox: {
|
||||
x: mouseEventData.currentPoints.image.x - 50,
|
||||
y: mouseEventData.currentPoints.image.y - 50,
|
||||
pointNearHandle: pointNearTextBox,
|
||||
active: false,
|
||||
movesIndependently: true,
|
||||
drawnIndependently: 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,
|
||||
imageId: mouseEventData.image.imageId,
|
||||
measurementText: 0,
|
||||
lesionName: '',
|
||||
isDeleted: false,
|
||||
lineNumber:"" //Indicates line number on image
|
||||
isTarget: true,
|
||||
uid: uuid.v4()
|
||||
};
|
||||
lineIndex++;
|
||||
return measurementData;
|
||||
}
|
||||
///////// END ACTIVE TOOL ///////
|
||||
|
||||
function pointNearTool(element, data, coords)
|
||||
{
|
||||
function pointNearTool(element, data, coords) {
|
||||
var lineSegment = {
|
||||
start: cornerstone.pixelToCanvas(element, data.handles.start), end: cornerstone.pixelToCanvas(element, data.handles.end)
|
||||
start: cornerstone.pixelToCanvas(element, data.handles.start),
|
||||
end: cornerstone.pixelToCanvas(element, data.handles.end)
|
||||
};
|
||||
var distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
|
||||
|
||||
if (pointNearTextBox(element, data.handles.textBox, coords)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
function pointNearTextBox(element, handle, coords) {
|
||||
if (!handle.boundingBox) {
|
||||
return;
|
||||
}
|
||||
return cornerstoneMath.point.insideRect(coords, handle.boundingBox);
|
||||
}
|
||||
|
||||
function suscribeLesionToolSelectedEvent(element) {
|
||||
var elementEvents = $._data(element, "events");
|
||||
var index = Object.keys(elementEvents).indexOf("LesionToolSelected");
|
||||
if (index < 0) {
|
||||
// Subscribe LesionToolSelected and calls measurementModified function when lesion measurement is changed or updated.
|
||||
$(element).on("LesionToolSelected", measurementModified);
|
||||
}
|
||||
|
||||
}
|
||||
///////// BEGIN IMAGE RENDERING ///////
|
||||
function onImageRendered(e, eventData) {
|
||||
|
||||
suscribeLesionToolSelectedEvent(e.currentTarget);
|
||||
|
||||
// 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) {
|
||||
if (!toolData) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateLesions(toolData, eventData);
|
||||
}
|
||||
|
||||
function updateLesions(toolData, eventData) {
|
||||
// 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();
|
||||
renderLesion(toolData.data[i], context, eventData);
|
||||
updateLesionCollection(toolData.data[i]);
|
||||
}
|
||||
|
||||
}
|
||||
///////// 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) {
|
||||
function updateLesionCollection(lesionData) {
|
||||
if (!lesionData.active) {
|
||||
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);
|
||||
}
|
||||
if (lesionData.timepointID && lesionData.timepointID !== "") {
|
||||
// Update Measurements Collection
|
||||
measurementManagerDAL.updateTimepointData(lesionData);
|
||||
} else {
|
||||
for (var i = 0; i < toolData.data.length; i++) {
|
||||
var data = toolData.data[i];
|
||||
activeLesionMeasurementData = lesionData;
|
||||
}
|
||||
}
|
||||
|
||||
//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;
|
||||
}
|
||||
}
|
||||
function renderLesion(data, context, eventData) {
|
||||
context.save();
|
||||
|
||||
var color;
|
||||
var element = eventData.element;
|
||||
var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
|
||||
var font = cornerstoneTools.textStyle.getFont();
|
||||
var config = cornerstoneTools.lesion.getConfiguration();
|
||||
|
||||
// configurable shadow from CornerstoneTools
|
||||
if (config && config.shadow) {
|
||||
context.shadowColor = '#000000';
|
||||
context.shadowOffsetX = +1;
|
||||
context.shadowOffsetY = +1;
|
||||
}
|
||||
|
||||
if (data.active) {
|
||||
color = cornerstoneTools.toolColors.getActiveColor();
|
||||
} else {
|
||||
color = cornerstoneTools.toolColors.getToolColor();
|
||||
}
|
||||
|
||||
enabledElement.image.render(enabledElement, true);
|
||||
// draw the line
|
||||
var handleStartCanvas = cornerstone.pixelToCanvas(element, data.handles.start);
|
||||
var handleEndCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
|
||||
var canvasTextLocation = cornerstone.pixelToCanvas(element, data.handles.textBox);
|
||||
|
||||
var context = enabledElement.canvas.getContext('2d');
|
||||
context.beginPath();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = lineWidth;
|
||||
context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
|
||||
context.lineTo(handleEndCanvas.x, handleEndCanvas.y);
|
||||
context.stroke();
|
||||
|
||||
var end = new Date();
|
||||
var diff = end - start;
|
||||
//console.log(diff + ' ms');
|
||||
// draw the handles
|
||||
cornerstoneTools.drawHandles(context, eventData, data.handles, color);
|
||||
|
||||
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
|
||||
//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
|
||||
};
|
||||
|
||||
enabledElement.invalid = false;
|
||||
context.moveTo(mid.x, mid.y);
|
||||
context.lineTo(canvasTextLocation.x + 20, canvasTextLocation.y + 20);
|
||||
context.stroke();
|
||||
|
||||
onImageRendered(e, eventData);
|
||||
// Draw the text
|
||||
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 textLines = [data.lesionName, text];
|
||||
|
||||
var boundingBox = cornerstoneTools.drawTextBox(context,
|
||||
textLines,
|
||||
canvasTextLocation.x, canvasTextLocation.y, color);
|
||||
|
||||
data.handles.textBox.boundingBox = boundingBox;
|
||||
|
||||
// Set measurement text to show lesion table
|
||||
data.measurementText = length.toFixed(1);
|
||||
|
||||
// Lesion Measurement is changed
|
||||
$(element).trigger("LesionTextChanged", data);
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
///////// END IMAGE RENDERING ///////
|
||||
|
||||
function updateLesion(e, eventData) {
|
||||
var enabledElement = eventData.enabledElement;
|
||||
var element = eventData.enabledElement.element;
|
||||
var isTarget = eventData.lesionData.isTarget;
|
||||
var lesionNumber = eventData.lesionData.lesionNumber;
|
||||
var type = eventData.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) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data,
|
||||
i;
|
||||
|
||||
switch (type) {
|
||||
case "delete":
|
||||
//If type is delete, remove measurement
|
||||
var deletedDataIndex = -1;
|
||||
|
||||
for (i = 0; i < toolData.data.length; i++) {
|
||||
data = toolData.data[i];
|
||||
|
||||
//When click a row of table measurements, measurement will be active and color will be green
|
||||
if (data.lesionNumber === lesionNumber && eventData.type !== "active" && isTarget) {
|
||||
data.visible = false;
|
||||
deletedDataIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedDataIndex >= 0 && deletedDataIndex < toolData.data.length) {
|
||||
toolData.data.splice(deletedDataIndex, 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case "active":
|
||||
for (i = 0; i < toolData.data.length; i++) {
|
||||
data = toolData.data[i];
|
||||
//When click a row of table measurements, measurement will be active and color will be green
|
||||
if (data.lesionNumber === lesionNumber && eventData.type === "active" && isTarget) {
|
||||
data.active = true;
|
||||
} else {
|
||||
data.active = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "inactive":
|
||||
for (i = 0; i < toolData.data.length; i++) {
|
||||
data = toolData.data[i];
|
||||
// Make inactive all lesions for the timepoint
|
||||
data.active = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
cornerstone.updateImage(element);
|
||||
}
|
||||
|
||||
|
||||
function loadImage(e, eventData) {
|
||||
// If type is active, load image and activate lesion
|
||||
// If type is inactive, update lesions of enabledElement as inactive
|
||||
|
||||
if (eventData.type === "active") {
|
||||
var stackToolDataSource = cornerstoneTools.getToolState(e.currentTarget, 'stack');
|
||||
var stackData = stackToolDataSource.data[0];
|
||||
var imageIds = stackData.imageIds;
|
||||
var imageIdIndex = imageIds.indexOf(eventData.lesionData.imageId);
|
||||
if (imageIdIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
cornerstone.loadAndCacheImage(imageIds[imageIdIndex]).then(function(image) {
|
||||
cornerstone.displayImage(eventData.enabledElement.element, image);
|
||||
updateLesion(e, eventData);
|
||||
});
|
||||
} else if (eventData.type === "inactive") {
|
||||
updateLesion(e, eventData);
|
||||
}
|
||||
}
|
||||
|
||||
//This function is called from cornerstone-viewport.html and updates lesion measurement and makes the lesion active
|
||||
function measurementModified(e, eventData) {
|
||||
loadImage(e, eventData);
|
||||
}
|
||||
|
||||
// module exports
|
||||
cornerstoneTools.lesion = cornerstoneTools.mouseButtonTool({
|
||||
createNewMeasurement : createNewMeasurement,
|
||||
onImageRendered: onImageRendered,
|
||||
pointNearTool : pointNearTool,
|
||||
pointNearToolForText: pointNearToolForText,
|
||||
toolType : toolType
|
||||
});
|
||||
cornerstoneTools.lesionTouch = cornerstoneTools.touchTool({
|
||||
createNewMeasurement: createNewMeasurement,
|
||||
addNewMeasurement: addNewMeasurement,
|
||||
onImageRendered: onImageRendered,
|
||||
pointNearTool: pointNearTool,
|
||||
pointNearToolForText: pointNearToolForText,
|
||||
toolType: toolType
|
||||
});
|
||||
|
||||
cornerstoneTools.lesionTouch = cornerstoneTools.touchTool({
|
||||
createNewMeasurement: createNewMeasurement,
|
||||
addNewMeasurement: addNewMeasurementTouch,
|
||||
onImageRendered: onImageRendered,
|
||||
pointNearTool: pointNearTool,
|
||||
toolType: toolType
|
||||
});
|
||||
|
||||
cornerstoneTools.lesion.setConfiguration(configuration);
|
||||
|
||||
return cornerstoneTools;
|
||||
|
||||
}($, cornerstone, cornerstoneMath, cornerstoneTools));
|
||||
@@ -0,0 +1,148 @@
|
||||
var measurementManagerDAL = (function() {
|
||||
PatientLocations = new Meteor.Collection(null);
|
||||
|
||||
function getLocationName(id) {
|
||||
var locationObject = PatientLocations.findOne(id);
|
||||
return locationObject.location || "";
|
||||
}
|
||||
|
||||
// Add timepoint data to Measurements collection
|
||||
function addTimepointData(lesionData) {
|
||||
var timepoints = Timepoints.find().fetch();
|
||||
var timepointsObject = {};
|
||||
|
||||
for (var i = 0; i < timepoints.length; i++) {
|
||||
var timepointId = timepoints[i].timepointID;
|
||||
var lesionTimepointId = lesionData.timepointID;
|
||||
|
||||
var timepointObject;
|
||||
if (timepointId === lesionTimepointId) {
|
||||
// Add real mesurement
|
||||
timepointObject = {
|
||||
longestDiameter: lesionData.measurementText,
|
||||
imageId: lesionData.imageId,
|
||||
};
|
||||
} else {
|
||||
// Add null measurement
|
||||
timepointObject = {
|
||||
longestDiameter: "",
|
||||
imageId: ""
|
||||
};
|
||||
}
|
||||
timepointsObject[timepointId] = timepointObject;
|
||||
}
|
||||
|
||||
var lesionDataObject = {
|
||||
lesionUID: uuid.v4(),
|
||||
number: Measurements.find().count() + 1,
|
||||
lesionNumber: lesionData.lesionNumber,
|
||||
isTarget: lesionData.isTarget,
|
||||
locationUID: lesionData.locationUID,
|
||||
location: getLocationName(lesionData.locationUID),
|
||||
timepoints: timepointsObject
|
||||
};
|
||||
Measurements.insert(lesionDataObject);
|
||||
}
|
||||
|
||||
// Update timepoint data in Measurements collection
|
||||
function updateTimepointData(lesionData) {
|
||||
// Find the specific lesion to be updated
|
||||
var measurement = Measurements.find({
|
||||
lesionNumber: lesionData.lesionNumber,
|
||||
isTarget: lesionData.isTarget
|
||||
}).fetch()[0];
|
||||
|
||||
// If no such lesion exists, stop here
|
||||
if (!measurement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update this specific lesion at the given timepoint
|
||||
var timepointID = lesionData.timepointID;
|
||||
|
||||
// Update timepoints from lesion data
|
||||
var timepoints = measurement.timepoints;
|
||||
timepoints[timepointID].longestDiameter = lesionData.measurementText;
|
||||
timepoints[timepointID].imageId = lesionData.imageId;
|
||||
|
||||
Measurements.update({
|
||||
lesionNumber: lesionData.lesionNumber,
|
||||
isTarget: lesionData.isTarget
|
||||
}, {
|
||||
$set: {
|
||||
timepoints: timepoints
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check timepointData is found in Measurements collection
|
||||
function hasTimepointData(lesionData) {
|
||||
var timepointData = Measurements.findOne({
|
||||
lesionNumber: lesionData.lesionNumber,
|
||||
isTarget: lesionData.isTarget
|
||||
});
|
||||
if (timepointData) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Adds new timepoint item to tiemepoints array
|
||||
function addLesionData(lesionData) {
|
||||
if (hasTimepointData(lesionData)) {
|
||||
// Update data
|
||||
updateTimepointData(lesionData);
|
||||
} else {
|
||||
// Insert data
|
||||
addTimepointData(lesionData);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns new lesion number according to timepointID
|
||||
function getNewLesionNumber(timepointID, isTarget) {
|
||||
// Get all current lesion measurements
|
||||
var measurements = Measurements.find({isTarget: isTarget}).fetch();
|
||||
|
||||
// If no measurements exist yet, start at 1
|
||||
if (!measurements.length) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If measurements exist, find the last lesion number
|
||||
// from the given timepoint
|
||||
var lesionNumberCounter = 0;
|
||||
for (var i = 0; i < measurements.length; i++) {
|
||||
var measurement = measurements[i];
|
||||
var timepoints = measurement.timepoints;
|
||||
|
||||
if (timepoints[timepointID].longestDiameter === '') {
|
||||
return measurement.lesionNumber;
|
||||
} else {
|
||||
lesionNumberCounter = lesionNumberCounter + 1;
|
||||
}
|
||||
}
|
||||
return lesionNumberCounter + 1;
|
||||
}
|
||||
|
||||
// If lesion number is added for any timepoint, returns lesion locationUID
|
||||
function lesionNumberExists(lesionData) {
|
||||
if (!hasTimepointData(lesionData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var measurement = Measurements.find({
|
||||
lesionNumber: lesionData.lesionNumber,
|
||||
isTarget: lesionData.isTarget
|
||||
}).fetch()[0];
|
||||
|
||||
return measurement.locationUID;
|
||||
}
|
||||
|
||||
return {
|
||||
addLesionData: addLesionData,
|
||||
getNewLesionNumber: getNewLesionNumber,
|
||||
lesionNumberExists: lesionNumberExists,
|
||||
updateTimepointData: updateTimepointData,
|
||||
getLocationName: getLocationName
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,490 @@
|
||||
(function($, cornerstone, cornerstoneMath, cornerstoneTools) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var toolType = 'nonTarget';
|
||||
|
||||
var configuration = {
|
||||
setLesionNumberCallback: setLesionNumberCallback,
|
||||
getNonTargetLesionLocationCallback: getNonTargetLesionLocationCallback,
|
||||
changeNonTargetLesionLocationCallback: changeNonTargetLesionLocationCallback,
|
||||
drawHandles: false,
|
||||
drawHandlesOnHover: true,
|
||||
arrowFirst: true
|
||||
};
|
||||
|
||||
// Set lesion number
|
||||
// Get Non-Target lesions on image
|
||||
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
|
||||
var lesionNumber = 1;
|
||||
doneCallback(lesionNumber);
|
||||
}
|
||||
// Define a callback to get your text annotation
|
||||
// This could be used, e.g. to open a modal
|
||||
function getNonTargetLesionLocationCallback(measurementData, eventData, doneCallback) {
|
||||
doneCallback(prompt('Enter your lesion location:'));
|
||||
}
|
||||
|
||||
function changeNonTargetLesionLocationCallback(measurementData, eventData, doneCallback) {
|
||||
doneCallback(prompt('Change your lesion location:'));
|
||||
}
|
||||
|
||||
/// --- Mouse Tool --- ///
|
||||
///////// BEGIN ACTIVE TOOL ///////
|
||||
function addNewMeasurement(mouseEventData) {
|
||||
|
||||
var element = mouseEventData.element;
|
||||
|
||||
function doneCallback(lesionNumber) {
|
||||
measurementData.lesionName = "Non-Target "+lesionNumber;
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
measurementData.active = true;
|
||||
cornerstone.updateImage(element);
|
||||
}
|
||||
|
||||
var measurementData = createNewMeasurement(mouseEventData);
|
||||
|
||||
var eventData = {
|
||||
mouseButtonMask: mouseEventData.which
|
||||
};
|
||||
|
||||
// associate this data with this imageId so we can render it and manipulate it
|
||||
cornerstoneTools.addToolState(mouseEventData.element, toolType, measurementData);
|
||||
|
||||
// since we are dragging to another place to drop the end point, we can just activate
|
||||
// the end point and let the moveHandle move it for us.
|
||||
$(mouseEventData.element).off('CornerstoneToolsMouseMove', cornerstoneTools.nonTarget.mouseMoveCallback);
|
||||
$(mouseEventData.element).off('CornerstoneToolsMouseDown', cornerstoneTools.nonTarget.mouseDownCallback);
|
||||
$(mouseEventData.element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.nonTarget.mouseDownActivateCallback);
|
||||
|
||||
var config = cornerstoneTools.nonTarget.getConfiguration();
|
||||
|
||||
// Set lesion number and lesion name
|
||||
if (measurementData.lesionName === undefined) {
|
||||
config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
|
||||
}
|
||||
|
||||
cornerstone.updateImage(element);
|
||||
|
||||
cornerstoneTools.moveNewHandle(mouseEventData, measurementData.handles.end, function() {
|
||||
if (cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
|
||||
// delete the measurement
|
||||
cornerstoneTools.removeToolState(mouseEventData.element, toolType, measurementData);
|
||||
}
|
||||
|
||||
config.getNonTargetLesionLocationCallback(measurementData, mouseEventData, doneCallback);
|
||||
|
||||
$(mouseEventData.element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.nonTarget.mouseMoveCallback);
|
||||
$(mouseEventData.element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.nonTarget.mouseDownCallback);
|
||||
$(mouseEventData.element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.nonTarget.mouseDownActivateCallback);
|
||||
|
||||
cornerstone.updateImage(mouseEventData.element);
|
||||
});
|
||||
}
|
||||
|
||||
function createNewMeasurement(mouseEventData) {
|
||||
// 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: false
|
||||
},
|
||||
textBox: {
|
||||
x: mouseEventData.currentPoints.image.x - 50,
|
||||
y: mouseEventData.currentPoints.image.y - 50,
|
||||
pointNearHandle: pointNearTextBox,
|
||||
active: false,
|
||||
movesIndependently: true,
|
||||
drawnIndependently: true
|
||||
}
|
||||
},
|
||||
imageId: mouseEventData.image.imageId,
|
||||
measurementText: '',
|
||||
isTarget: false,
|
||||
uid: uuid.v4()
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (pointNearTextBox(element, data.handles.textBox, coords)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return distanceToPoint < 25;
|
||||
}
|
||||
|
||||
function pointNearTextBox(element, handle, coords) {
|
||||
if (!handle.boundingBox) {
|
||||
return;
|
||||
}
|
||||
return cornerstoneMath.point.insideRect(coords, handle.boundingBox);
|
||||
}
|
||||
|
||||
function drawArrow(context, start, end, color, lineWidth) {
|
||||
//variables to be used when creating the arrow
|
||||
var headLength = 10;
|
||||
|
||||
var angle = Math.atan2(end.y - start.y, end.x - start.x);
|
||||
|
||||
//starting path of the arrow from the start square to the end square and drawing the stroke
|
||||
context.beginPath();
|
||||
context.moveTo(start.x, start.y);
|
||||
context.lineTo(end.x, end.y);
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = lineWidth;
|
||||
context.stroke();
|
||||
|
||||
//starting a new path from the head of the arrow to one of the sides of the point
|
||||
context.beginPath();
|
||||
context.moveTo(end.x, end.y);
|
||||
context.lineTo(end.x - headLength * Math.cos(angle - Math.PI / 7), end.y - headLength * Math.sin(angle - Math.PI / 7));
|
||||
|
||||
//path from the side point of the arrow, to the other side point
|
||||
context.lineTo(end.x - headLength * Math.cos(angle + Math.PI / 7), end.y - headLength * Math.sin(angle + Math.PI / 7));
|
||||
|
||||
//path from the side point back to the tip of the arrow, and then again to the opposite side point
|
||||
context.lineTo(end.x, end.y);
|
||||
context.lineTo(end.x - headLength * Math.cos(angle - Math.PI / 7), end.y - headLength * Math.sin(angle - Math.PI / 7));
|
||||
|
||||
//draws the paths created above
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = lineWidth;
|
||||
context.stroke();
|
||||
context.fillStyle = color;
|
||||
context.fill();
|
||||
}
|
||||
|
||||
function suscribeNonTargetToolModifiedEvent(element) {
|
||||
var elementEvents = $._data(element, "events");
|
||||
var index = Object.keys(elementEvents).indexOf("NonTargetToolSelected");
|
||||
if (index < 0) {
|
||||
// Subscribe LesionToolModified and calls measurementModified function when lesion measurement is changed or updated.
|
||||
$(element).on("NonTargetToolSelected", measurementModified);
|
||||
}
|
||||
|
||||
}
|
||||
///////// BEGIN IMAGE RENDERING ///////
|
||||
function onImageRendered(e, eventData) {
|
||||
|
||||
suscribeNonTargetToolModifiedEvent(e.currentTarget);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
updateLesions(toolData, eventData);
|
||||
}
|
||||
|
||||
function updateLesions(toolData, eventData) {
|
||||
// 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);
|
||||
|
||||
for (var i = 0; i < toolData.data.length; i++) {
|
||||
renderLesion(toolData.data[i], context, eventData);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLesion(lesion, context, eventData) {
|
||||
context.save();
|
||||
|
||||
var color;
|
||||
var lineWidth = cornerstoneTools.toolStyle.getToolWidth();
|
||||
var font = cornerstoneTools.textStyle.getFont();
|
||||
var config = cornerstoneTools.nonTarget.getConfiguration();
|
||||
|
||||
// configurable shadow from CornerstoneTools
|
||||
if (config && config.shadow) {
|
||||
context.shadowColor = '#000000';
|
||||
context.shadowOffsetX = +1;
|
||||
context.shadowOffsetY = +1;
|
||||
}
|
||||
|
||||
if (lesion.active) {
|
||||
color = cornerstoneTools.toolColors.getActiveColor();
|
||||
} else {
|
||||
color = cornerstoneTools.toolColors.getToolColor();
|
||||
}
|
||||
|
||||
// Draw the arrow
|
||||
var handleStartCanvas = cornerstone.pixelToCanvas(eventData.element, lesion.handles.start);
|
||||
var handleEndCanvas = cornerstone.pixelToCanvas(eventData.element, lesion.handles.end);
|
||||
var canvasTextLocation = cornerstone.pixelToCanvas(eventData.element, lesion.handles.textBox);
|
||||
|
||||
if (config.arrowFirst) {
|
||||
drawArrow(context, handleEndCanvas, handleStartCanvas, color, lineWidth);
|
||||
} else {
|
||||
drawArrow(context, handleStartCanvas, handleEndCanvas, color, lineWidth);
|
||||
}
|
||||
|
||||
if (config.drawHandles) {
|
||||
cornerstoneTools.drawHandles(context, eventData, lesion.handles, color);
|
||||
} else if (config.drawHandlesOnHover && lesion.handles.start.active) {
|
||||
cornerstoneTools.drawHandles(context, eventData, [ lesion.handles.start ], color);
|
||||
} else if (config.drawHandlesOnHover && lesion.handles.end.active) {
|
||||
cornerstoneTools.drawHandles(context, eventData, [ lesion.handles.end ], color);
|
||||
}
|
||||
|
||||
//Draw linked line as dashed
|
||||
var mid = {
|
||||
x: (handleStartCanvas.x + handleEndCanvas.x) / 2,
|
||||
y: (handleStartCanvas.y + handleEndCanvas.y) / 2
|
||||
};
|
||||
context.setLineDash([2, 3]);
|
||||
context.beginPath();
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = 1 / eventData.viewport.scale;
|
||||
var mid = {
|
||||
x: mid.x,
|
||||
y: mid.y
|
||||
};
|
||||
|
||||
context.moveTo(mid.x, mid.y);
|
||||
context.lineTo(canvasTextLocation.x + 20, canvasTextLocation.y + 20);
|
||||
context.stroke();
|
||||
|
||||
|
||||
// Draw the text
|
||||
if (lesion.lesionName && lesion.lesionName !== '') {
|
||||
context.font = font;
|
||||
var boundingBox = cornerstoneTools.drawTextBox(context, lesion.lesionName, canvasTextLocation.x, canvasTextLocation.y, color);
|
||||
lesion.handles.textBox.boundingBox = boundingBox;
|
||||
}
|
||||
|
||||
|
||||
context.restore();
|
||||
|
||||
}
|
||||
// ---- Touch tool ----
|
||||
|
||||
///////// BEGIN ACTIVE TOOL ///////
|
||||
function addNewMeasurementTouch(touchEventData) {
|
||||
var element = touchEventData.element;
|
||||
|
||||
function doneCallback(lesionNumber) {
|
||||
measurementData.lesionName = "Non-Target "+lesionNumber;
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
measurementData.active = true;
|
||||
cornerstone.updateImage(element);
|
||||
}
|
||||
|
||||
var measurementData = createNewMeasurement(touchEventData);
|
||||
cornerstoneTools.addToolState(element, toolType, measurementData);
|
||||
$(element).off('CornerstoneToolsTouchDrag', cornerstoneTools.nonTargetTouch.touchMoveHandle);
|
||||
$(element).off('CornerstoneToolsDragStartActive', cornerstoneTools.nonTargetTouch.touchDownActivateCallback);
|
||||
$(element).off('CornerstoneToolsTap', cornerstoneTools.nonTargetTouch.tapCallback);
|
||||
var config = cornerstoneTools.nonTarget.getConfiguration();
|
||||
|
||||
// Set lesion number and lesion name
|
||||
if (measurementData.lesionName === undefined) {
|
||||
config.setLesionNumberCallback(measurementData, touchEventData, doneCallback);
|
||||
}
|
||||
cornerstone.updateImage(element);
|
||||
|
||||
cornerstoneTools.moveNewHandleTouch(touchEventData, measurementData.handles.end, function() {
|
||||
cornerstone.updateImage(element);
|
||||
|
||||
if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
|
||||
// delete the measurement
|
||||
cornerstoneTools.removeToolState(element, toolType, measurementData);
|
||||
}
|
||||
|
||||
config.getNonTargetLesionLocationCallback(measurementData, touchEventData, doneCallback);
|
||||
|
||||
|
||||
$(element).on('CornerstoneToolsTouchDrag', cornerstoneTools.nonTargetTouch.touchMoveHandle);
|
||||
$(element).on('CornerstoneToolsDragStartActive', cornerstoneTools.nonTargetTouch.touchDownActivateCallback);
|
||||
$(element).on('CornerstoneToolsTap', cornerstoneTools.nonTargetTouch.tapCallback);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function doubleClickCallback(e, eventData) {
|
||||
var element = eventData.element;
|
||||
var data;
|
||||
|
||||
function doneChangingTextCallback(data, updatedText, deleteTool) {
|
||||
if (deleteTool === true) {
|
||||
cornerstoneTools.removeToolState(element, toolType, data);
|
||||
} else {
|
||||
data.text = updatedText;
|
||||
}
|
||||
|
||||
data.active = false;
|
||||
cornerstone.updateImage(element);
|
||||
}
|
||||
|
||||
if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var config = cornerstoneTools.nonTarget.getConfiguration();
|
||||
|
||||
var coords = eventData.currentPoints.canvas;
|
||||
var toolData = cornerstoneTools.getToolState(element, toolType);
|
||||
|
||||
// now check to see if there is a handle we can move
|
||||
if (!toolData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < toolData.data.length; i++) {
|
||||
data = toolData.data[i];
|
||||
if (pointNearTool(element, data, coords)) {
|
||||
data.active = true;
|
||||
cornerstone.updateImage(element);
|
||||
// Allow relabelling via a callback
|
||||
config.changeTextCallback(data, doneChangingTextCallback);
|
||||
|
||||
e.stopImmediatePropagation();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false; // false = causes jquery to preventDefault() and stopPropagation() this event
|
||||
}
|
||||
|
||||
function updateLesion(e, eventObject) {
|
||||
var start = new Date();
|
||||
|
||||
var enabledElement = eventObject.enabledElement;
|
||||
var isTarget = eventObject.lesionData.isTarget;
|
||||
var lesionNumber = eventObject.lesionData.lesionNumber;
|
||||
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.lesionNumber === eventObject.lesionNumber && eventObject.type !== "active" && !isTarget) {
|
||||
data.visible = false;
|
||||
deletedDataIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedDataIndex >= 0 && deletedDataIndex < toolData.data.length) {
|
||||
toolData.data.splice(deletedDataIndex, 1);
|
||||
}
|
||||
} else if (type === "active") {
|
||||
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.lesionNumber === eventObject.lesionData.lesionNumber && eventObject.type === "active" && !isTarget) {
|
||||
data.active = true;
|
||||
} else {
|
||||
data.active = false;
|
||||
}
|
||||
}
|
||||
} else if (type === "inactive") {
|
||||
for (var i = 0; i < toolData.data.length; i++) {
|
||||
var data = toolData.data[i];
|
||||
// Make inactive all lesions for the timepoint
|
||||
data.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
lesionNumber: lesionNumber,
|
||||
isTarget: isTarget,
|
||||
type: type //Holds image will be deleted or active
|
||||
};
|
||||
|
||||
enabledElement.invalid = false;
|
||||
|
||||
onImageRendered(e, eventData);
|
||||
}
|
||||
|
||||
function loadImage(e, eventObject) {
|
||||
// If type is active, load image and activate lesion
|
||||
// If type is inactive, update lesions of enabledElement as inactive
|
||||
|
||||
if (eventObject.type === "active") {
|
||||
var stackToolDataSource = cornerstoneTools.getToolState(e.currentTarget, 'stack');
|
||||
var stackData = stackToolDataSource.data[0];
|
||||
var imageIdsArr = stackData.imageIds;
|
||||
var indexOfImage = imageIdsArr.indexOf(eventObject.lesionData.imageId);
|
||||
if (indexOfImage > -1) {
|
||||
cornerstone.loadAndCacheImage(stackData.imageIds[indexOfImage]).then(function(image) {
|
||||
cornerstone.displayImage(eventObject.enabledElement.element, image);
|
||||
updateLesion(e, eventObject);
|
||||
});
|
||||
}
|
||||
} else if (eventObject.type === "inactive") {
|
||||
updateLesion(e, eventObject);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//This function is called from cornerstone-viewport.html and updates lesion measurement and makes the lesion active
|
||||
function measurementModified(e, eventObject) {
|
||||
loadImage(e, eventObject);
|
||||
}
|
||||
|
||||
cornerstoneTools.nonTarget = cornerstoneTools.mouseButtonTool({
|
||||
addNewMeasurement: addNewMeasurement,
|
||||
createNewMeasurement: createNewMeasurement,
|
||||
onImageRendered: onImageRendered,
|
||||
pointNearTool: pointNearTool,
|
||||
toolType: toolType
|
||||
//mouseDoubleClickCallback: doubleClickCallback
|
||||
});
|
||||
|
||||
cornerstoneTools.nonTarget.setConfiguration(configuration);
|
||||
|
||||
cornerstoneTools.nonTargetTouch = cornerstoneTools.touchTool({
|
||||
addNewMeasurement: addNewMeasurementTouch,
|
||||
createNewMeasurement: createNewMeasurement,
|
||||
onImageRendered: onImageRendered,
|
||||
pointNearTool: pointNearTool,
|
||||
toolType: toolType
|
||||
// pressCallback: doubleClickCallback
|
||||
});
|
||||
|
||||
|
||||
})($, cornerstone, cornerstoneMath, cornerstoneTools);
|
||||
@@ -1,3 +0,0 @@
|
||||
<template name="lesionDialog">
|
||||
{{> lesionLocationDialog}}
|
||||
</template>
|
||||
@@ -1,46 +0,0 @@
|
||||
Template.lesionDialog.onRendered(function () {
|
||||
// Show Lesion Dialog
|
||||
$(document).on("ShowLesionDialog", function (e, eventData) {
|
||||
var dialogPointsOnPage = eventData.currentPoints.page;
|
||||
$("#modal-dialog-container").css({
|
||||
"top": dialogPointsOnPage.y,
|
||||
"left": dialogPointsOnPage.x
|
||||
});
|
||||
|
||||
$("#lesionDialog").modal("show");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Listen lesionMeaurementCreated Event and when this event is triggered, show lesionLocationDialog Box
|
||||
/*
|
||||
$(document).on("lesionMeasurementCreated",function(e,eventCaller,data){
|
||||
console.log("Lesion measurement xreated");
|
||||
var element = data.element;
|
||||
var tabId = data.tabId;
|
||||
var viewports = $("#content"+tabId).find(".imageViewerViewport");
|
||||
var viewportIndex = $(viewports).index($(element));
|
||||
data.viewportIndex = viewportIndex;
|
||||
data.totalViewportCount = viewports.length;
|
||||
|
||||
//Set Lesion Table Data to insert into lesion table
|
||||
setLesionTableData(data);
|
||||
|
||||
// Check popup will shown or not
|
||||
var returnVal = lesionNumberIsAdded(data);
|
||||
if(!returnVal.isFound) {
|
||||
if( !Session.get("lesionLocationSelected") ) {
|
||||
$("#modal-dialog-container").css({
|
||||
"top": eventCaller.pageY,
|
||||
"left": eventCaller.pageX
|
||||
});
|
||||
|
||||
$("#lesionDialog").modal("show");
|
||||
}
|
||||
} else {
|
||||
// If lesionNumber is added before:
|
||||
data.rowIndex = returnVal.rowIndex;
|
||||
$(document).trigger("addLesionToExistedRow",data);
|
||||
}
|
||||
});*/
|
||||
@@ -1,19 +1,17 @@
|
||||
<template name="lesionLocationDialog">
|
||||
<div class="modal fade" id="lesionDialog" style=" background-color: transparent;">
|
||||
<div id="modal-dialog-container" class="modal-dialog modal-sm" style="position: absolute; border: none; ">
|
||||
<div class="modal-content" style="padding: 10px;">
|
||||
<div class="dialogHeader">
|
||||
<div style="float:left;"><h4>Select Lesion Location</h4></div>
|
||||
<div style="float:right;"><button class="btn" id="btnCloseLesionPopup" style="outline: 0; border: none; background-color: transparent;"><i class="fa fa-close"></i></button></div>
|
||||
</div>
|
||||
<div class="dialogContent" style="margin-bottom: 20px;">
|
||||
<div class="lesionLocation">
|
||||
<label>Lesion Location</label>
|
||||
<select id="selectLesionLocation" style="margin-top:5px;">
|
||||
<option value="-1"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lesionLocationDialog">
|
||||
<div class="dialogHeader">
|
||||
<h4>Select Lesion Location</h4>
|
||||
</div>
|
||||
<div class="dialogContent">
|
||||
<div class="lesionLocation">
|
||||
<label>Lesion Location</label>
|
||||
<select id="selectLesionLocation">
|
||||
<option value="-1"></option>
|
||||
{{ #each lesionLocations}}
|
||||
<option value='{{_id}}'>{{location}}</option>
|
||||
{{ /each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +1,172 @@
|
||||
//fill selectLesionLocation element
|
||||
var lesionLocationsArray = [
|
||||
{location:"Brain Brainstem",hasDescription:false, description:""},
|
||||
{location:"Brain Cerebellum Left",hasDescription:false, description:""},
|
||||
{location:"Brain Cerebrum Left",hasDescription:false, description:""},
|
||||
{location:"Brain Cerebrum Right",hasDescription:false, description:""},
|
||||
{location:"Brain Multiple Sites",hasDescription:false, description:""}
|
||||
];
|
||||
// This event determines whether or not to show the lesion dialog
|
||||
// If there already exists a lesion with this specific lesion number,
|
||||
// related to the chosen location.
|
||||
function getLesionLocationCallback(measurementData, eventData, doneCallback) {
|
||||
// Get the lesion location dialog
|
||||
var lesionDialog = $("#lesionLocationDialog");
|
||||
|
||||
Template.lesionLocationDialog.onRendered(function () {
|
||||
// Find the select option box
|
||||
var selector = lesionDialog.find("select#selectLesionLocation");
|
||||
|
||||
function fillSelectLesionLocation () {
|
||||
var el = $("#selectLesionLocation");
|
||||
el.find('option:not(:first)').remove();
|
||||
$.each(lesionLocationsArray, function(key, value) {
|
||||
el.append("<option value='" + key+ "'>" + value.location + "</option>");
|
||||
});
|
||||
// Get the current element's timepointID
|
||||
// TODO: Change this when we are no longer storing timepointID in the viewport element DOM data
|
||||
measurementData.timepointID = $(eventData.element).data('timepointID');
|
||||
|
||||
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
|
||||
// exists at a different timepoint
|
||||
var lesionNumber = measurementManagerDAL.getNewLesionNumber(measurementData.timepointID, isTarget=true);
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
|
||||
// Find out if this lesion number is already added in the lesion manager for another timepoint
|
||||
// If it is, stop here because we don't need the dialog.
|
||||
var locationUID = measurementManagerDAL.lesionNumberExists(measurementData);
|
||||
if (locationUID) {
|
||||
measurementData.locationUID = locationUID;
|
||||
measurementManagerDAL.updateTimepointData(measurementData);
|
||||
closeHandler();
|
||||
return;
|
||||
}
|
||||
// If it isn't, continue to open the dialog and have the user choose a lesion location
|
||||
|
||||
// Attach close handler if the user clicks the close button
|
||||
var close = lesionDialog.find("#btnCloseLesionPopup");
|
||||
close.off('click');
|
||||
close.on('click', function() {
|
||||
closeHandler();
|
||||
});
|
||||
|
||||
function closeHandler() {
|
||||
// Hide the lesion dialog
|
||||
lesionDialog.css('display', 'none');
|
||||
|
||||
// Fire the doneCallback with the lesion number
|
||||
doneCallback(lesionNumber);
|
||||
|
||||
// Get the current value of the select option box
|
||||
var currentValue = selector.find("option:selected").val();
|
||||
|
||||
// Select the first option for the next time the dialog is opened
|
||||
selector.val(currentValue);
|
||||
}
|
||||
|
||||
// Fill dropdown
|
||||
fillSelectLesionLocation();
|
||||
});
|
||||
// Attach keypress handlers so the user can close with the Enter button
|
||||
lesionDialog.off("keypress");
|
||||
lesionDialog.on('keypress', keyPressHandler);
|
||||
|
||||
Template.lesionLocationDialog.events({
|
||||
'click button#btnCloseLesionPopup': function (e) {
|
||||
$("#lesionDialog").modal("hide");
|
||||
},
|
||||
|
||||
'change select#selectLesionLocation': function (e) {
|
||||
var el = $(e.target);
|
||||
var selectedLocationIndex = el.val()
|
||||
if(selectedLocationIndex !== "-1"){
|
||||
|
||||
var locationObj = lesionLocationsArray[selectedLocationIndex];
|
||||
|
||||
// Trigger location selected event
|
||||
$(document).trigger("lesionLocationSelected",locationObj);
|
||||
|
||||
// Set activeModule parameters in index.html
|
||||
$("#lesionDialog").modal("hide");
|
||||
|
||||
// Select first option
|
||||
el.val($("#selectLesionLocation option:first").val());
|
||||
|
||||
// Set lesion location selected session to prevent open
|
||||
Session.set("lesionLocationSelected", true);
|
||||
// This is the keypress callback function
|
||||
function keyPressHandler(e) {
|
||||
// If Enter is pressed, close the dialog
|
||||
if (e.which === 13) {
|
||||
closeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Show the lesion location dialog above
|
||||
lesionDialog.css({
|
||||
top: eventData.currentPoints.page.y - lesionDialog.outerHeight() - 40,
|
||||
left: eventData.currentPoints.page.x - lesionDialog.outerWidth() / 2,
|
||||
display: 'block'
|
||||
});
|
||||
|
||||
// Attach a callback for the select box
|
||||
selector.off('change');
|
||||
selector.on('change', function(e) {
|
||||
// Get the current value of the selector
|
||||
var selectedOptionId = this.value;
|
||||
|
||||
// If the selected option is still the default (-1)
|
||||
// then stop here
|
||||
if (selectedOptionId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selected location data
|
||||
var locationObj = LesionLocations.findOne({_id: selectedOptionId});
|
||||
|
||||
|
||||
var id;
|
||||
var existingLocation = PatientLocations.findOne({location: locationObj.location});
|
||||
if (existingLocation) {
|
||||
id = existingLocation._id;
|
||||
} else {
|
||||
// Adds location data to PatientLocation and retrieve the location ID
|
||||
id = PatientLocations.insert({location: locationObj.location});
|
||||
}
|
||||
|
||||
// Link locationUID with active lesion measurementData
|
||||
measurementData.locationUID = id;
|
||||
|
||||
/// Set the isTarget value to true, since this is the target-lesion dialog callback
|
||||
measurementData.isTarget = true;
|
||||
|
||||
// Adds lesion data to timepoints array
|
||||
measurementManagerDAL.addLesionData(measurementData);
|
||||
|
||||
// Close the dialog
|
||||
closeHandler();
|
||||
});
|
||||
}
|
||||
|
||||
function changeLesionLocationCallback(measurementData, eventData, doneCallback) {
|
||||
doneCallback(prompt('Change your lesion location:'));
|
||||
}
|
||||
|
||||
var config = {
|
||||
getLesionLocationCallback: getLesionLocationCallback,
|
||||
changeLesionLocationCallback: changeLesionLocationCallback
|
||||
};
|
||||
|
||||
cornerstoneTools.lesion.setConfiguration(config);
|
||||
|
||||
|
||||
LesionLocations = new Meteor.Collection(null);
|
||||
|
||||
LesionLocations.insert({
|
||||
location: "Brain Brainstem",
|
||||
hasDescription: false,
|
||||
description: ""
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
location: "Brain Cerebellum Left",
|
||||
hasDescription: false,
|
||||
description: ""
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
location: "Brain Cerebrum Left",
|
||||
hasDescription: false,
|
||||
description: ""
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
location: "Brain Cerebrum Right",
|
||||
hasDescription: false,
|
||||
description: ""
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
location: "Brain Multiple Sites",
|
||||
hasDescription: false,
|
||||
description: ""
|
||||
});
|
||||
|
||||
var lastAddedLesionData;
|
||||
|
||||
Template.lesionLocationDialog.onRendered(function() {
|
||||
// Observe Measurements Collection Changes
|
||||
Measurements.find().observe({
|
||||
added: function(lesionData) {
|
||||
lastAddedLesionData = lesionData;
|
||||
},
|
||||
changed: function(lesionData) {
|
||||
console.log("lesionData has changed!");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.lesionLocationDialog.helpers({
|
||||
'lesionLocations': function() {
|
||||
return LesionLocations.find();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
#lesionLocationDialog
|
||||
display: none
|
||||
position: absolute
|
||||
z-index: 100
|
||||
background: white
|
||||
padding: 10px
|
||||
border-radius: 10px
|
||||
|
||||
#closeLesionPopup
|
||||
outline: 0
|
||||
border: none
|
||||
background-color: transparent
|
||||
|
||||
#selectLesionLocation
|
||||
margin-top: 5px
|
||||
|
||||
.dialogContent
|
||||
margin-bottom: 20px
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
<template name="lesionTable">
|
||||
<div id="lesionTableContainer">
|
||||
<table class="table table-bordered lesionTable" id="tblLesion">
|
||||
<table class="table table-striped noselect lesionTable" id="tblLesion">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="thNumber">Lesion #</th>
|
||||
<th id="thTarget">Target</th>
|
||||
<th id="thLocation">Location</th>
|
||||
<th id="thTarget">Target</th>
|
||||
<!--Each time point name as a column-->
|
||||
{{ #each timepointNames }}
|
||||
{{ >lesionTableTimepointHeader }}
|
||||
{{ #each timepoints }}
|
||||
{{ >lesionTableTimepointHeader }}
|
||||
{{ /each }}
|
||||
<!--What is this for?-->
|
||||
<!--<th id="thSpacer"> </th>-->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!--Each measurement as row-->
|
||||
{{ #each measurement }}
|
||||
{{ >lesionTableRow }}
|
||||
{{ /each }}
|
||||
<!--Each measurement as row-->
|
||||
<!--Each time point as a column-->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,187 +1,95 @@
|
||||
measurements = new Meteor.Collection(null);
|
||||
Measurements = new Meteor.Collection(null);
|
||||
Timepoints = new Meteor.Collection(null);
|
||||
|
||||
var test = {
|
||||
lesionNumber: 1,
|
||||
isTarget: true,
|
||||
location: 'Brain',
|
||||
timepoints: {
|
||||
"now": {
|
||||
imageId: "blah",
|
||||
timepointUid: "timepointX",
|
||||
date: 2010,
|
||||
isDirty: true,
|
||||
longestDiameter: 5
|
||||
},
|
||||
"earlier": {
|
||||
imageId: "blah2",
|
||||
timepointUid: "timepointy",
|
||||
date: 2010,
|
||||
isDirty: true,
|
||||
longestDiameter: 8
|
||||
// Activate selected lesions when lesion table row is clicked
|
||||
function updateLesions(e) {
|
||||
// lesionNumber of measurement = id of row
|
||||
var lesionNumber = parseInt($(e.currentTarget).attr("id"), 10);
|
||||
var isTarget = $(e.currentTarget).find('td').eq(2).html().trim() === 'N'?false:true;
|
||||
|
||||
// Find data for specific lesion
|
||||
var measurementData = Measurements.find({
|
||||
lesionNumber: lesionNumber,
|
||||
isTarget: isTarget
|
||||
}).fetch()[0];
|
||||
|
||||
var timepoints = measurementData.timepoints;
|
||||
|
||||
$(".imageViewerViewport").each(function(index, element) {
|
||||
// Get the timepointID related to the image viewer viewport
|
||||
// from the DOM itself. This will be changed later when a
|
||||
// real association between viewports and timepoints is created.
|
||||
var timepointID = $(element).data('timepointID');
|
||||
var timepointObject = timepoints[timepointID];
|
||||
|
||||
// Defines event data
|
||||
var eventData = {
|
||||
enabledElement: cornerstone.getEnabledElement(element),
|
||||
lesionData: {
|
||||
isTarget: isTarget,
|
||||
lesionNumber: lesionNumber,
|
||||
imageId: timepointObject.imageId
|
||||
},
|
||||
type: "active"
|
||||
};
|
||||
|
||||
if (timepointObject.longestDiameter === "") {
|
||||
eventData.type = "inactive";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
measurements.insert(test);
|
||||
if (!isTarget) {
|
||||
$(element).trigger("NonTargetToolSelected", eventData);
|
||||
|
||||
// Deactivate lesion tool measurements
|
||||
eventData.type = "inactive";
|
||||
$(element).trigger("LesionToolSelected", eventData);
|
||||
return;
|
||||
}
|
||||
$(element).trigger("LesionToolSelected", eventData);
|
||||
|
||||
// Deactivate nonTarget tool measurements
|
||||
eventData.type = "inactive";
|
||||
$(element).trigger("NonTargetToolSelected", eventData);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Template.lesionTable.onRendered(function() {
|
||||
// For the moment we will associate the timepoint
|
||||
// with the viewport element by storing the timepointID
|
||||
// inside the element's DOM data. This is temporary.
|
||||
$(".imageViewerViewport").each(function(index, element) {
|
||||
var timepointID = uuid.v4();
|
||||
|
||||
var timepointName = "Baseline";
|
||||
if (index > 0) {
|
||||
timepointName = "Current"; //"Follow Up "+i;
|
||||
}
|
||||
|
||||
|
||||
// FUTURE = On load series data into viewport, create a new timepoint
|
||||
// unless it already exists
|
||||
Timepoints.insert({
|
||||
timepointID: timepointID,
|
||||
timepointName: timepointName
|
||||
});
|
||||
|
||||
$(element).data('timepointID', timepointID);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Template.lesionTable.helpers({
|
||||
'measurement': function() {
|
||||
return measurements.find();
|
||||
return Measurements.find();
|
||||
},
|
||||
'timepointNames': function() {
|
||||
return [
|
||||
{name: 'now'},
|
||||
{name: 'earlier'}
|
||||
];
|
||||
'timepoints': function() {
|
||||
return Timepoints.find();
|
||||
}
|
||||
});
|
||||
|
||||
/*var lesionTableData;
|
||||
setLesionTableData = function (data) {
|
||||
lesionTableData = data;
|
||||
};
|
||||
|
||||
getLesionTableData = function () {
|
||||
return lesionTableData;
|
||||
};
|
||||
|
||||
lesionNumberIsAdded = function(data) {
|
||||
var tabId = data.tabId;
|
||||
var returnValue = {isFound: false, rowIndex : -1};
|
||||
$('#content'+tabId+' table tbody tr td.lesionNumber').each( function(){
|
||||
console.log($(this).text());
|
||||
var lesionNumber = parseInt($(this).text());
|
||||
|
||||
if(lesionNumber == data.lesionNumber) {
|
||||
var $tr = $(this).closest('tr');
|
||||
var rowIndex = $tr.index();
|
||||
returnValue.rowIndex = rowIndex;
|
||||
returnValue.isFound = true;
|
||||
}
|
||||
});
|
||||
|
||||
return returnValue;
|
||||
};
|
||||
|
||||
returnTblStr = function (obj) {
|
||||
// Get count of lds
|
||||
var tabId = obj.tabId;
|
||||
var lds = $("#content"+tabId).find(".lesionTable th.ld");
|
||||
|
||||
var rowStr = '<tr style="background-color: #b3b3b3;">' +
|
||||
'<td style="text-align:center;"><button type="button" class="btnRemove fa fa-times fa-lg" title="Remove"></button></td>' +
|
||||
'<td style="text-align:center;" class="lesionNumber">'+obj.lesionNumber+'</td>' +
|
||||
'<td style="text-align:center;">Y</td>';
|
||||
|
||||
// Add measurement to right column
|
||||
if(obj.totalViewportCount) {
|
||||
for(var i=0; i< lds.length; i++) {
|
||||
if(i === obj.viewportIndex) {
|
||||
rowStr += '<td>'+obj.lesionLocationObj.location+'</td><td id='+obj.lineIndex+' class="lesion" style="text-align: left;">'+obj.lesions+'</td>';
|
||||
|
||||
}else{
|
||||
rowStr += '<td>'+obj.lesionLocationObj.location+'</td><td class="lesion" style="text-align: left;"></td>';
|
||||
}
|
||||
}
|
||||
}
|
||||
rowStr += '</tr>';
|
||||
|
||||
return rowStr;
|
||||
};
|
||||
|
||||
//Link lesionLocationSelected event
|
||||
$(document).on("lesionLocationSelected",function(event,data ){
|
||||
var lesionData = getLesionTableData();
|
||||
lesionData.lesionLocationObj = data;
|
||||
var tabId = lesionData.tabId;
|
||||
//var el_index = lesionData.viewportIndex;
|
||||
var tableEl = $("#content"+tabId).find(".lesionTable");
|
||||
var tblStr = returnTblStr(lesionData);
|
||||
$(tableEl).append(tblStr);
|
||||
|
||||
});
|
||||
|
||||
//Link addLesionToExistedRow event
|
||||
$(document).on("addLesionToExistedRow",function(event,data ){
|
||||
// Get count of lds
|
||||
var tabId = data.tabId;
|
||||
var lds = $("#content"+tabId).find(".lesionTable th.ld");
|
||||
// Add measurement to right column
|
||||
for(var i=0; i< lds.length; i++) {
|
||||
if(i === data.viewportIndex) {
|
||||
var rowIndex = data.rowIndex + 1;
|
||||
var row = $("#content"+tabId).find(".lesionTable tr").eq(rowIndex);
|
||||
var cell = $(row).find("td.lesion").eq(data.viewportIndex);
|
||||
$(cell).attr("id", data.lineIndex);
|
||||
$(cell).html(data.lesions);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("lesionTextChanged", function(event, data){
|
||||
var cellId = data.index;
|
||||
$("td#"+cellId).html(data.measurementText);
|
||||
});
|
||||
|
||||
|
||||
Template.lesionTable.onRendered( function () {
|
||||
|
||||
// TODO: Set lesion table as hiding-panel
|
||||
console.log(this);
|
||||
var template = this;
|
||||
var tabId = this.data.tabId;
|
||||
var viewports = $("#content"+tabId).find(".imageViewerViewport");
|
||||
var lesionTable = $("#content"+tabId).find(".lesionTable");
|
||||
|
||||
var viewportCount = viewports.length;
|
||||
|
||||
var lesionTableWidth = $(lesionTable).width();
|
||||
var viewportWidth = viewports.width();
|
||||
var ldColumnRatio = viewportWidth / lesionTableWidth * 100;
|
||||
|
||||
// Add LD column as viewportCount
|
||||
// Set lesion table columns width
|
||||
|
||||
if(viewportCount > 0) {
|
||||
if(viewportCount == 1) {
|
||||
$('#content'+tabId+' thead tr').append( $('<th />', {text : 'Location', width: ldColumnRatio/2+'%', class:'thLocation'}) )
|
||||
$('#content'+tabId+' thead tr').append( $('<th />', {text : 'LD', width: ldColumnRatio/2+'%', class:'ld'}) )
|
||||
|
||||
} else if(viewportCount == 2) {
|
||||
|
||||
for(var i = 0; i< viewportCount; i++) {
|
||||
$('#content'+tabId+' thead tr').append( $('<th />', {text : 'Location'+i, width: ldColumnRatio/2+'%', class:'thLocation'}) )
|
||||
$('#content'+tabId+' thead tr').append( $('<th />', {text : 'LD'+i, width: ldColumnRatio/2+'%', class:'ld'}) )
|
||||
}
|
||||
|
||||
} else if(viewportCount > 2) {
|
||||
// TODO: Set when viewport count is bigger than 2
|
||||
}
|
||||
}
|
||||
// TODO: When layout is changed, add and calculate new columns for measurement
|
||||
});
|
||||
|
||||
Template.lesionTable.events({
|
||||
'click button.btnRemove': function (e) {
|
||||
var el = $(e.target);
|
||||
},
|
||||
'click table tbody tr': function(e) {
|
||||
console.log(this);
|
||||
var row = e.currentTarget;
|
||||
var lesionCells = $(row).find("td.lesion");
|
||||
for(var i = 0; i< lesionCells.length; i++) {
|
||||
var cell = lesionCells[i];
|
||||
var idAttr = $(cell).attr("id");
|
||||
if(idAttr != undefined) {
|
||||
// TODO: Create eventObject and trigger LesionToolModified event
|
||||
var viewports = $(".imageViewerViewport");
|
||||
var eventObject = {
|
||||
enabledElement: cornerstone.getEnabledElement(viewports[0]),
|
||||
lineIndex: parseInt(idAttr),
|
||||
type: "active"
|
||||
};
|
||||
$(viewports[0]).trigger("LesionToolModified", eventObject);
|
||||
}
|
||||
}
|
||||
'click table#tblLesion tbody tr': function(e) {
|
||||
updateLesions(e);
|
||||
}
|
||||
});*/
|
||||
});
|
||||
@@ -1,44 +1,76 @@
|
||||
#lesionTableContainer
|
||||
width: 100%
|
||||
height: 25%
|
||||
width: 100%
|
||||
height: 16%
|
||||
|
||||
.lesionTable
|
||||
|
||||
table.lesionTable
|
||||
thead
|
||||
th
|
||||
background-color: #424242
|
||||
font-style: italic
|
||||
td, th
|
||||
border: 1px solid white
|
||||
border-collapse: collapse
|
||||
|
||||
td.lesionNumber, th#thNumber
|
||||
width: 70px
|
||||
|
||||
td.location, th#thLocation
|
||||
width: 250px
|
||||
|
||||
td.target, th#thTarget
|
||||
width: 70px
|
||||
|
||||
td.lesionTableTimepointCell
|
||||
width: 70px
|
||||
|
||||
thead
|
||||
width: 100%
|
||||
display: block
|
||||
|
||||
tr
|
||||
width: 100%
|
||||
|
||||
th
|
||||
border-bottom-width: 0
|
||||
font-weight: normal
|
||||
color:white
|
||||
line-height: 15px
|
||||
text-align: center
|
||||
height: 15px
|
||||
width: 20px
|
||||
margin: 0
|
||||
padding: 5px
|
||||
background-color: #424242
|
||||
width: 70px
|
||||
|
||||
table.lesionTable > thead > tr > th {
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
height: 25px;
|
||||
line-height: 15px;
|
||||
}
|
||||
#thTarget
|
||||
width: 1%
|
||||
|
||||
table.lesionTable > tbody > tr > td {
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
height: 25px;
|
||||
line-height: 15px;
|
||||
}
|
||||
#thLocation
|
||||
text-align: left
|
||||
width: 15%
|
||||
|
||||
table.lesionTable tbody tr:hover{
|
||||
background-color: #d1d1d1;
|
||||
}
|
||||
#thSpacer
|
||||
width: 61%
|
||||
|
||||
tbody
|
||||
display: block
|
||||
height: 150px // Seems like this has to be fixed for overflow to work properly!
|
||||
overflow: auto
|
||||
width: 100%
|
||||
|
||||
table.lesionTable tbody tr{
|
||||
background-color: #b3b3b3;
|
||||
color: #424242;
|
||||
line-height: 14px;
|
||||
}
|
||||
tr.lesionTableRow
|
||||
width: 100%
|
||||
|
||||
&:nth-child(even)
|
||||
background: #888
|
||||
&:nth-child(odd)
|
||||
background: #BBB
|
||||
&:hover
|
||||
cursor: pointer
|
||||
background-color: #009BD2
|
||||
color: white
|
||||
|
||||
.btnRemove{
|
||||
border: none;
|
||||
outline:none;
|
||||
background-color: transparent;
|
||||
}
|
||||
td
|
||||
border-bottom-width: 0
|
||||
margin: 0
|
||||
padding: 5px
|
||||
line-height: 15px
|
||||
|
||||
&.lesionNumber, &.target, &.lesionTableTimepointCell
|
||||
text-align: center
|
||||
@@ -1,11 +1,11 @@
|
||||
<template name="lesionTableRow">
|
||||
<tr>
|
||||
<th class='lesionNumber'>{{lesionNumber}}</th>
|
||||
<th class='target'>{{isTarget}}</th>
|
||||
<th class='location'>{{location}}</th>
|
||||
<tr id="{{lesionNumber}}" class="lesionTableRow">
|
||||
<td class='lesionNumber'>{{number}}</td>
|
||||
<td class='location'>{{location}}</td>
|
||||
<td class='target'>{{#if isTarget}} Y {{else}} N {{/if}}</td>
|
||||
<!--Each time point as a column-->
|
||||
{{ #each timepoints}}
|
||||
{{ >lesionTableTimepointCell}}
|
||||
{{ /each }}
|
||||
{{# each timepoints }}
|
||||
{{> lesionTableTimepointCell}}
|
||||
{{/ each }}
|
||||
</tr>
|
||||
</template>
|
||||
@@ -1,10 +1,5 @@
|
||||
Template.lesionTableRow.helpers({
|
||||
'timepoints': function() {
|
||||
var array = [];
|
||||
var timepoints = this.timepoints;
|
||||
Object.keys(timepoints).forEach(function(key) {
|
||||
array.push(timepoints[key]);
|
||||
});
|
||||
return array;
|
||||
},
|
||||
return Timepoints.find();
|
||||
}
|
||||
});
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
<template name="lesionTableTimepointCell">
|
||||
<td>{{longestDiameter}}</td>
|
||||
<td class="lesionTableTimepointCell">{{longestDiameter}}</td>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
Template.lesionTableTimepointCell.helpers({
|
||||
'longestDiameter': function() {
|
||||
// Search Measurements by lesion and timepoint
|
||||
var lesionData = Template.parentData(1);
|
||||
return lesionData.timepoints[this.timepointID].longestDiameter;
|
||||
}
|
||||
});
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
<template name="lesionTableTimepointHeader">
|
||||
<th>{{name}}</th>
|
||||
<th>{{timepointName}}</th>
|
||||
</template>
|
||||
Whitespace-only changes.
@@ -0,0 +1,32 @@
|
||||
#nonTargetLesionLocationDialog {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
background: white;
|
||||
padding: 10px 20px 10px 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#selectNonTargetLesionLocation, #selectNonTargetLesionLocationResponse {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialogContent, .lesionLocation, .locationResponse {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.elementContainer {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.locationOK {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn{
|
||||
background-color: #bbb;
|
||||
border: 1px solid #888;
|
||||
color: #000000;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<template name="nonTargetLesionDialog">
|
||||
<div id="nonTargetLesionLocationDialog">
|
||||
<div class="dialogHeader">
|
||||
<div class="dialogHeader">
|
||||
<div><h4>Select Lesion Location</h4></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialogContent">
|
||||
<div class="lesionLocation">
|
||||
<div class="elementContainer">
|
||||
<label>Lesion Location</label>
|
||||
</div>
|
||||
<div class="elementContainer">
|
||||
<select id="selectNonTargetLesionLocation">
|
||||
<option value="-1"></option>
|
||||
{{ #each lesionLocations}}
|
||||
<option value='{{_id}}'>{{location}}</option>
|
||||
{{ /each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="locationResponse">
|
||||
<div class="elementContainer">
|
||||
<label>Response</label>
|
||||
</div>
|
||||
<div class="elementContainer">
|
||||
<select id="selectNonTargetLesionLocationResponse">
|
||||
<option value="-1"></option>
|
||||
{{ #each locationResponses}}
|
||||
<option value='{{code}}'>{{code}} - {{text}}</option>
|
||||
{{ /each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="locationOK">
|
||||
<button class="btn" id="nonTargetLesionOK">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,231 @@
|
||||
// This event sets lesion number for new lesion
|
||||
|
||||
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
|
||||
|
||||
measurementData.timepointID = $(eventData.element).data('timepointID');
|
||||
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
|
||||
// exists at a different timepoint
|
||||
var lesionNumber = measurementManagerDAL.getNewLesionNumber(measurementData.timepointID, isTarget=false);
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
|
||||
// Set lesion number
|
||||
doneCallback(lesionNumber);
|
||||
}
|
||||
|
||||
// This event determines whether or not to show the Non-Target lesion dialog
|
||||
// If there already exists a lesion with this specific lesion number,
|
||||
// related to the chosen location.
|
||||
|
||||
function getNonTargetLesionLocationCallback(measurementData, eventData, doneCallback) {
|
||||
|
||||
// Get the non-target lesion location dialog
|
||||
var nonTargetlesionDialog = $("#nonTargetLesionLocationDialog");
|
||||
|
||||
// Find the select option box
|
||||
var selectorLocation = nonTargetlesionDialog.find("select#selectNonTargetLesionLocation");
|
||||
var selectorResponse = nonTargetlesionDialog.find("select#selectNonTargetLesionLocationResponse");
|
||||
|
||||
// OK button
|
||||
var btnOK = nonTargetlesionDialog.find("button#nonTargetLesionOK");
|
||||
|
||||
|
||||
// If selector location is disabled, make it enable
|
||||
enableLocationSelection();
|
||||
|
||||
// Find out if this lesion number is already added in the lesion manager for another timepoint
|
||||
// If it is, disable selector location
|
||||
var locationUID = measurementManagerDAL.lesionNumberExists(measurementData);
|
||||
if (locationUID) {
|
||||
measurementData.locationUID = locationUID;
|
||||
// Disable the selection of a new location
|
||||
disableLocationSelection(measurementData.locationUID);
|
||||
}
|
||||
|
||||
// Disable selector location to prevent selecting a new location
|
||||
function disableLocationSelection(locationUID) {
|
||||
var locationName = measurementManagerDAL.getLocationName(locationUID);
|
||||
selectorLocation.find('option').each(function()
|
||||
{
|
||||
if ($(this).text() === locationName) {
|
||||
// Select location in locations dropdown list
|
||||
selectorLocation.find('option').eq($(this).index()).prop("selected", true);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
selectorLocation.prop("disabled", true);
|
||||
}
|
||||
|
||||
// Enable selector location
|
||||
function enableLocationSelection() {
|
||||
selectorLocation.removeAttr("disabled");
|
||||
}
|
||||
|
||||
function closeHandler() {
|
||||
|
||||
// Hide the lesion dialog
|
||||
nonTargetlesionDialog.css('display', 'none');
|
||||
// Enable selector location
|
||||
enableLocationSelection();
|
||||
|
||||
// Get the current value of the select option box
|
||||
selectorLocation.find("option:first").prop("selected", "selected");
|
||||
selectorResponse.find("option:first").prop("selected", "selected");
|
||||
|
||||
}
|
||||
|
||||
// Attach keypress handlers so the user can close with the Enter button
|
||||
nonTargetlesionDialog.off("keypress");
|
||||
nonTargetlesionDialog.on('keypress', keyPressHandler);
|
||||
|
||||
// This is the keypress callback function
|
||||
function keyPressHandler(e) {
|
||||
// If Enter is pressed, close the dialog
|
||||
if (e.which === 13) {
|
||||
closeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
// Show the lesion location dialog above
|
||||
nonTargetlesionDialog.css({
|
||||
top: eventData.currentPoints.page.y,
|
||||
left: eventData.currentPoints.page.x,
|
||||
display: 'block'
|
||||
});
|
||||
|
||||
// Click OK button
|
||||
// Add lesion to lesion table
|
||||
btnOK.off('click');
|
||||
btnOK.on('click', function(e) {
|
||||
// Get the current value of the selector
|
||||
var selectedOptionId = selectorLocation.find("option:selected").val();
|
||||
var responseOptionCode = nonTargetlesionDialog.find("select#selectNonTargetLesionLocationResponse option:selected").val();
|
||||
|
||||
// If the selected option is still the default (-1)
|
||||
// then stop here
|
||||
if (selectedOptionId < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the selected response option is still the default (-1)
|
||||
// then stop here
|
||||
if (responseOptionCode < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selected location data
|
||||
var locationObj = LesionLocations.findOne({_id: selectedOptionId});
|
||||
|
||||
var id;
|
||||
var existingLocation = PatientLocations.findOne({location: locationObj.location});
|
||||
if (existingLocation) {
|
||||
id = existingLocation._id;
|
||||
} else {
|
||||
// Adds location data to PatientLocation and retrieve the location ID
|
||||
id = PatientLocations.insert({location: locationObj.location});
|
||||
}
|
||||
|
||||
// Link locationUID with active lesion measurementData
|
||||
measurementData.locationUID = id;
|
||||
|
||||
/// Set the isTarget value to true, since this is the target-lesion dialog callback
|
||||
measurementData.isTarget = false;
|
||||
|
||||
// measurementText is set from location response list
|
||||
measurementData.measurementText = responseOptionCode;
|
||||
|
||||
// Adds lesion data to timepoints array
|
||||
measurementManagerDAL.addLesionData(measurementData);
|
||||
|
||||
// Close the dialog
|
||||
closeHandler();
|
||||
});
|
||||
}
|
||||
|
||||
function changeNonTargetLesionLocationCallback(measurementData, eventData, doneCallback) {
|
||||
doneCallback(prompt('Change your lesion location:'));
|
||||
}
|
||||
|
||||
var config = {
|
||||
setLesionNumberCallback: setLesionNumberCallback,
|
||||
getNonTargetLesionLocationCallback: getNonTargetLesionLocationCallback,
|
||||
changeNonTargetLesionLocationCallback: changeNonTargetLesionLocationCallback
|
||||
};
|
||||
|
||||
cornerstoneTools.nonTarget.setConfiguration(config);
|
||||
|
||||
|
||||
LocationResponses = new Meteor.Collection(null);
|
||||
|
||||
LocationResponses.insert(
|
||||
{
|
||||
text: "Complete response",
|
||||
code: "CR",
|
||||
description: ""
|
||||
}
|
||||
);
|
||||
LocationResponses.insert(
|
||||
{
|
||||
text: "Progressive disease",
|
||||
code: "PD",
|
||||
description: ""
|
||||
}
|
||||
);
|
||||
LocationResponses.insert(
|
||||
{
|
||||
text: "Complete response",
|
||||
code: "CR",
|
||||
description: ""
|
||||
}
|
||||
);
|
||||
LocationResponses.insert(
|
||||
{
|
||||
text: "Stable disease",
|
||||
code: "SD",
|
||||
description: ""
|
||||
}
|
||||
);
|
||||
LocationResponses.insert(
|
||||
{
|
||||
text: "Non-measurable",
|
||||
code: "NM",
|
||||
description: ""
|
||||
}
|
||||
);
|
||||
LocationResponses.insert(
|
||||
{
|
||||
text: "Unknown",
|
||||
code: "UN",
|
||||
description: ""
|
||||
}
|
||||
);
|
||||
LocationResponses.insert(
|
||||
{
|
||||
text: "Not Evaluable",
|
||||
code: "NE",
|
||||
description: ""
|
||||
}
|
||||
);
|
||||
LocationResponses.insert(
|
||||
{
|
||||
text: "Non-CR/Non-PD",
|
||||
code: "NN",
|
||||
description: ""
|
||||
}
|
||||
);
|
||||
LocationResponses.insert(
|
||||
{
|
||||
text: "Excluded",
|
||||
code: "EX",
|
||||
description: ""
|
||||
}
|
||||
);
|
||||
|
||||
Template.nonTargetLesionDialog.helpers({
|
||||
'lesionLocations': function() {
|
||||
return LesionLocations.find();
|
||||
},
|
||||
'locationResponses': function() {
|
||||
return LocationResponses.find();
|
||||
}
|
||||
});
|
||||
Executable
+250
@@ -0,0 +1,250 @@
|
||||
// uuid.js
|
||||
//
|
||||
// Copyright (c) 2010-2012 Robert Kieffer
|
||||
// MIT License - http://opensource.org/licenses/mit-license.php
|
||||
|
||||
(function() {
|
||||
var _global = this;
|
||||
|
||||
// Unique ID creation requires a high quality random # generator. We feature
|
||||
// detect to determine the best RNG source, normalizing to a function that
|
||||
// returns 128-bits of randomness, since that's what's usually required
|
||||
var _rng;
|
||||
|
||||
// Allow for MSIE11 msCrypto
|
||||
var _crypto = _global.crypto || _global.msCrypto;
|
||||
|
||||
// Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html
|
||||
//
|
||||
// Moderately fast, high quality
|
||||
if (typeof(_global.require) == 'function') {
|
||||
try {
|
||||
var _rb = _global.require('crypto').randomBytes;
|
||||
_rng = _rb && function() {return _rb(16);};
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if (!_rng && _crypto && _crypto.getRandomValues) {
|
||||
// WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
|
||||
//
|
||||
// Moderately fast, high quality
|
||||
var _rnds8 = new Uint8Array(16);
|
||||
_rng = function whatwgRNG() {
|
||||
_crypto.getRandomValues(_rnds8);
|
||||
return _rnds8;
|
||||
};
|
||||
}
|
||||
|
||||
if (!_rng) {
|
||||
// Math.random()-based (RNG)
|
||||
//
|
||||
// If all else fails, use Math.random(). It's fast, but is of unspecified
|
||||
// quality.
|
||||
var _rnds = new Array(16);
|
||||
_rng = function() {
|
||||
for (var i = 0, r; i < 16; i++) {
|
||||
if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
|
||||
_rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
|
||||
}
|
||||
|
||||
return _rnds;
|
||||
};
|
||||
}
|
||||
|
||||
// Buffer class to use
|
||||
var BufferClass = typeof(_global.Buffer) == 'function' ? _global.Buffer : Array;
|
||||
|
||||
// Maps for number <-> hex string conversion
|
||||
var _byteToHex = [];
|
||||
var _hexToByte = {};
|
||||
for (var i = 0; i < 256; i++) {
|
||||
_byteToHex[i] = (i + 0x100).toString(16).substr(1);
|
||||
_hexToByte[_byteToHex[i]] = i;
|
||||
}
|
||||
|
||||
// **`parse()` - Parse a UUID into it's component bytes**
|
||||
function parse(s, buf, offset) {
|
||||
var i = (buf && offset) || 0, ii = 0;
|
||||
|
||||
buf = buf || [];
|
||||
s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
|
||||
if (ii < 16) { // Don't overflow!
|
||||
buf[i + ii++] = _hexToByte[oct];
|
||||
}
|
||||
});
|
||||
|
||||
// Zero out remaining bytes if string was short
|
||||
while (ii < 16) {
|
||||
buf[i + ii++] = 0;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
// **`unparse()` - Convert UUID byte array (ala parse()) into a string**
|
||||
function unparse(buf, offset) {
|
||||
var i = offset || 0, bth = _byteToHex;
|
||||
return bth[buf[i++]] + bth[buf[i++]] +
|
||||
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
||||
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
||||
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
||||
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
||||
bth[buf[i++]] + bth[buf[i++]] +
|
||||
bth[buf[i++]] + bth[buf[i++]] +
|
||||
bth[buf[i++]] + bth[buf[i++]];
|
||||
}
|
||||
|
||||
// **`v1()` - Generate time-based UUID**
|
||||
//
|
||||
// Inspired by https://github.com/LiosK/UUID.js
|
||||
// and http://docs.python.org/library/uuid.html
|
||||
|
||||
// random #'s we need to init node and clockseq
|
||||
var _seedBytes = _rng();
|
||||
|
||||
// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
|
||||
var _nodeId = [
|
||||
_seedBytes[0] | 0x01,
|
||||
_seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
|
||||
];
|
||||
|
||||
// Per 4.2.2, randomize (14 bit) clockseq
|
||||
var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
|
||||
|
||||
// Previous uuid creation time
|
||||
var _lastMSecs = 0, _lastNSecs = 0;
|
||||
|
||||
// See https://github.com/broofa/node-uuid for API details
|
||||
function v1(options, buf, offset) {
|
||||
var i = buf && offset || 0;
|
||||
var b = buf || [];
|
||||
|
||||
options = options || {};
|
||||
|
||||
var clockseq = options.clockseq != null ? options.clockseq : _clockseq;
|
||||
|
||||
// UUID timestamps are 100 nano-second units since the Gregorian epoch,
|
||||
// (1582-10-15 00:00). JSNumbers aren't precise enough for this, so
|
||||
// time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
|
||||
// (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
|
||||
var msecs = options.msecs != null ? options.msecs : new Date().getTime();
|
||||
|
||||
// Per 4.2.1.2, use count of uuid's generated during the current clock
|
||||
// cycle to simulate higher resolution clock
|
||||
var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1;
|
||||
|
||||
// Time since last uuid creation (in msecs)
|
||||
var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000;
|
||||
|
||||
// Per 4.2.1.2, Bump clockseq on clock regression
|
||||
if (dt < 0 && options.clockseq == null) {
|
||||
clockseq = clockseq + 1 & 0x3fff;
|
||||
}
|
||||
|
||||
// Reset nsecs if clock regresses (new clockseq) or we've moved onto a new
|
||||
// time interval
|
||||
if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) {
|
||||
nsecs = 0;
|
||||
}
|
||||
|
||||
// Per 4.2.1.2 Throw error if too many uuids are requested
|
||||
if (nsecs >= 10000) {
|
||||
throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec');
|
||||
}
|
||||
|
||||
_lastMSecs = msecs;
|
||||
_lastNSecs = nsecs;
|
||||
_clockseq = clockseq;
|
||||
|
||||
// Per 4.1.4 - Convert from unix epoch to Gregorian epoch
|
||||
msecs += 12219292800000;
|
||||
|
||||
// `time_low`
|
||||
var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000;
|
||||
b[i++] = tl >>> 24 & 0xff;
|
||||
b[i++] = tl >>> 16 & 0xff;
|
||||
b[i++] = tl >>> 8 & 0xff;
|
||||
b[i++] = tl & 0xff;
|
||||
|
||||
// `time_mid`
|
||||
var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff;
|
||||
b[i++] = tmh >>> 8 & 0xff;
|
||||
b[i++] = tmh & 0xff;
|
||||
|
||||
// `time_high_and_version`
|
||||
b[i++] = tmh >>> 24 & 0xf | 0x10; // include version
|
||||
b[i++] = tmh >>> 16 & 0xff;
|
||||
|
||||
// `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
|
||||
b[i++] = clockseq >>> 8 | 0x80;
|
||||
|
||||
// `clock_seq_low`
|
||||
b[i++] = clockseq & 0xff;
|
||||
|
||||
// `node`
|
||||
var node = options.node || _nodeId;
|
||||
for (var n = 0; n < 6; n++) {
|
||||
b[i + n] = node[n];
|
||||
}
|
||||
|
||||
return buf ? buf : unparse(b);
|
||||
}
|
||||
|
||||
// **`v4()` - Generate random UUID**
|
||||
|
||||
// See https://github.com/broofa/node-uuid for API details
|
||||
function v4(options, buf, offset) {
|
||||
// Deprecated - 'format' argument, as supported in v1.2
|
||||
var i = buf && offset || 0;
|
||||
|
||||
if (typeof(options) == 'string') {
|
||||
buf = options == 'binary' ? new BufferClass(16) : null;
|
||||
options = null;
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
var rnds = options.random || (options.rng || _rng)();
|
||||
|
||||
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
|
||||
rnds[6] = (rnds[6] & 0x0f) | 0x40;
|
||||
rnds[8] = (rnds[8] & 0x3f) | 0x80;
|
||||
|
||||
// Copy bytes to buffer, if provided
|
||||
if (buf) {
|
||||
for (var ii = 0; ii < 16; ii++) {
|
||||
buf[i + ii] = rnds[ii];
|
||||
}
|
||||
}
|
||||
|
||||
return buf || unparse(rnds);
|
||||
}
|
||||
|
||||
// Export public API
|
||||
var uuid = v4;
|
||||
uuid.v1 = v1;
|
||||
uuid.v4 = v4;
|
||||
uuid.parse = parse;
|
||||
uuid.unparse = unparse;
|
||||
uuid.BufferClass = BufferClass;
|
||||
|
||||
if (typeof(module) != 'undefined' && module.exports) {
|
||||
// Publish as node.js module
|
||||
module.exports = uuid;
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
// Publish as AMD module
|
||||
define(function() {return uuid;});
|
||||
|
||||
|
||||
} else {
|
||||
// Publish as global (in browsers)
|
||||
var _previousRoot = _global.uuid;
|
||||
|
||||
// **`noConflict()` - (browser only) to reset global 'uuid' var**
|
||||
uuid.noConflict = function() {
|
||||
_global.uuid = _previousRoot;
|
||||
return uuid;
|
||||
};
|
||||
|
||||
_global.uuid = uuid;
|
||||
}
|
||||
}).call(this);
|
||||
@@ -15,12 +15,12 @@ Package.onUse(function (api) {
|
||||
api.use('cornerstone');
|
||||
|
||||
api.addFiles('compatibility/lesionTool.js', 'client', {bare: true});
|
||||
|
||||
api.addFiles('components/lesionDialog/lesionDialog.html', 'client');
|
||||
api.addFiles('components/lesionDialog/lesionDialog.js', 'client');
|
||||
api.addFiles('compatibility/nonTargetTool.js', 'client', {bare: true});
|
||||
api.addFiles('compatibility/measurementManagerDAL.js', 'client', {bare: true});
|
||||
|
||||
api.addFiles('components/lesionLocationDialog/lesionLocationDialog.html', 'client');
|
||||
api.addFiles('components/lesionLocationDialog/lesionLocationDialog.js', 'client');
|
||||
api.addFiles('components/lesionLocationDialog/lesionLocationDialog.styl', 'client');
|
||||
|
||||
api.addFiles('components/lesionTable/lesionTable.html', 'client');
|
||||
api.addFiles('components/lesionTable/lesionTable.styl', 'client');
|
||||
@@ -30,6 +30,20 @@ Package.onUse(function (api) {
|
||||
api.addFiles('components/lesionTableRow/lesionTableRow.js', 'client');
|
||||
|
||||
api.addFiles('components/lesionTableTimepointCell/lesionTableTimepointCell.html', 'client');
|
||||
api.addFiles('components/lesionTableTimepointCell/lesionTableTimepointCell.js', 'client');
|
||||
|
||||
api.addFiles('components/lesionTableTimepointHeader/lesionTableTimepointHeader.html', 'client');
|
||||
api.addFiles('components/lesionTableTimepointHeader/lesionTableTimepointHeader.js', 'client');
|
||||
|
||||
api.addFiles('components/nonTargetLesionDialog/nonTargetLesionDialog.html', 'client');
|
||||
api.addFiles('components/nonTargetLesionDialog/nonTargetLesionDialog.css', 'client');
|
||||
api.addFiles('components/nonTargetLesionDialog/nonTargetLesionDialog.js', 'client');
|
||||
|
||||
|
||||
// Library functions
|
||||
api.addFiles('lib/uuid.js', 'client');
|
||||
|
||||
api.export('Measurements', 'client');
|
||||
api.export('Timepoints', 'client');
|
||||
|
||||
});
|
||||
@@ -1,5 +1,12 @@
|
||||
<template name="hidingPanel">
|
||||
<div class="hidingPanel">
|
||||
{{> studyBrowser}}
|
||||
<div class="toggleBtnContainer">
|
||||
<button id="btnCollapse">
|
||||
<i id="btnCollapseIcon" class="btnCollapseIcon fa fa-angle-left fa-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="studyBrowserContainer">
|
||||
{{> studyBrowser}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
Template.hidingPanel.events({
|
||||
'click button#btnCollapse': function(e,template) {
|
||||
|
||||
// Check hidingPanel is open or not
|
||||
var hidingPanelOpen = template.hidingPanelOpen.get();
|
||||
hidingPanelOpen = !hidingPanelOpen;
|
||||
template.hidingPanelOpen.set(hidingPanelOpen);
|
||||
|
||||
// Get hidingPanel width from hidingPanelWidth reactiveVar
|
||||
var contentId = this.contentId;
|
||||
var hidingPanelElement = $("#"+contentId).find(".hidingPanel");
|
||||
var hidingPanelWidth = template.hidingPanelWidth.get();
|
||||
if(hidingPanelWidth == 0) {
|
||||
hidingPanelWidth = $(hidingPanelElement).width();
|
||||
template.hidingPanelWidth.set(parseInt(hidingPanelWidth));
|
||||
}
|
||||
|
||||
$(hidingPanelElement).toggleClass("hidingPanelCollapse");
|
||||
|
||||
var studyBrowserContainerElement = $("#"+contentId).find(".studyBrowserContainer");
|
||||
$(studyBrowserContainerElement).toggleClass("studyBrowserContainerCollapse");
|
||||
|
||||
var btnCollapseIconElement = $("#"+contentId).find("#btnCollapseIcon");
|
||||
$(btnCollapseIconElement).toggleClass("btnCollapseIcon-collapse");
|
||||
|
||||
// Set viewportAndLesionTable width according to hiding panel status
|
||||
if (hidingPanelOpen) {
|
||||
var viewportAndLesionTableElement = $("#"+contentId).find("#viewportAndLesionTable");
|
||||
var viewportAndLesionTableElementWidth = $(viewportAndLesionTableElement).width();
|
||||
$(viewportAndLesionTableElement).width(viewportAndLesionTableElementWidth - hidingPanelWidth +"px");
|
||||
|
||||
} else {
|
||||
var viewportAndLesionTableElement = $("#"+contentId).find("#viewportAndLesionTable");
|
||||
var viewportAndLesionTableElementWidth = $(viewportAndLesionTableElement).width();
|
||||
$(viewportAndLesionTableElement).width(viewportAndLesionTableElementWidth + hidingPanelWidth +"px");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
Template.hidingPanel.onCreated(function() {
|
||||
this.hidingPanelOpen = new ReactiveVar(true);
|
||||
this.hidingPanelWidth = new ReactiveVar(0);
|
||||
});
|
||||
@@ -1,6 +1,70 @@
|
||||
.hidingPanel
|
||||
width: 120px
|
||||
height: 100%
|
||||
padding-top: 35px
|
||||
position: relative
|
||||
float: left
|
||||
|
||||
.hidingPanel,.hidingPanelCollapse, .toggleBtnContainer, .studyBrowserContainer, .studyBrowserContainerCollapse, #btnCollapse,.btnCollapseIcon, .btnCollapseIcon-collapse, .viewportStyle {
|
||||
|
||||
-o-transition: all 0.3s ease-out;
|
||||
-ms-transition: all 0.3s ease-out;
|
||||
-moz-transition: all 0.3s ease-out;
|
||||
-webkit-transition: all 0.3s ease-out;
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.hidingPanel {
|
||||
width: 120px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.hidingPanelCollapse{
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.toggleBtnContainer{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.studyBrowserContainer {
|
||||
position: absolute;
|
||||
display: block;
|
||||
margin-top: 30px;
|
||||
height: calc(100% - 30px);
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.studyBrowserContainerCollapse {
|
||||
opacity: 0;
|
||||
width: 0px;
|
||||
|
||||
}
|
||||
.studyBrowserContainerCollapseDisplay {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#btnCollapse{
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
|
||||
.btnCollapseIcon-collapse{
|
||||
-moz-transform: rotate(180deg);
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<template name="loadingText">
|
||||
<div class="loadingTextDiv">
|
||||
<h5>Loading... <i class="fa fa-spinner fa-pulse"></i></h5>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
.loadingTextDiv
|
||||
margin: 30px
|
||||
|
||||
h5
|
||||
text-align: center
|
||||
@@ -0,0 +1,7 @@
|
||||
<template name="removableBackdrop">
|
||||
<div class="removableBackdrop noselect"
|
||||
oncontextmenu='return false;'
|
||||
unselectable='on'
|
||||
onselectstart='return false;'>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
.removableBackdrop
|
||||
position: fixed
|
||||
top: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
left: 0
|
||||
z-index: 50
|
||||
background-color: transparent
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<template name="imageThumbnail">
|
||||
<div class="imageThumbnail">
|
||||
<div class="imageThumbnailLoadingIndicator thumbnailLoadingIndicator">
|
||||
<p>Loading {{percentComplete}}</p>
|
||||
</div>
|
||||
<div class="imageThumbnailErrorLoadingIndicator thumbnailLoadingIndicator">
|
||||
<p>Error</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,11 +1,33 @@
|
||||
Template.imageThumbnail.onRendered(function() {
|
||||
var instance = this.data.instances[0];
|
||||
var element = this.find('.imageThumbnail');
|
||||
cornerstone.enable(element);
|
||||
var instance = this.data.stack.instances[0];
|
||||
var element = this.find('.imageThumbnail');
|
||||
cornerstone.enable(element);
|
||||
|
||||
var imageId = getImageId(instance);
|
||||
var imageId = getImageId(instance);
|
||||
|
||||
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||
cornerstone.displayImage(element, image);
|
||||
});
|
||||
var elem = $(element);
|
||||
elem.find('.imageThumbnailLoadingIndicator').css('display', 'block');
|
||||
|
||||
var thumbnailIndex = $('.imageThumbnail').index(element);
|
||||
ThumbnailLoading[thumbnailIndex] = imageId;
|
||||
|
||||
this.data.thumbnailIndex = thumbnailIndex;
|
||||
|
||||
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||
cornerstone.displayImage(element, image);
|
||||
|
||||
delete ThumbnailLoading[thumbnailIndex];
|
||||
elem.find('.imageThumbnailLoadingIndicator').css('display', 'none');
|
||||
}, function(error) {
|
||||
elem.find('.imageThumbnailErrorLoadingIndicator').css('display', 'block');
|
||||
});
|
||||
});
|
||||
|
||||
Template.imageThumbnail.helpers({
|
||||
'percentComplete': function() {
|
||||
var percentComplete = Session.get('CornerstoneThumbnailLoadProgress' + this.thumbnailIndex);
|
||||
if (percentComplete && percentComplete !== 100) {
|
||||
return percentComplete + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2,4 +2,22 @@
|
||||
width: 90px
|
||||
height: 90px
|
||||
background-color: black
|
||||
margin: 0 auto
|
||||
margin: 0 auto
|
||||
position: relative
|
||||
|
||||
.thumbnailLoadingIndicator
|
||||
display: none
|
||||
pointer-events: none
|
||||
color: #f8a13f
|
||||
height: 20px
|
||||
width: 100%
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
margin: auto
|
||||
position: absolute
|
||||
|
||||
p
|
||||
text-align: center
|
||||
font-size: 10pt
|
||||
@@ -1,5 +1,14 @@
|
||||
Template.studyBrowser.helpers({
|
||||
studies : function() {
|
||||
return this.studies;
|
||||
var studies = Session.get('studies');
|
||||
|
||||
var array = [];
|
||||
studies.forEach(function(study, index) {
|
||||
array.push({
|
||||
studyIndex: index,
|
||||
study: study
|
||||
});
|
||||
});
|
||||
return array;
|
||||
}
|
||||
});
|
||||
@@ -6,4 +6,4 @@
|
||||
overflow-y: auto
|
||||
overflow-x: hidden
|
||||
background-color: black
|
||||
padding-bottom: 20px
|
||||
padding-bottom: 20px
|
||||
@@ -26,13 +26,8 @@ function thumbnailDragStartHandler(e, data) {
|
||||
var imageThumbnail = $(targetThumbnail);
|
||||
|
||||
// Store this data for use during drag and drop
|
||||
OHIF.viewer.dragAndDropData = {
|
||||
studyInstanceUid: imageThumbnail.data('studyInstanceUid'),
|
||||
seriesInstanceUid: imageThumbnail.data('seriesInstanceUid')
|
||||
};
|
||||
OHIF.viewer.dragAndDropData = data;
|
||||
|
||||
$.extend(OHIF.viewer.dragAndDropData, data);
|
||||
|
||||
// Clone the image thumbnail
|
||||
var targetId = targetThumbnail.id + 'DragClone';
|
||||
var clone = cloneElement(targetThumbnail, targetId);
|
||||
@@ -204,6 +199,8 @@ Template.thumbnailEntry.events({
|
||||
// Touch drag/drop events
|
||||
'touchstart .thumbnailEntry, mousedown .thumbnailEntry': function(e) {
|
||||
var data = Template.parentData(2);
|
||||
data.studyInstanceUid = this.stack.studyInstanceUid;
|
||||
data.seriesInstanceUid = this.stack.seriesInstanceUid;
|
||||
thumbnailDragStartHandler(e, data);
|
||||
},
|
||||
'touchmove .thumbnailEntry': function(e) {
|
||||
@@ -216,9 +213,6 @@ Template.thumbnailEntry.events({
|
||||
|
||||
Template.thumbnailEntry.helpers({
|
||||
seriesDescription : function() {
|
||||
if (!this.seriesDescription) {
|
||||
return '';
|
||||
}
|
||||
return this.seriesDescription;
|
||||
return this.stack.seriesDescription || '';
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,15 @@
|
||||
Template.thumbnails.helpers({
|
||||
thumbnails: function() {
|
||||
var stacks = createStacks(this);
|
||||
return stacks;
|
||||
}
|
||||
thumbnails: function() {
|
||||
var stacks = createStacks(this.study);
|
||||
var studyIndex = this.studyIndex;
|
||||
|
||||
var array = [];
|
||||
stacks.forEach(function(stack, index) {
|
||||
array.push({
|
||||
thumbnailIndex: index * (studyIndex + 1),
|
||||
stack: stack
|
||||
});
|
||||
});
|
||||
return array;
|
||||
}
|
||||
});
|
||||
@@ -1,13 +1,11 @@
|
||||
/**
|
||||
* 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
|
||||
* @param element {node} DOM Node representing the viewport element
|
||||
*/
|
||||
function enablePrefetchOnElement(viewportIndex) {
|
||||
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||
//console.log('Enabling prefetch on new element');
|
||||
function enablePrefetchOnElement(element) {
|
||||
log.info("imageViewerViewport enablePrefetchOnElement");
|
||||
|
||||
// Loop through all viewports and disable stackPrefetch
|
||||
$('.imageViewerViewport').each(function() {
|
||||
@@ -24,10 +22,21 @@ function enablePrefetchOnElement(viewportIndex) {
|
||||
}
|
||||
}
|
||||
|
||||
function displayReferenceLines(viewportIndex) {
|
||||
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||
/**
|
||||
* This function disables reference lines for a specific viewport element.
|
||||
* It also enables reference lines for all other viewports with the
|
||||
* class .imageViewerViewport.
|
||||
*
|
||||
* @param element {node} DOM Node representing the viewport element
|
||||
*/
|
||||
function displayReferenceLines(element) {
|
||||
log.info("imageViewerViewport displayReferenceLines");
|
||||
|
||||
$('.imageViewerViewport').each(function(index, element) {
|
||||
// Disable reference lines for the current element
|
||||
cornerstoneTools.referenceLines.tool.disable(element);
|
||||
|
||||
// Loop through all other viewport elements and enable reference lines
|
||||
$('.imageViewerViewport').not(element).each(function(index, element) {
|
||||
var imageId;
|
||||
try {
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
@@ -40,193 +49,363 @@ function displayReferenceLines(viewportIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index === viewportIndex) {
|
||||
cornerstoneTools.referenceLines.tool.disable(element);
|
||||
return;
|
||||
}
|
||||
cornerstoneTools.referenceLines.tool.enable(element, OHIF.viewer.updateImageSynchronizer);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var allCornerstoneEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
|
||||
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
|
||||
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
|
||||
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
|
||||
'CornerstoneToolsMultiTouchDragStart';
|
||||
|
||||
/**
|
||||
* This function loads a study series into a viewport element.
|
||||
*
|
||||
* @param data {object} Object containing the study, series, and viewport element to be used
|
||||
*/
|
||||
function loadSeriesIntoViewport(data) {
|
||||
if (!data.series || !data.element) {
|
||||
log.info("imageViewerViewport loadSeriesIntoViewport");
|
||||
|
||||
// Make sure we have all the data required to render the series
|
||||
if (!data.study || !data.series || !data.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
var contentId = $("#viewer").parents('.tab-pane.active').attr('id');
|
||||
|
||||
var study = data.study;
|
||||
var series = data.series;
|
||||
// Get the current element and it's index in the list of all viewports
|
||||
// The viewport index is often used to store information about a viewport element
|
||||
var element = data.element;
|
||||
var viewportIndex = $(".imageViewerViewport").index(element);
|
||||
|
||||
var allEvents = 'CornerstoneToolsMouseDown CornerstoneToolsMouseDownActivate ' +
|
||||
'CornerstoneToolsMouseClick CornerstoneToolsMouseDrag CornerstoneToolsMouseUp ' +
|
||||
'CornerstoneToolsMouseWheel CornerstoneToolsTap CornerstoneToolsTouchPress ' +
|
||||
'CornerstoneToolsTouchStart CornerstoneToolsTouchStartActive ' +
|
||||
'CornerstoneToolsDragStartActive CornerstoneToolsMultiTouchDragStart';
|
||||
// Get the contentID of the current worklist tab, if the viewport is running
|
||||
// alongside the worklist package
|
||||
var contentId = Session.get('activeContentId');
|
||||
|
||||
// If the viewer is inside a tab, create an object related to the specified viewport
|
||||
// This data will be saved so that the tab can be reloaded to the same state after tabs
|
||||
// are switched
|
||||
if (contentId) {
|
||||
ViewerData[contentId].loadedSeriesData[viewportIndex] = {};
|
||||
}
|
||||
|
||||
// Create an empty array to populate with image IDs
|
||||
var imageIds = [];
|
||||
|
||||
// Loop through the current series and add metadata to the
|
||||
// Cornerstone meta data provider. This will be used to fill information
|
||||
// into the viewport overlays, and to calculate reference lines and orientation markers
|
||||
var series = data.series;
|
||||
var numImages = series.instances.length;
|
||||
series.instances.forEach(function(instance, imageIndex) {
|
||||
var imageId = getImageId(instance);
|
||||
imageIds.push(imageId);
|
||||
var data = {
|
||||
var metaData = {
|
||||
instance: instance,
|
||||
series: series,
|
||||
study: study,
|
||||
study: data.study,
|
||||
numImages: numImages,
|
||||
imageIndex: imageIndex + 1
|
||||
};
|
||||
addMetaData(imageId, data);
|
||||
|
||||
addMetaData(imageId, metaData);
|
||||
});
|
||||
|
||||
// Define the current image stack using the newly created image IDs
|
||||
var stack = {
|
||||
currentImageIdIndex: data.currentImageIdIndex || 0,
|
||||
imageIds: imageIds
|
||||
};
|
||||
|
||||
// Get the current image ID for the stack that will be rendered
|
||||
var imageId = imageIds[stack.currentImageIdIndex];
|
||||
|
||||
// Save the current image ID inside the template data so it can be
|
||||
// retrieved from the template helpers
|
||||
var templateData = Template.currentData();
|
||||
templateData.imageId = imageId;
|
||||
|
||||
// Save the current image ID inside the ViewportLoading object.
|
||||
//
|
||||
// The ViewportLoading object relates the viewport elements with whichever
|
||||
// image is currently being loaded into them. This is useful so that we can
|
||||
// place progress (download %) for each image inside the proper viewports.
|
||||
ViewportLoading[viewportIndex] = imageId;
|
||||
|
||||
// Enable Cornerstone for the viewport element
|
||||
//
|
||||
// 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.enable(element, cornerstone.webGL.renderer.render);
|
||||
|
||||
var startLoadingHandler = cornerstoneTools.loadHandlerManager.getStartLoadHandler();
|
||||
// Get the handler functions that will run when loading has finished or thrown
|
||||
// an error. These are used to show/hide loading / error text boxes on each viewport.
|
||||
var endLoadingHandler = cornerstoneTools.loadHandlerManager.getEndLoadHandler();
|
||||
var errorLoadingHandler = cornerstoneTools.loadHandlerManager.getErrorLoadingHandler();
|
||||
|
||||
if (startLoadingHandler) {
|
||||
startLoadingHandler(element);
|
||||
}
|
||||
|
||||
// Start loading the image.
|
||||
cornerstone.loadAndCacheImage(imageId).then(function(image) {
|
||||
// Fire an event to give notice that a new series was loaded in the viewport
|
||||
var eventType = "ViewerBaseSeriesLoaded";
|
||||
var eventData = {
|
||||
element: element,
|
||||
stack: stack,
|
||||
study: data.study,
|
||||
series: data.series
|
||||
};
|
||||
$(document).trigger(eventType, eventData);
|
||||
|
||||
// If there is a saved object containing Cornerstone viewport data
|
||||
// (e.g. scale, invert, window settings) in the input data, apply it now.
|
||||
//
|
||||
// Otherwise, display the loaded image in the viewport element with the
|
||||
// default viewport settings.
|
||||
if (data.viewport) {
|
||||
cornerstone.displayImage(element, image, data.viewport);
|
||||
} else {
|
||||
cornerstone.displayImage(element, image);
|
||||
}
|
||||
|
||||
if (endLoadingHandler) {
|
||||
endLoadingHandler(element);
|
||||
}
|
||||
|
||||
// Remove the data for this viewport from the ViewportLoading object
|
||||
// This will stop the loading percentage complete from being displayed.
|
||||
delete ViewportLoading[viewportIndex];
|
||||
|
||||
// Call the handler function that represents the end of the image loading phase
|
||||
// (e.g. hide the progress text box)
|
||||
endLoadingHandler(element);
|
||||
|
||||
// Resize the canvas to fit the current viewport element size. Fit the displayed
|
||||
// image to the canvas dimensions.
|
||||
cornerstone.resize(element, true);
|
||||
|
||||
// Remove the 'empty' class from the viewport to hide any instruction text
|
||||
element.classList.remove('empty');
|
||||
|
||||
// Hide the viewport instructions (i.e. 'Drag a stack here') and show
|
||||
// the viewport overlay data.
|
||||
$(element).siblings('.viewportInstructions').hide();
|
||||
$(element).siblings('.imageViewerViewportOverlay').show();
|
||||
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
|
||||
|
||||
// Add stack state managers for the stack tool, CINE tool, and reference lines
|
||||
cornerstoneTools.addStackStateManager(element, [ 'stack', 'playClip', 'referenceLines' ]);
|
||||
|
||||
// Enable orientation markers, if applicable
|
||||
// Get the current viewport settings
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
updateOrientationMarkers(element, viewport);
|
||||
|
||||
// Enable orientation markers, if applicable
|
||||
updateOrientationMarkers(element);
|
||||
|
||||
// Clear any old stack data
|
||||
cornerstoneTools.clearToolState(element, 'stack');
|
||||
cornerstoneTools.addToolState(element, 'stack', stack);
|
||||
|
||||
// Enable mouse input
|
||||
// Enable mouse, mouseWheel, and touch input on the element
|
||||
cornerstoneTools.mouseInput.enable(element);
|
||||
cornerstoneTools.touchInput.enable(element);
|
||||
cornerstoneTools.mouseWheelInput.enable(element);
|
||||
|
||||
// Use the tool manager to enable the currently active tool for this
|
||||
// newly rendered element
|
||||
var activeTool = toolManager.getActiveTool();
|
||||
toolManager.setActiveTool(activeTool);
|
||||
|
||||
cornerstoneTools.magnify.enable(element);
|
||||
|
||||
// Define a function to run whenever the Cornerstone viewport is rendered
|
||||
// (e.g. following a change of window or zoom)
|
||||
function onImageRendered(e, eventData) {
|
||||
log.info('imageViewerViewport onImageRendered');
|
||||
|
||||
// Use Session to trigger reactive updates in the viewportOverlay helper functions
|
||||
// This lets the viewport overlay always display correct window / zoom values
|
||||
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
|
||||
|
||||
// Save the current viewport into the ViewerData global variable, as well as the
|
||||
// Meteor Session. This lets the viewport be saved/reloaded on a hot-code reload
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
ViewerData[contentId].viewer.imageViewerLoadedSeriesDictionary[viewportIndex].viewport = viewport;
|
||||
ViewerData[contentId].loadedSeriesData[viewportIndex].viewport = viewport;
|
||||
Session.set('ViewerData', ViewerData);
|
||||
}
|
||||
|
||||
// Attach the onImageRendered callback to the CornerstoneImageRendered event
|
||||
$(element).off('CornerstoneImageRendered', onImageRendered);
|
||||
$(element).on('CornerstoneImageRendered', onImageRendered);
|
||||
|
||||
// Set a random value for the Session variable in order to trigger an overlay update
|
||||
Session.set('CornerstoneImageRendered' + viewportIndex, Random.id());
|
||||
|
||||
// Define a function to run whenever the Cornerstone viewport changes images
|
||||
// (e.g. during scrolling)
|
||||
function onNewImage(e, eventData) {
|
||||
log.info('imageViewerViewport onNewImage');
|
||||
|
||||
// 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());
|
||||
|
||||
// If this viewport is displaying a stack of images, save the current image
|
||||
// index in the stack to the global ViewerData object, as well as the Meteor Session.
|
||||
var stack = cornerstoneTools.getToolState(element, 'stack');
|
||||
if (stack && stack.data.length && stack.data[0].imageIds.length > 1) {
|
||||
var imageIdIndex = stack.data[0].imageIds.indexOf(templateData.imageId);
|
||||
ViewerData[contentId].viewer.imageViewerLoadedSeriesDictionary[viewportIndex].currentImageIdIndex = imageIdIndex;
|
||||
ViewerData[contentId].loadedSeriesData[viewportIndex].currentImageIdIndex = imageIdIndex;
|
||||
Session.set('ViewerData', ViewerData);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the onNewImage callback to the CornerstoneNewImage event
|
||||
$(element).off('CornerstoneNewImage', onNewImage);
|
||||
$(element).on('CornerstoneNewImage', onNewImage);
|
||||
|
||||
// Set a random value for the Session variable in order to trigger an overlay update
|
||||
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
||||
|
||||
// Define a function to trigger an event whenever a new viewport is being used
|
||||
// This is used to update the value of the "active viewport", when the user interacts
|
||||
// with a new viewport element
|
||||
function sendActivationTrigger(e, eventData) {
|
||||
var activeViewportIndex = data.activeViewport.curValue;
|
||||
log.info('imageViewerViewport sendActivationTrigger');
|
||||
|
||||
// Check if the current active viewport in the Meteor Session
|
||||
// Is the same as the viewport in which the activation event was fired.
|
||||
// If it was, no changes are necessary, so stop here.
|
||||
var activeViewportIndex = Session.get('activeViewport');
|
||||
var viewportIndex = $(".imageViewerViewport").index(eventData.element);
|
||||
if (viewportIndex === activeViewportIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, trigger an 'ActivateViewport' event to be handled by the Template event
|
||||
// handler
|
||||
eventData.viewportIndex = viewportIndex;
|
||||
var customEvent = jQuery.Event('ActivateViewport', eventData);
|
||||
customEvent.type = 'ActivateViewport'; // Need to overwrite the type set in the touch tools
|
||||
var customEvent = $.Event('ActivateViewport', eventData);
|
||||
|
||||
// Need to overwrite the type set in the original event
|
||||
customEvent.type = 'ActivateViewport';
|
||||
$(e.target).trigger(customEvent, eventData);
|
||||
}
|
||||
|
||||
$(element).off(allEvents, sendActivationTrigger);
|
||||
$(element).on(allEvents, sendActivationTrigger);
|
||||
// Attach the sendActivationTrigger function to all of the Cornerstone interaction events
|
||||
$(element).off(allCornerstoneEvents, sendActivationTrigger);
|
||||
$(element).on(allCornerstoneEvents, sendActivationTrigger);
|
||||
|
||||
Session.set('CornerstoneNewImage' + viewportIndex, Random.id());
|
||||
|
||||
OHIF.viewer.imageViewerLoadedSeriesDictionary[viewportIndex] = {
|
||||
// Store the current series data inside a global variable
|
||||
OHIF.viewer.loadedSeriesData[viewportIndex] = {
|
||||
studyInstanceUid: data.studyInstanceUid,
|
||||
seriesInstanceUid: data.seriesInstanceUid,
|
||||
currentImageIdIndex: data.currentImageIdIndex,
|
||||
viewport: viewport
|
||||
};
|
||||
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
|
||||
|
||||
// Check if image plane (orientation / loction) data is present for the current image
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', image.imageId);
|
||||
|
||||
// If it is, and reference lines are enabled, add this element to the global synchronizer
|
||||
// that is used for updating reference lines, and enable reference lines for this viewport.
|
||||
if (OHIF.viewer.refLinesEnabled && imagePlane && imagePlane.frameOfReferenceUID) {
|
||||
OHIF.viewer.updateImageSynchronizer.add(element);
|
||||
displayReferenceLines(viewportIndex);
|
||||
displayReferenceLines(element);
|
||||
}
|
||||
|
||||
}, function(error) {
|
||||
if (errorLoadingHandler) {
|
||||
errorLoadingHandler(element, imageId, error);
|
||||
if (viewportIndex === Session.get('activeViewport')) {
|
||||
enablePrefetchOnElement(element);
|
||||
}
|
||||
}, function(error) {
|
||||
// If something goes wrong while loading the image, fire the error handler.
|
||||
errorLoadingHandler(element, imageId, error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function searches an object to return the keys that contain a specific value
|
||||
*
|
||||
* @param object {object} The object to be searched
|
||||
* @param value The value to be found
|
||||
*
|
||||
* @returns {array} The keys for which the object has the specified value
|
||||
*/
|
||||
function getKeysByValue(object, value) {
|
||||
// http://stackoverflow.com/questions/9907419/javascript-object-get-key-by-value
|
||||
return Object.keys(object).filter(function(key) {
|
||||
return object[key] === value;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Meteor.startup(function() {
|
||||
// On Meteor startup, define the global objects used to store loading imageIds
|
||||
// by viewport / thumbnail element
|
||||
ViewportLoading = {};
|
||||
ThumbnailLoading = {};
|
||||
|
||||
// Whenever the CornerstoneImageLoadProgress is fired, identify which viewports
|
||||
// the "in-progress" image is to be displayed in. Then pass the percent complete
|
||||
// via the Meteor Session to the other templates to be displayed in the relevant viewports.
|
||||
$(cornerstone).on('CornerstoneImageLoadProgress', function(e, eventData) {
|
||||
viewportIndices = getKeysByValue(ViewportLoading, eventData.imageId);
|
||||
viewportIndices.forEach(function(viewportIndex) {
|
||||
Session.set('CornerstoneLoadProgress' + viewportIndex, eventData.percentComplete);
|
||||
});
|
||||
|
||||
thumbnailIndices = getKeysByValue(ThumbnailLoading, eventData.imageId);
|
||||
thumbnailIndices.forEach(function(thumbnailIndex) {
|
||||
Session.set('CornerstoneThumbnailLoadProgress' + thumbnailIndex, eventData.percentComplete);
|
||||
});
|
||||
});
|
||||
|
||||
var config = {
|
||||
magnifySize: 300,
|
||||
magnificationLevel: 3
|
||||
};
|
||||
|
||||
cornerstoneTools.magnify.setConfiguration(config);
|
||||
});
|
||||
|
||||
Template.imageViewerViewport.onRendered(function() {
|
||||
var studies = this.data.studies;
|
||||
log.info("imageViewerViewport onRendered");
|
||||
|
||||
// When the imageViewerViewport template is rendered
|
||||
var element = this.find(".imageViewerViewport");
|
||||
|
||||
|
||||
// Display the loading indicator for this element
|
||||
$(element).siblings('.imageViewerLoadingIndicator').css('display', 'block');
|
||||
|
||||
// Get the current active viewport index, if this viewport has the same index,
|
||||
// add the CSS 'active' class to highlight this viewport.
|
||||
var activeViewport = Session.get('activeViewport');
|
||||
if (activeViewport === this.data.viewportIndex) {
|
||||
$('#imageViewerViewports .viewportContainer').removeClass('active');
|
||||
$(element).parents('.viewportContainer').addClass('active');
|
||||
}
|
||||
|
||||
// Create a data object to pass to the series loading function (loadSeriesIntoViewport)
|
||||
var data = {
|
||||
element: element,
|
||||
viewport: this.data.viewport,
|
||||
currentImageIdIndex: this.data.currentImageIdIndex,
|
||||
activeViewport: this.data.activeViewport,
|
||||
studyInstanceUid: this.data.studyInstanceUid,
|
||||
seriesInstanceUid: this.data.seriesInstanceUid
|
||||
seriesInstanceUid: this.data.seriesInstanceUid,
|
||||
activeViewport: activeViewport
|
||||
};
|
||||
|
||||
// If no seriesInstanceUid or studyInstanceUid were supplied, display the drag/drop
|
||||
// instructions and then stop here since we don't know what to display in the viewport.
|
||||
if (this.data.seriesInstanceUid === undefined || this.data.studyInstanceUid === undefined) {
|
||||
element.classList.add('empty');
|
||||
$(element).siblings('.imageViewerLoadingIndicator').css('display', 'none');
|
||||
$(element).siblings('.viewportInstructions').show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the current set of studies from the Meteor Session
|
||||
var studies = Session.get('studies');
|
||||
|
||||
// Look through every study and their series' until we find the
|
||||
// series that matches the seriesInstanceUid and studyInstanceUid
|
||||
var studyInstanceUid = this.data.studyInstanceUid;
|
||||
var seriesInstanceUid = this.data.seriesInstanceUid;
|
||||
|
||||
studies.every(function(study) {
|
||||
if (study.studyInstanceUid === studyInstanceUid) {
|
||||
data.study = study;
|
||||
@@ -242,22 +421,51 @@ Template.imageViewerViewport.onRendered(function() {
|
||||
return true;
|
||||
});
|
||||
|
||||
// If we didn't find anything, stop here
|
||||
if (!data.study || !data.series) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, load pass the data object into loadSeriesIntoViewport
|
||||
loadSeriesIntoViewport(data);
|
||||
});
|
||||
|
||||
Template.imageViewerViewport.onDestroyed(function() {
|
||||
log.info("imageViewerViewport onDestroyed");
|
||||
|
||||
// When a viewport element is being destroyed
|
||||
var element = this.find(".imageViewerViewport");
|
||||
|
||||
// Try to stop any currently playing clips
|
||||
// Otherwise the interval will continuously throw errors
|
||||
try {
|
||||
cornerstoneTools.stopClip(element);
|
||||
} catch(error) {
|
||||
log.warn(error);
|
||||
}
|
||||
|
||||
// Disable the viewport element with Cornerstone
|
||||
// This also triggers the removal of the element from all available
|
||||
// synchronizers, such as the one used for reference lines.
|
||||
cornerstone.disable(element);
|
||||
});
|
||||
|
||||
Template.imageViewerViewport.events({
|
||||
'ActivateViewport .imageViewerViewport': function(e) {
|
||||
if (this.viewportIndex === this.activeViewport.curValue) {
|
||||
return;
|
||||
}
|
||||
console.log('ActivateViewport index: ' + this.viewportIndex);
|
||||
this.activeViewport.curValue = this.viewportIndex;
|
||||
enablePrefetchOnElement(this.viewportIndex);
|
||||
displayReferenceLines(this.viewportIndex);
|
||||
},
|
||||
log.info("imageViewerViewport ActivateViewport");
|
||||
|
||||
// When an ActivateViewport event is fired, update the Meteor Session
|
||||
// with the viewport index that it was fired from.
|
||||
Session.set("activeViewport", e.viewportIndex);
|
||||
|
||||
// Add the 'active' class to the parent container to highlight the active viewport
|
||||
$('#imageViewerViewports .viewportContainer').removeClass('active');
|
||||
$(e.currentTarget).parents('.viewportContainer').addClass('active');
|
||||
|
||||
// Finally, enable stack prefetching and hide the reference lines from
|
||||
// the newly activated viewport
|
||||
var element = e.currentTarget;
|
||||
enablePrefetchOnElement(element);
|
||||
displayReferenceLines(element);
|
||||
}
|
||||
});
|
||||
@@ -20,4 +20,22 @@
|
||||
right: 0
|
||||
left: 0
|
||||
margin: auto
|
||||
height: 20px
|
||||
height: 20px
|
||||
|
||||
/* These are some good CSS settings for a circular magnifying glass
|
||||
Try removing the border-radius to make a square magnifying glass */
|
||||
.magnifyTool{
|
||||
border: 4px white solid;
|
||||
box-shadow: 2px 2px 10px #1e1e1e;
|
||||
border-radius: 50%;
|
||||
display: none;
|
||||
cursor: none;
|
||||
}
|
||||
/* prevents selection when left click dragging */
|
||||
.disable-selection {
|
||||
-moz-user-select: none; -webkit-user-select: none; -ms-user-select:none; user-select:none;
|
||||
}
|
||||
/* prevents cursor from changing to the i bar on the overlays*/
|
||||
.noIbar {
|
||||
cursor:default;
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
{{ #each viewportArray }}
|
||||
<div class="viewportContainer" style="height:{{height}}%;width:{{width}}%;">
|
||||
<div class="removable">
|
||||
{{ >imageViewerViewport viewportIndex=viewportIndex studies=studies activeViewport=activeViewport seriesInstanceUid=seriesInstanceUid studyInstanceUid=studyInstanceUid currentImageIdIndex=currentImageIdIndex viewport=viewport}}
|
||||
{{ >imageViewerViewport viewportIndex=viewportIndex seriesInstanceUid=seriesInstanceUid studyInstanceUid=studyInstanceUid currentImageIdIndex=currentImageIdIndex viewport=viewport}}
|
||||
</div>
|
||||
</div>
|
||||
{{ /each }}
|
||||
|
||||
+110
-31
@@ -1,49 +1,55 @@
|
||||
Template.imageViewerViewports.onRendered(function() {
|
||||
this.autorun(function() {
|
||||
var studies = Session.get('studies');
|
||||
var eventType = "ViewerBaseStudyLoaded";
|
||||
var eventData = {
|
||||
studies: studies
|
||||
};
|
||||
$(document).trigger(eventType, eventData);
|
||||
});
|
||||
});
|
||||
|
||||
Template.imageViewerViewports.helpers({
|
||||
height: function() {
|
||||
var viewportRows;
|
||||
if (!Template.parentData(1).viewportRows) {
|
||||
viewportRows = 1;
|
||||
} else {
|
||||
viewportRows = Template.parentData(1).viewportRows.curValue; //Having issues with .get(), not sure why?
|
||||
}
|
||||
var viewportRows = this.viewportRows || 1;
|
||||
return 100 / viewportRows;
|
||||
},
|
||||
width: function() {
|
||||
var viewportColumns;
|
||||
if (!Template.parentData(1).viewportColumns) {
|
||||
viewportColumns = 1;
|
||||
} else {
|
||||
viewportColumns = Template.parentData(1).viewportColumns.curValue; //Having issues with .get(), not sure why?
|
||||
}
|
||||
var viewportColumns = this.viewportColumns || 1;
|
||||
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 viewportRows;
|
||||
if (!this.viewportRows) {
|
||||
viewportRows = 1;
|
||||
} else {
|
||||
viewportRows = this.viewportRows.curValue; //Having issues with .get(), not sure why?
|
||||
log.info("imageViewerViewports viewportArray");
|
||||
|
||||
var studies = Session.get('studies');
|
||||
|
||||
var viewportRows = this.viewportRows || 1;
|
||||
var viewportColumns = this.viewportColumns || 1;
|
||||
|
||||
var contentId = this.contentId || $("#viewer").parents(".tab-pane.active").attr('id');
|
||||
if (this.viewportRows && this.viewportColumns) {
|
||||
viewportRows = this.viewportRows || 1;
|
||||
viewportColumns = this.viewportColumns || 1;
|
||||
} else if (ViewerData[contentId].viewportRows && ViewerData[contentId].viewportColumns) {
|
||||
viewportRows = ViewerData[contentId].viewportRows;
|
||||
viewportColumns = ViewerData[contentId].viewportColumns;
|
||||
}
|
||||
|
||||
var viewportColumns;
|
||||
if (!this.viewportColumns) {
|
||||
viewportColumns = 1;
|
||||
} else {
|
||||
viewportColumns = this.viewportColumns.curValue; //Having issues with .get(), not sure why?
|
||||
}
|
||||
|
||||
// Update viewerData
|
||||
ViewerData[contentId].viewportRows = viewportRows;
|
||||
ViewerData[contentId].viewportColumns = viewportColumns;
|
||||
Session.set("ViewerData", ViewerData);
|
||||
|
||||
var viewportData;
|
||||
if (OHIF && OHIF.viewer && !$.isEmptyObject(OHIF.viewer.imageViewerLoadedSeriesDictionary)) {
|
||||
viewportData = OHIF.viewer.imageViewerLoadedSeriesDictionary;
|
||||
if (!$.isEmptyObject(ViewerData[contentId].loadedSeriesData)) {
|
||||
viewportData = ViewerData[contentId].loadedSeriesData;
|
||||
}
|
||||
|
||||
var hangingProtocol = getHangingProtocol();
|
||||
var inputData = {
|
||||
viewportColumns: viewportColumns,
|
||||
viewportRows: viewportRows,
|
||||
studies: this.studies
|
||||
studies: studies
|
||||
};
|
||||
var hangingProtocolViewportData = hangingProtocol(inputData);
|
||||
|
||||
@@ -52,8 +58,10 @@ Template.imageViewerViewports.helpers({
|
||||
for (var i=0; i < numViewports; ++i) {
|
||||
var data = {
|
||||
viewportIndex: i,
|
||||
studies: this.studies,
|
||||
activeViewport: this.activeViewport
|
||||
// These two are necessary because otherwise the width and height helpers
|
||||
// don't get the right data context. Seems to be related to the "each" loop.
|
||||
viewportColumns: viewportColumns,
|
||||
viewportRows: viewportRows
|
||||
};
|
||||
if (viewportData && viewportData[i]) {
|
||||
data.seriesInstanceUid = viewportData[i].seriesInstanceUid;
|
||||
@@ -70,4 +78,75 @@ Template.imageViewerViewports.helpers({
|
||||
}
|
||||
return array;
|
||||
},
|
||||
});
|
||||
|
||||
var savedSeriesData,
|
||||
savedViewportRows,
|
||||
savedViewportColumns;
|
||||
|
||||
Template.imageViewerViewports.events({
|
||||
'dblclick .imageViewerViewport': function(e) {
|
||||
var container = $(".viewerMain").get(0);
|
||||
var data;
|
||||
var contentId = this.contentId || $("#viewer").parents(".tab-pane.active").attr('id');
|
||||
|
||||
// If there is more than one viewport on screen
|
||||
// And one of them is double-clicked, it should be rendered alone
|
||||
// If it is double-clicked again, the viewer should revert to the previous layout
|
||||
if ($(e.currentTarget).hasClass('zoomed')) {
|
||||
// Revert to saved settings
|
||||
ViewerData[contentId].loadedSeriesData = $.extend(true, {}, savedSeriesData);
|
||||
ViewerData[contentId].viewportRows = savedViewportRows;
|
||||
ViewerData[contentId].viewportColumns = savedViewportColumns;
|
||||
|
||||
savedViewportRows = 0;
|
||||
savedViewportColumns = 0;
|
||||
|
||||
data = {
|
||||
viewportRows: ViewerData[contentId].viewportRows,
|
||||
viewportColumns: ViewerData[contentId].viewportColumns,
|
||||
};
|
||||
|
||||
// Render the imageViewerViewports template with these settings
|
||||
$('#imageViewerViewports').remove();
|
||||
UI.renderWithData(Template.imageViewerViewports, data, container);
|
||||
|
||||
// Remove the 'zoomed' class from any viewports
|
||||
$('.imageViewerViewport').removeClass('zoomed');
|
||||
} else {
|
||||
// Zoom to single viewport
|
||||
|
||||
// If only one viewport is on-screen, stop here
|
||||
if (ViewerData[contentId].viewportRows === 1 &&
|
||||
ViewerData[contentId].viewportColumns === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the current settings
|
||||
savedSeriesData = $.extend(true, {}, ViewerData[contentId].loadedSeriesData);
|
||||
savedViewportRows = ViewerData[contentId].viewportRows;
|
||||
savedViewportColumns = ViewerData[contentId].viewportColumns;
|
||||
|
||||
// Get the clicked-on viewport's index
|
||||
var viewportIndex = this.viewportIndex;
|
||||
|
||||
// Set the first viewport's data to be the same as the currently clicked-on viewport
|
||||
ViewerData[contentId].loadedSeriesData[0] = ViewerData[contentId].loadedSeriesData[viewportIndex];
|
||||
|
||||
// Set the basic template data
|
||||
data = {
|
||||
viewportRows: 1,
|
||||
viewportColumns: 1,
|
||||
};
|
||||
|
||||
// Render the imageViewerViewports template with these settings
|
||||
$('#imageViewerViewports').remove();
|
||||
UI.renderWithData(Template.imageViewerViewports, data, container);
|
||||
|
||||
// Add the 'zoomed' class to the lone remaining viewport
|
||||
$('.imageViewerViewport').eq(0).addClass('zoomed');
|
||||
|
||||
}
|
||||
log.info('dblclick');
|
||||
}
|
||||
});
|
||||
@@ -6,6 +6,12 @@
|
||||
float: left
|
||||
border: 2px solid #777
|
||||
|
||||
&.active, &:hover.active
|
||||
border: 2px solid #9A6617
|
||||
|
||||
&:hover
|
||||
border: 2px solid white
|
||||
|
||||
.removable
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Adds the 'hover' class to cells above and to the left of the current cell
|
||||
* This is used to "fill in" the grid that the user will change the layout to,
|
||||
* if they click on a specific table cell.
|
||||
*
|
||||
* @param currentCell
|
||||
*/
|
||||
function highlightCells(currentCell) {
|
||||
var cells = $('.layoutChooser table td');
|
||||
cells.removeClass('hover');
|
||||
@@ -27,31 +34,18 @@ Template.layoutChooser.events({
|
||||
},
|
||||
'click .layoutChooser table td': function(evt) {
|
||||
$('#imageViewerViewports').remove();
|
||||
var container = $(".viewerMain").get(0);
|
||||
|
||||
var currentCell = $(evt.currentTarget);
|
||||
var rowIndex = currentCell.closest('tr').index();
|
||||
var columnIndex = currentCell.index();
|
||||
|
||||
var data = {};
|
||||
|
||||
// Add 1 because the indices start from zero
|
||||
if (this.viewportRows) {
|
||||
this.viewportRows.curValue = rowIndex + 1;
|
||||
data.viewportRows = this.viewportRows;
|
||||
} else {
|
||||
data.viewportRows = 1;
|
||||
}
|
||||
|
||||
if (this.viewportColumns) {
|
||||
this.viewportColumns.curValue = columnIndex + 1;
|
||||
data.viewportColumns = this.viewportColumns;
|
||||
} else {
|
||||
data.viewportColumns = 1;
|
||||
}
|
||||
|
||||
data.studies = Template.parentData(2).studies;
|
||||
data.activeViewport = Template.parentData(2).activeViewport;
|
||||
var data = {
|
||||
viewportRows: rowIndex + 1,
|
||||
viewportColumns: columnIndex + 1,
|
||||
};
|
||||
|
||||
var container = $(".viewerMain").get(0);
|
||||
UI.renderWithData(Template.imageViewerViewports, data, container);
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
<template name="loadingIndicator">
|
||||
<div class="imageViewerLoadingIndicator loadingIndicator">
|
||||
<div class="indicatorContents">
|
||||
<p>Loading...</p>
|
||||
<p>Loading... {{percentComplete}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="imageViewerErrorLoadingIndicator loadingIndicator">
|
||||
|
||||
@@ -58,4 +58,13 @@ errorLoadingHandler = function(element, imageId, error, source) {
|
||||
if (error) {
|
||||
errorLoadingIndicator.find('.details').text("Details: " + error);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Template.loadingIndicator.helpers({
|
||||
'percentComplete': function(e) {
|
||||
var percentComplete = Session.get('CornerstoneLoadProgress' + this.viewportIndex);
|
||||
if (percentComplete && percentComplete !== 100) {
|
||||
return percentComplete + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
Template.playClipButton.helpers({
|
||||
'isPlaying': function() {
|
||||
Session.get('UpdateCINE');
|
||||
var viewportIndex = this.activeViewport.curValue;
|
||||
return !!OHIF.viewer.isPlaying[viewportIndex];
|
||||
var activeViewport = Session.get('activeViewport');
|
||||
return !!OHIF.viewer.isPlaying[activeViewport];
|
||||
}
|
||||
});
|
||||
@@ -6,11 +6,11 @@
|
||||
{{ /each }}
|
||||
|
||||
{{ #if includePlayClipButton }}
|
||||
{{ >playClipButton activeViewport=activeViewport}}
|
||||
{{ >playClipButton }}
|
||||
{{ /if }}
|
||||
|
||||
{{ #if includeLayoutButton }}
|
||||
{{ >layoutButton viewportRows=viewportRows viewportColumns=viewportColumns}}
|
||||
{{ >layoutButton }}
|
||||
{{ /if }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,27 @@ function getDefaultButtonData() {
|
||||
iconClasses: 'fa fa-sun-o'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'wwwcRegion',
|
||||
title: 'Window by Region',
|
||||
classes: 'imageViewerTool',
|
||||
iconClasses: 'fa fa-square'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'magnify',
|
||||
title: 'Magnify',
|
||||
classes: 'imageViewerTool',
|
||||
iconClasses: 'fa fa-circle'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'annotate',
|
||||
title: 'Annotation',
|
||||
classes: 'imageViewerTool',
|
||||
iconClasses: 'fa fa-arrows-h'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'invert',
|
||||
title: 'Invert',
|
||||
@@ -70,6 +91,20 @@ function getDefaultButtonData() {
|
||||
classes: 'imageViewerTool',
|
||||
iconClasses: 'fa fa-square-o'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'resetViewport',
|
||||
title: 'Reset Viewport',
|
||||
classes: 'imageViewerCommand',
|
||||
iconClasses: 'fa fa-undo'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'clearTools',
|
||||
title: 'Clear tools',
|
||||
classes: 'imageViewerCommand',
|
||||
iconClasses: 'fa fa-trash'
|
||||
});
|
||||
return buttonData;
|
||||
}
|
||||
|
||||
@@ -84,8 +119,8 @@ Template.toolbar.events({
|
||||
if (!OHIF.viewer.functionList.hasOwnProperty(command)) {
|
||||
return;
|
||||
}
|
||||
var viewportIndex = Template.parentData(0).activeViewport.get();
|
||||
var element = $('.imageViewerViewport').get(viewportIndex);
|
||||
var activeViewport = Session.get('activeViewport');
|
||||
var element = $('.imageViewerViewport').get(activeViewport);
|
||||
OHIF.viewer.functionList[command](element);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
color: #ffffff
|
||||
background-color: #424242
|
||||
border-color: #424242
|
||||
outline: none
|
||||
|
||||
&.active, &:active
|
||||
background-color: #3B678E
|
||||
+10
-2
@@ -1,5 +1,13 @@
|
||||
updateOrientationMarkers = function(element, viewport) {
|
||||
// Updates the orientation labels on the viewport
|
||||
/**
|
||||
* Updates the orientation labels on a Cornerstone-enabled Viewport element
|
||||
* when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
|
||||
*
|
||||
* @param element The DOM element of the Cornerstone viewport
|
||||
*/
|
||||
updateOrientationMarkers = function(element) {
|
||||
// Get the current viewport settings
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template name="viewportOverlay">
|
||||
<div class="imageViewerViewportOverlay noselect">
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{patientName}}</div>
|
||||
<div>{{formatPN patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="topright dicomTag">
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{studyDate}} {{studyTime}}</div>
|
||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomright dicomTag">
|
||||
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
/**
|
||||
* Creates a set of series to be placed in the Study Browser
|
||||
* The series that appear in the Study Browser must represent
|
||||
* imaging modalities.
|
||||
*
|
||||
* Furthermore, for drag/drop functionality,
|
||||
* it is easiest if the stack objects also contain information about
|
||||
* which study they are linked to.
|
||||
*
|
||||
* @param study The study instance to be used
|
||||
* @returns {Array} An array of series to be placed in the Study Browser
|
||||
*/
|
||||
createStacks = function(study) {
|
||||
// Define an empty array of stacks
|
||||
var stacks = [];
|
||||
|
||||
// TODO: Split by multi-frame, modality, image size, etc
|
||||
study.seriesList.forEach(function(series) {
|
||||
// If the series has no instanced, skip it
|
||||
if (!series.instances) {
|
||||
return;
|
||||
}
|
||||
@@ -12,6 +27,11 @@ createStacks = function(study) {
|
||||
if (!anInstance || !anInstance.rows) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Include the study instance Uid for drag/drop purposes
|
||||
series.studyInstanceUid = study.studyInstanceUid;
|
||||
|
||||
// Add this series to the list of stacks
|
||||
stacks.push(series);
|
||||
});
|
||||
return stacks;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
(function($) {
|
||||
|
||||
|
||||
//Collapse the container
|
||||
function collapseContainer ( container ) {
|
||||
container.toggleClass("collapseHorizontal");
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
encodeQueryData = function(data)
|
||||
{
|
||||
var ret = [];
|
||||
|
||||
for (var d in data) {
|
||||
if(data[d]) {
|
||||
ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
|
||||
}
|
||||
if(data[d]) {
|
||||
ret.push(encodeURIComponent(d) + "=" + encodeURIComponent(data[d]));
|
||||
}
|
||||
}
|
||||
return ret.join("&");
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
function checkToken(token, data, dataOffset) {
|
||||
|
||||
if(dataOffset + token.length > data.length) {
|
||||
//console.log('dataOffset >> ', dataOffset);
|
||||
return false;
|
||||
}
|
||||
|
||||
var endIndex = dataOffset;
|
||||
|
||||
for(var i = 0; i < token.length; i++) {
|
||||
if(token[i] !== data[endIndex++]) {
|
||||
if(endIndex > 520000) {
|
||||
console.log('token=',uint8ArrayToString(token));
|
||||
console.log('data=', uint8ArrayToString(data, dataOffset, endIndex-dataOffset));
|
||||
console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(data[endIndex]), endIndex);
|
||||
console.log('miss at %d %s dataOffset=%d', i, String.fromCharCode(token[endIndex]), endIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
findIndexOfString = function(data, str, offset) {
|
||||
|
||||
offset = offset || 0;
|
||||
|
||||
var token = stringToUint8Array(str);
|
||||
|
||||
for(var i=offset; i < data.length; i++) {
|
||||
if(token[0] === data[i]) {
|
||||
//console.log('match @', i);
|
||||
if(checkToken(token, data, i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
getActiveViewportElement = function() {
|
||||
// TODO: Fix this, just hardcoded for now
|
||||
var element = $('.imageViewerViewport').get(0);
|
||||
return element;
|
||||
};
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Obtain an imageId for Cornerstone from an image instance
|
||||
*
|
||||
* @param instance
|
||||
* @returns {string} The imageId to be used by Cornerstone
|
||||
*/
|
||||
getImageId = function(instance) {
|
||||
if (!instance) {
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Obtain an imageId for Cornerstone based on the WADO-RS scheme
|
||||
*
|
||||
* @param instance
|
||||
* @returns {string} The imageId to be used by Cornerstone
|
||||
*/
|
||||
|
||||
getWADORSImageId = function(instance) {
|
||||
var columnPixelSpacing = 1.0;
|
||||
var rowPixelSpacing = 1.0;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/**
|
||||
* A global Blaze UI helper function to format DICOM Dates using the Moment library
|
||||
*/
|
||||
UI.registerHelper('formatDA', function (context, format, options) {
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
/*var date = DICOMUtilities.toDate(context, '', false);
|
||||
if(!date) {
|
||||
return context;
|
||||
}*/
|
||||
var date = context;
|
||||
var dateAsMoment = moment(date);
|
||||
var dateAsMoment = moment(context, "YYYYMMDD");
|
||||
var strFormat = "MMM D, YYYY";
|
||||
if (options) {
|
||||
strFormat = format;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
UI.registerHelper('formatNumberPrecision', function(context, precision, options) {
|
||||
if(context) {
|
||||
/**
|
||||
* A global Blaze UI helper to format a float value to a specified precision
|
||||
*/
|
||||
UI.registerHelper('formatNumberPrecision', function(context, precision) {
|
||||
if (context) {
|
||||
return parseFloat(context).toFixed(precision);
|
||||
}
|
||||
});
|
||||
@@ -1,20 +1,9 @@
|
||||
UI.registerHelper('formatDA', function (context, format, options) {
|
||||
/**
|
||||
* A global Blaze UI helper to format a patient name for display purposes
|
||||
*/
|
||||
UI.registerHelper('formatPN', function (context) {
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
var pn = context;
|
||||
var str = '';
|
||||
if(pn.prefix) {
|
||||
str = pn.prefix + ' ';
|
||||
}
|
||||
str += (pn.familyName || '') + ', ';
|
||||
str += (pn.givenName || '');
|
||||
if(pn.middleName) {
|
||||
str += ' ' + pn.middleName;
|
||||
}
|
||||
if(pn.suffix) {
|
||||
str += ' ' + pn.suffix;
|
||||
}
|
||||
|
||||
return str;
|
||||
return context.replace('^', ', ');
|
||||
});
|
||||
@@ -1,13 +1,14 @@
|
||||
UI.registerHelper('formatTM', function(context, format, options) {
|
||||
/**
|
||||
* A global Blaze UI helper to format a DICOM Time for display using the Moment library
|
||||
*/
|
||||
UI.registerHelper('formatTM', function(context, format) {
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
var date = context;
|
||||
var dateAsMoment = moment(date);
|
||||
var dateAsMoment = moment(context);
|
||||
var strFormat = "HH:mm:ss";
|
||||
if (options) {
|
||||
strFormat = format;
|
||||
}
|
||||
|
||||
return dateAsMoment.format(strFormat);
|
||||
});
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* Helper function to determine if the current client devices
|
||||
* is touch-capable. This can be used to modify certain aspects of the UI.
|
||||
*
|
||||
* The check may not work on all devices!
|
||||
*
|
||||
* @returns {boolean} true if the client device is touch-capable, false otherwise
|
||||
*/
|
||||
isTouchDevice = function() {
|
||||
return (('ontouchstart' in window) ||
|
||||
(navigator.MaxTouchPoints > 0) ||
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
var metaDataLookup = {};
|
||||
|
||||
/**
|
||||
* Cornerstone MetaData provider to store image meta data
|
||||
* Data from instances, series, and studies are associated with
|
||||
* imageIds to facilitate usage of this information by Cornerstone's Tools
|
||||
*
|
||||
* e.g. the imagePlane metaData object contains instance information about
|
||||
* row/column pixel spacing, patient position, and patient orientation. It
|
||||
* is used in CornerstoneTools to position reference lines and orientation markers.
|
||||
*
|
||||
* @param {String} imageId The Cornerstone ImageId
|
||||
* @param {Object} data An object containing instance, series, and study metaData
|
||||
*/
|
||||
addMetaData = function(imageId, data) {
|
||||
var instanceMetaData = data.instance;
|
||||
var seriesMetaData = data.series;
|
||||
@@ -10,27 +22,33 @@ addMetaData = function(imageId, data) {
|
||||
var metaData = {};
|
||||
|
||||
metaData.study = {
|
||||
date: instanceMetaData.studyDate,
|
||||
time: instanceMetaData.studyTime,
|
||||
description: instanceMetaData.studyDescription
|
||||
instanceUid: studyMetaData.studyInstanceUid,
|
||||
date: studyMetaData.studyDate,
|
||||
time: studyMetaData.studyTime,
|
||||
description: studyMetaData.studyDescription
|
||||
};
|
||||
|
||||
metaData.series = {
|
||||
description: seriesMetaData.seriesDescription,
|
||||
number: seriesMetaData.seriesNumber,
|
||||
modality: seriesMetaData.modality,
|
||||
instanceUid: seriesMetaData.instanceUid,
|
||||
instanceUid: seriesMetaData.seriesInstanceUid,
|
||||
numImages: numImages
|
||||
};
|
||||
|
||||
metaData.instance = {
|
||||
wadouri: instanceMetaData.wadouri,
|
||||
imageType: instanceMetaData.imageType,
|
||||
photometricInterpretation: instanceMetaData.photometricInterpretation,
|
||||
sopInstanceUid: instanceMetaData.sopInstanceUid,
|
||||
sopClassUid: instanceMetaData.sopClassUid,
|
||||
number: instanceMetaData.instanceNumber,
|
||||
index: imageIndex
|
||||
};
|
||||
|
||||
metaData.patient = {
|
||||
name: instanceMetaData.patientName,
|
||||
id: instanceMetaData.patientId
|
||||
name: studyMetaData.patientName,
|
||||
id: studyMetaData.patientId
|
||||
};
|
||||
|
||||
// If there is sufficient information, populate
|
||||
@@ -66,6 +84,15 @@ addMetaData = function(imageId, data) {
|
||||
metaDataLookup[imageId] = metaData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Looks up metaData for Cornerstone Tools given a specified type and imageId
|
||||
* A type may be, e.g. 'study', or 'patient', or 'imagePlane'. These types
|
||||
* are keys in the stored metaData objects.
|
||||
*
|
||||
* @param type
|
||||
* @param imageId
|
||||
* @returns {Object} Relevant metaData of the specified type
|
||||
*/
|
||||
function provider(type, imageId) {
|
||||
var imageMetaData = metaDataLookup[imageId];
|
||||
if (!imageMetaData) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* This function destroys and rerenders the imageViewerViewport template.
|
||||
* This function destroys and re-renders the imageViewerViewport template.
|
||||
* It uses the data provided (object containing seriesIndex and studyIndex) to
|
||||
* load a new series into the produced viewport.
|
||||
*
|
||||
@@ -7,12 +7,16 @@
|
||||
* @param data An object containing a seriesIndex and studyIndex for a study to load into this viewport
|
||||
*/
|
||||
rerenderViewportWithNewSeries = function(element, data) {
|
||||
// Get the container and index of the current viewport.
|
||||
// The parent container is identified because it is later removed from the DOM
|
||||
var container = $(element).parents('.viewportContainer').get(0);
|
||||
var viewportIndex = $(container).index();
|
||||
|
||||
// Record the current viewportIndex so this can be passed into the re-rendering call
|
||||
data.viewportIndex = viewportIndex;
|
||||
|
||||
// Update the dictionary of loaded series for the specified viewport
|
||||
OHIF.viewer.imageViewerLoadedSeriesDictionary[viewportIndex] = {
|
||||
OHIF.viewer.loadedSeriesData[viewportIndex] = {
|
||||
seriesInstanceUid: data.seriesInstanceUid,
|
||||
studyInstanceUid: data.studyInstanceUid,
|
||||
currentImageIdIndex: 0
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Sorts the series and instances inside a study instance by their series
|
||||
* and instance numbers in ascending order.
|
||||
*
|
||||
* @param {Object} study The study instance
|
||||
*/
|
||||
sortStudy = function(study) {
|
||||
if (!study) {
|
||||
return;
|
||||
|
||||
@@ -1,7 +1 @@
|
||||
stringToUint8Array = function(str) {
|
||||
var uint=new Uint8Array(str.length);
|
||||
for(var i=0,j=str.length;i<j;i++){
|
||||
uint[i]=str.charCodeAt(i);
|
||||
}
|
||||
return uint;
|
||||
};
|
||||
|
||||
@@ -86,11 +86,14 @@ toolManager = {
|
||||
if (!element.classList.contains('empty') && canvases.length > 0) {
|
||||
// First, deactivate the current active tool
|
||||
tools[activeTool].mouse.deactivate(element, 1);
|
||||
tools[activeTool].touch.deactivate(element);
|
||||
|
||||
if (tools[activeTool].touch) {
|
||||
tools[activeTool].touch.deactivate(element);
|
||||
}
|
||||
|
||||
// First, get the stack toolData
|
||||
var toolData = cornerstoneTools.getToolState(element, 'stack');
|
||||
if (toolData === undefined || toolData.data === undefined || toolData.data.length === 0) {
|
||||
if (!toolData || !toolData.data || !toolData.data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,7 +130,10 @@ toolManager = {
|
||||
// Activate the chosen tool
|
||||
tools[tool].mouse.activate(element, 1);
|
||||
}
|
||||
tools[tool].touch.activate(element);
|
||||
|
||||
if (tools[tool].touch) {
|
||||
tools[tool].touch.activate(element);
|
||||
}
|
||||
|
||||
cornerstoneTools.zoomTouchPinch.activate(element);
|
||||
cornerstoneTools.panMultiTouch.activate(element);
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
uint8ArrayToString = function(data, offset, length) {
|
||||
offset = offset || 0;
|
||||
length = length || data.length - offset;
|
||||
var str = "";
|
||||
for(var i=offset; i < offset + length; i++) {
|
||||
str += String.fromCharCode(data[i]);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
@@ -1,5 +1,13 @@
|
||||
updateOrientationMarkers = function(element, viewport) {
|
||||
// Updates the orientation labels on the viewport
|
||||
/**
|
||||
* Updates the orientation labels on a Cornerstone-enabled Viewport element
|
||||
* when the viewport settings change (e.g. when a horizontal flip or a rotation occurs)
|
||||
*
|
||||
* @param element The DOM element of the Cornerstone viewport
|
||||
*/
|
||||
updateOrientationMarkers = function(element) {
|
||||
// Get the current viewport settings
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
var imagePlane = cornerstoneTools.metaData.get('imagePlane', enabledElement.image.imageId);
|
||||
|
||||
@@ -14,7 +22,7 @@ updateOrientationMarkers = function(element, viewport) {
|
||||
|
||||
var markers = {
|
||||
top: oppositeColumnString,
|
||||
left: oppositeRowString,
|
||||
left: oppositeRowString
|
||||
};
|
||||
|
||||
// If any vertical or horizontal flips are applied, change the orientation strings ahead of
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Create package logger using loglevel
|
||||
// https://atmospherejs.com/spacejamio/loglevel
|
||||
log = loglevel.createPackageLogger('viewerbase', defaultLevel = 'info');
|
||||
@@ -10,10 +10,17 @@ Package.onUse(function (api) {
|
||||
api.use('standard-app-packages');
|
||||
api.use('jquery');
|
||||
api.use('stylus');
|
||||
api.use('accounts-password');
|
||||
api.use('ian:accounts-ui-bootstrap-3');
|
||||
api.use('http');
|
||||
api.use('practicalmeteor:loglevel');
|
||||
api.use('momentjs:moment');
|
||||
|
||||
// Our custom package
|
||||
api.use('cornerstone');
|
||||
|
||||
api.addFiles('log.js', ['client', 'server']);
|
||||
|
||||
// ---------- Components ----------
|
||||
|
||||
// Basic components
|
||||
@@ -22,7 +29,14 @@ Package.onUse(function (api) {
|
||||
api.addFiles('components/basic/login/login.html', 'client');
|
||||
api.addFiles('components/basic/notFound/notFound.html', 'client');
|
||||
|
||||
api.addFiles('components/basic/loadingText/loadingText.html', 'client');
|
||||
api.addFiles('components/basic/loadingText/loadingText.styl', 'client');
|
||||
|
||||
api.addFiles('components/basic/removableBackdrop/removableBackdrop.html', 'client');
|
||||
api.addFiles('components/basic/removableBackdrop/removableBackdrop.styl', 'client');
|
||||
|
||||
api.addFiles('components/basic/hidingPanel/hidingPanel.html', 'client');
|
||||
api.addFiles('components/basic/hidingPanel/hidingPanel.js', 'client');
|
||||
api.addFiles('components/basic/hidingPanel/hidingPanel.styl', 'client');
|
||||
|
||||
// Study Browser components
|
||||
@@ -81,33 +95,24 @@ Package.onUse(function (api) {
|
||||
api.addFiles('lib/accountsConfig.js', 'client');
|
||||
api.addFiles('lib/createStacks.js', 'client');
|
||||
api.addFiles('lib/encodeQueryData.js', ['client', 'server']);
|
||||
api.addFiles('lib/findIndexOfString.js', 'client');
|
||||
api.addFiles('lib/getActiveViewportElement.js', 'client');
|
||||
api.addFiles('lib/getImageId.js', 'client');
|
||||
api.addFiles('lib/getWADORSImageId.js', 'client');
|
||||
api.addFiles('lib/isTouchDevice.js', 'client');
|
||||
api.addFiles('lib/metaDataProvider.js', 'client');
|
||||
api.addFiles('lib/rerenderViewportWithNewSeries.js', 'client');
|
||||
api.addFiles('lib/sortStudy.js', 'client');
|
||||
api.addFiles('lib/stringToUint8Array.js', 'client');
|
||||
api.addFiles('lib/toolManager.js', 'client');
|
||||
api.addFiles('lib/uint8ArrayToString.js', 'client');
|
||||
api.addFiles('lib/updateOrientationMarkers.js', 'client');
|
||||
|
||||
//api.export('accountsConfig', 'client');
|
||||
api.export('createStacks', 'client');
|
||||
api.export('encodeQueryData', ['client', 'server']);
|
||||
api.export('findIndexOfString', 'client');
|
||||
api.export('getActiveViewportElement', 'client');
|
||||
api.export('getImageId', 'client');
|
||||
api.export('getWADORSImageId', 'client');
|
||||
api.export('isTouchDevice', 'client');
|
||||
api.export('metaDataProvider', 'client');
|
||||
api.export('rerenderViewportWithNewSeries', 'client');
|
||||
api.export('sortStudy', 'client');
|
||||
api.export('stringToUint8Array', 'client');
|
||||
api.export('toolManager', 'client');
|
||||
api.export('uint8ArrayToString', 'client');
|
||||
api.export('updateOrientationMarkers', 'client');
|
||||
|
||||
// UI Helpers
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
Meteor.methods({
|
||||
'GetStudyMetadata' : function(studyInstanceUid) {
|
||||
console.log('GetStudyMetadata(%s)', studyInstanceUid);
|
||||
/**
|
||||
* Retrieves Study metadata given a Study Instance UID
|
||||
* This Meteor method is available from both the client and the server
|
||||
*/
|
||||
'GetStudyMetadata' : function(studyInstanceUid) {
|
||||
log.info('GetStudyMetadata(%s)', studyInstanceUid);
|
||||
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
// Get the server data. This is user-defined in the
|
||||
// config.json files used to run the Meteor server
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
|
||||
//return Services.QIDO.Instances(server, studyInstanceUid);
|
||||
return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
|
||||
}
|
||||
return Services.WADO.RetrieveMetadata(server, studyInstanceUid);
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,13 @@
|
||||
Meteor.methods({
|
||||
'WorklistSearch' : function(filter) {
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
return Services.QIDO.Studies(server, filter);
|
||||
}
|
||||
/**
|
||||
* Use the specified filter to conduct a search from the DICOM server
|
||||
* @param filter
|
||||
*/
|
||||
'WorklistSearch': function(filter) {
|
||||
// Get the server data. This is user-defined in the
|
||||
// config.json files used to run the Meteor server
|
||||
var server = Meteor.settings.dicomWeb.endpoints[0];
|
||||
|
||||
return Services.QIDO.Studies(server, filter);
|
||||
}
|
||||
});
|
||||
@@ -1,59 +1,83 @@
|
||||
/**
|
||||
* Creates a QIDO URL given the server settings and a study instance UID
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {string} URL to be used for QIDO calls
|
||||
*/
|
||||
function buildUrl(server, studyInstanceUid) {
|
||||
|
||||
var url = server.qidoRoot + '/studies/' + studyInstanceUid + '/instances';
|
||||
|
||||
return url;
|
||||
return server.qidoRoot + '/studies/' + studyInstanceUid + '/instances';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses data returned from a QIDO search and transforms it into
|
||||
* an array of series that are present in the study
|
||||
*
|
||||
* @param server The DICOM server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {Array} Series List
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
resultData.forEach(function(instance) {
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if(!series) {
|
||||
series = {
|
||||
seriesInstanceUid : seriesInstanceUid,
|
||||
seriesNumber : DICOMWeb.getString(instance['00200011']),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
// The uri for the dicomweb
|
||||
// NOTE: DCM4CHEE seems to return the data zipped
|
||||
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
|
||||
// know how to parse yet
|
||||
//var uri = DICOMWeb.getString(instance['00081190']);
|
||||
//uri = uri.replace('wado-rs', 'dicom-web');
|
||||
resultData.forEach(function(instance) {
|
||||
// Use seriesMap to cache series data
|
||||
// If the series instance UID has already been used to
|
||||
// process series data, continue using that series
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
|
||||
// manually create a WADO-URI from the UID's
|
||||
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
// If no series data exists in the seriesMap cache variable,
|
||||
// process any available series data
|
||||
if(!series) {
|
||||
series = {
|
||||
seriesInstanceUid : seriesInstanceUid,
|
||||
seriesNumber : DICOMWeb.getString(instance['00200011']),
|
||||
instances: []
|
||||
};
|
||||
|
||||
series.instances.push({
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: DICOMWeb.getString(instance['00200013'])
|
||||
});
|
||||
// Save this data in the seriesMap cache variable
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
// The uri for the dicomweb
|
||||
// NOTE: DCM4CHEE seems to return the data zipped
|
||||
// NOTE: Orthanc returns the data with multi-part mime which cornerstoneWADOImageLoader doesn't
|
||||
// know how to parse yet
|
||||
//var uri = DICOMWeb.getString(instance['00081190']);
|
||||
//uri = uri.replace('wado-rs', 'dicom-web');
|
||||
|
||||
// manually create a WADO-URI from the UIDs
|
||||
// NOTE: Haven't been able to get Orthanc's WADO-URI to work yet - maybe its not configured?
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
var uri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
|
||||
// Add this instance to the current series
|
||||
series.instances.push({
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
uri: uri,
|
||||
instanceNumber: DICOMWeb.getString(instance['00200013'])
|
||||
});
|
||||
});
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a set of instances using a QIDO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{wadoUriRoot: String, studyInstanceUid: String, seriesList: Array}}
|
||||
*/
|
||||
Services.QIDO.Instances = function(server, studyInstanceUid) {
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
var study = {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
|
||||
};
|
||||
|
||||
return study;
|
||||
return {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
|
||||
};
|
||||
};
|
||||
@@ -1,38 +1,56 @@
|
||||
/**
|
||||
* Produces a QIDO URL given server details and a set of specified search filter
|
||||
* items
|
||||
*
|
||||
* @param server
|
||||
* @param filter
|
||||
* @returns {string} The URL with encoded filter query data
|
||||
*/
|
||||
function filterToQIDOURL(server, filter) {
|
||||
var parameters = {
|
||||
PatientName : filter.patientName,
|
||||
PatientID: filter.patientId,
|
||||
AccessionNumber : filter.accessionNumber,
|
||||
limit : filter.limit || 20,
|
||||
includefield : server.qidoSupportsIncludeField ? 'all' : undefined
|
||||
};
|
||||
var url = server.qidoRoot + '/studies?' + encodeQueryData(parameters);
|
||||
return url;
|
||||
var parameters = {
|
||||
PatientName : filter.patientName,
|
||||
PatientID: filter.patientId,
|
||||
AccessionNumber : filter.accessionNumber,
|
||||
limit : filter.limit || 20,
|
||||
includefield : server.qidoSupportsIncludeField ? 'all' : undefined
|
||||
};
|
||||
return server.qidoRoot + '/studies?' + encodeQueryData(parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses resulting data from a QIDO call into a set of Study MetaData
|
||||
*
|
||||
* @param resultData
|
||||
* @returns {Array} An array of Study MetaData objects
|
||||
*/
|
||||
function resultDataToStudies(resultData) {
|
||||
var studies = [];
|
||||
resultData.forEach(function(study) {
|
||||
studies.push({
|
||||
patientName: DICOMWeb.getName(study['00100010']),
|
||||
patientId: DICOMWeb.getString(study['00100020']),
|
||||
accessionNumber : DICOMWeb.getString(study['00080050']),
|
||||
studyDate: DICOMWeb.getString(study['00080020']),
|
||||
modalities: DICOMWeb.getString(study['00080061']),
|
||||
studyDescription: DICOMWeb.getString(study['00081030']),
|
||||
imageCount: DICOMWeb.getString(study['00201208']),
|
||||
studyInstanceUid: DICOMWeb.getString(study['0020000D'])
|
||||
//url : server.wadoRoot + '/studies/' + DICOMWeb.getString(study['0020000D']) + '/metadata'
|
||||
var studies = [];
|
||||
|
||||
resultData.forEach(function(study) {
|
||||
studies.push({
|
||||
studyInstanceUid: DICOMWeb.getString(study['0020000D']),
|
||||
// 00080005 = SpecificCharacterSet
|
||||
studyDate: DICOMWeb.getString(study['00080020']),
|
||||
studyTime: DICOMWeb.getString(study['00080030']),
|
||||
accessionNumber: DICOMWeb.getString(study['00080050']),
|
||||
referringPhysicianName: DICOMWeb.getString(study['00080090']),
|
||||
// 00081190 = URL
|
||||
patientName: DICOMWeb.getName(study['00100010']),
|
||||
patientId: DICOMWeb.getString(study['00100020']),
|
||||
patientBirthdate: DICOMWeb.getString(study['00100030']),
|
||||
patientSex: DICOMWeb.getString(study['00100040']),
|
||||
imageCount: DICOMWeb.getString(study['00201208']),
|
||||
studyId: DICOMWeb.getString(study['00200010']),
|
||||
studyDescription: DICOMWeb.getString(study['00081030']),
|
||||
modalities: DICOMWeb.getString(study['00080061'])
|
||||
});
|
||||
});
|
||||
});
|
||||
return studies;
|
||||
return studies;
|
||||
}
|
||||
|
||||
|
||||
Services.QIDO.Studies = function(server, filter) {
|
||||
|
||||
var url = filterToQIDOURL(server, filter);
|
||||
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
return resultDataToStudies(result.data);
|
||||
var url = filterToQIDOURL(server, filter);
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
return resultDataToStudies(result.data);
|
||||
};
|
||||
@@ -1,88 +1,139 @@
|
||||
/**
|
||||
* Creates a URL for a WADO search
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildUrl(server, studyInstanceUid) {
|
||||
|
||||
var url = server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
|
||||
|
||||
return url;
|
||||
return server.wadoRoot + '/studies/' + studyInstanceUid + '/metadata';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SourceImageSequence, if it exists, in order
|
||||
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
|
||||
* is used to refer to this image in any accompanying DICOM-SR documents.
|
||||
*
|
||||
* @param instance
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
function getSourceImageInstanceUid(instance) {
|
||||
// TODO= Parse the whole Source Image Sequence
|
||||
// This is a really poor workaround for now.
|
||||
// Later we should probably parse the whole sequence.
|
||||
var SourceImageSequence = instance['00082112'];
|
||||
if (SourceImageSequence && SourceImageSequence.Value && SourceImageSequence.Value.length) {
|
||||
return SourceImageSequence.Value[0]['00081155'].Value[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses result data from a WADO search into Study MetaData
|
||||
* Returns an object populated with study metadata, including the
|
||||
* series list.
|
||||
*
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @param resultData
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
resultData.forEach(function(instance) {
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if(!series) {
|
||||
series = {
|
||||
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
seriesInstanceUid : seriesInstanceUid,
|
||||
seriesNumber : DICOMWeb.getNumber(instance['00200011']),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
var seriesMap = {};
|
||||
var seriesList = [];
|
||||
|
||||
if (!resultData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
var anInstance = resultData[0];
|
||||
if (!anInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceSummary = {
|
||||
// -----------
|
||||
// TODO = Fix this
|
||||
// This doesn't seem like the best place to put this, but otherwise we have no study info
|
||||
patientName: DICOMWeb.getName(instance['00100010']),
|
||||
patientId: DICOMWeb.getString(instance['00100020']),
|
||||
accessionNumber : DICOMWeb.getString(instance['00080050']),
|
||||
studyDate: DICOMWeb.getString(instance['00080020']),
|
||||
modalities: DICOMWeb.getString(instance['00080061']),
|
||||
studyDescription: DICOMWeb.getString(instance['00081030']),
|
||||
imageCount: DICOMWeb.getString(instance['00201208']),
|
||||
studyInstanceUid: DICOMWeb.getString(instance['0020000D']),
|
||||
// -----------
|
||||
imageType: DICOMWeb.getString(instance['00080008']),
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
||||
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
||||
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
||||
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
||||
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
||||
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
||||
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
||||
rows: DICOMWeb.getNumber(instance['00280010']),
|
||||
columns: DICOMWeb.getNumber(instance['00280011']),
|
||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||
var studyData = {
|
||||
seriesList: seriesList,
|
||||
patientName: DICOMWeb.getName(anInstance['00100010']),
|
||||
patientId: DICOMWeb.getString(anInstance['00100020']),
|
||||
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
|
||||
studyDate: DICOMWeb.getString(anInstance['00080020']),
|
||||
modalities: DICOMWeb.getString(anInstance['00080061']),
|
||||
studyDescription: DICOMWeb.getString(anInstance['00081030']),
|
||||
imageCount: DICOMWeb.getString(anInstance['00201208']),
|
||||
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
|
||||
};
|
||||
|
||||
if(server.imageRendering === 'wadouri') {
|
||||
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
} else {
|
||||
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
}
|
||||
resultData.forEach(function(instance) {
|
||||
var seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
var series = seriesMap[seriesInstanceUid];
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
seriesInstanceUid: seriesInstanceUid,
|
||||
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
||||
instances: []
|
||||
};
|
||||
seriesMap[seriesInstanceUid] = series;
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
return seriesList;
|
||||
var sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
|
||||
var instanceSummary = {
|
||||
imageType: DICOMWeb.getString(instance['00080008']),
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
sopInstanceUid: sopInstanceUid,
|
||||
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
||||
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
||||
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
||||
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
||||
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
||||
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
||||
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
||||
rows: DICOMWeb.getNumber(instance['00280010']),
|
||||
columns: DICOMWeb.getNumber(instance['00280011']),
|
||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance)
|
||||
};
|
||||
|
||||
if (server.imageRendering === 'wadouri') {
|
||||
instanceSummary.wadouri = server.wadoUriRoot + '?requestType=WADO&studyUID=' + studyInstanceUid + '&seriesUID=' + seriesInstanceUid + '&objectUID=' + sopInstanceUid + "&contentType=application%2Fdicom";
|
||||
} else {
|
||||
instanceSummary.wadorsuri = server.wadoRoot + '/studies/' + studyInstanceUid + '/series/' + seriesInstanceUid + '/instances/' + sopInstanceUid + '/frames/1';
|
||||
}
|
||||
|
||||
series.instances.push(instanceSummary);
|
||||
});
|
||||
|
||||
return studyData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieved Study MetaData from a DICOM server using a WADO call
|
||||
* @param server
|
||||
* @param studyInstanceUid
|
||||
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
|
||||
*/
|
||||
Services.WADO.RetrieveMetadata = function(server, studyInstanceUid) {
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
|
||||
var url = buildUrl(server, studyInstanceUid);
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
|
||||
var result = DICOMWeb.getJSON(url, server.requestOptions);
|
||||
var study = resultDataToStudyMetadata(server, studyInstanceUid, result.data);
|
||||
if (!study) {
|
||||
study = {};
|
||||
}
|
||||
|
||||
var study = {
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
studyInstanceUid: studyInstanceUid,
|
||||
seriesList: resultDataToStudyMetadata(server, studyInstanceUid, result.data)
|
||||
};
|
||||
study.wadoUriRoot = server.wadoUriRoot;
|
||||
study.studyInstanceUid = studyInstanceUid;
|
||||
|
||||
return study;
|
||||
return study;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
<template name="tabContent">
|
||||
<div role="tabpanel" class="tab-pane {{active}}" id="{{contentid}}">
|
||||
{{ >loadingText }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,4 @@
|
||||
.tab-pane
|
||||
.loadingTextDiv
|
||||
h5
|
||||
color: #777
|
||||
@@ -1,19 +1,46 @@
|
||||
Template.tabTitle.events({
|
||||
/**
|
||||
* Closes a tab when the close button is pressed in the title
|
||||
* The next tab to the left is loaded if the current tab is closed
|
||||
*
|
||||
* @param e The click event used to close the tab
|
||||
*/
|
||||
'click .close': function(e) {
|
||||
// Identify the tab title DOM node
|
||||
var tab = $(e.currentTarget).parents('a[data-toggle="tab"]').eq(0);
|
||||
|
||||
// Get the relevent contentId that this tab title represents
|
||||
// Replace any hash marks (#) that were required by Bootstrap's tab switching
|
||||
var contentId = tab.data("target").replace("#", "");
|
||||
|
||||
// Check if we are closing the active tab. If we are, prepare to switch
|
||||
// to the next tab to the left.
|
||||
var activeContentId = Session.get('activeContentId');
|
||||
if (activeContentId === contentId) {
|
||||
// Find the index of the tab that is being closed
|
||||
var tabIndex = tab.parent('li').index();
|
||||
|
||||
var tabIndex = tab.parent('li').index();
|
||||
var newActiveTabIndex = Math.max(tabIndex - 1, 0);
|
||||
var newActiveTab = $(".tabTitle").eq(newActiveTabIndex);
|
||||
var newActiveTabLink = newActiveTab.find("a[data-toggle=tab]");
|
||||
var newContentId = newActiveTabLink.data("target").replace("#", "");
|
||||
// Find the index the tab to its left
|
||||
var newActiveTabIndex = Math.max(tabIndex - 1, 0);
|
||||
|
||||
switchToTab(newContentId);
|
||||
|
||||
var tabObjectId = tabs.find({contentid: contentId}).fetch()[0]._id;
|
||||
tabs.remove(tabObjectId);
|
||||
// Find the DOM node of the tab that will be activated
|
||||
var newActiveTab = $(".tabTitle").eq(newActiveTabIndex);
|
||||
|
||||
// Find the content ID of the tab that will be switched to
|
||||
var newActiveTabLink = newActiveTab.find("a[data-toggle=tab]");
|
||||
var newContentId = newActiveTabLink.data("target").replace("#", "");
|
||||
|
||||
// Switch to this tab
|
||||
switchToTab(newContentId);
|
||||
}
|
||||
|
||||
// Find the tab to be closed in the Tabs collection
|
||||
var tabObjectId = WorklistTabs.findOne({contentid: contentId})._id;
|
||||
|
||||
// Remove this tab from the Tabs collection so it is no longer rendered
|
||||
WorklistTabs.remove(tabObjectId);
|
||||
|
||||
// Remove any stored data related to this tab from the global ViewerData structure
|
||||
delete ViewerData[contentId];
|
||||
}
|
||||
});
|
||||
@@ -1 +1,4 @@
|
||||
|
||||
.tabTitle
|
||||
button.close
|
||||
// Prevent blue glow around button in Chrome
|
||||
outline: none
|
||||
@@ -3,7 +3,7 @@
|
||||
<li role="presentation" class="tabTitle active">
|
||||
<a data-target="#worklistTab" role="tab" data-toggle="tab">Study List</a>
|
||||
</li>
|
||||
{{ #each tabs }}
|
||||
{{ #each worklistTabs }}
|
||||
{{ >tabTitle }}
|
||||
{{ /each }}
|
||||
</ul>
|
||||
@@ -14,7 +14,7 @@
|
||||
{{> worklistResult }}
|
||||
</div>
|
||||
</div>
|
||||
{{ #each tabs }}
|
||||
{{ #each worklistTabs }}
|
||||
{{ >tabContent }}
|
||||
{{ /each }}
|
||||
</div>
|
||||
|
||||
@@ -1,41 +1,124 @@
|
||||
/**
|
||||
* Template: Worklist
|
||||
*
|
||||
* This is the main component of the Worklist package
|
||||
*/
|
||||
|
||||
// Define the ViewerData global object
|
||||
// If there is currently any Session data for this object,
|
||||
// use this to repopulate the variable
|
||||
ViewerData = Session.get('ViewerData') || {};
|
||||
|
||||
Template.worklist.helpers({
|
||||
'tabs': function() {
|
||||
console.log('Updating tabs');
|
||||
return tabs.find();
|
||||
}
|
||||
});
|
||||
// Define the StudyMetaData object. This is used as a cache
|
||||
// to store study meta data information to prevent unnecessary
|
||||
// calls to the server
|
||||
var StudyMetaData = {};
|
||||
|
||||
Template.worklist.events({
|
||||
'click a[data-toggle="tab"]': function(e) {
|
||||
var contentId = $(e.currentTarget).data('target').replace("#", "");
|
||||
switchToTab(contentId);
|
||||
}
|
||||
});
|
||||
// Create the WorklistTabs collection
|
||||
WorklistTabs = new Meteor.Collection(null);
|
||||
|
||||
switchToTab = function(contentId) {
|
||||
var data = ViewerData[contentId];
|
||||
/**
|
||||
* Retrieves study metadata using a server call, and fires a callback
|
||||
* when completed.
|
||||
*
|
||||
* @params {string} studyInstanceUid The UID of the Study to be retrieved
|
||||
* @params {function} doneCallback The callback function to be executed when the study retrieval has finished
|
||||
*/
|
||||
getStudyMetadata = function(studyInstanceUid, doneCallback) {
|
||||
log.info('worklistStudy getStudyMetadata');
|
||||
|
||||
console.log("Switching to tab: " + contentId);
|
||||
$('.tabTitle a[data-target="#' + contentId + '"]').tab('show');
|
||||
|
||||
$("#viewer").remove();
|
||||
|
||||
var container = $('.tab-content').find("#" + contentId).get(0);
|
||||
if (!container) {
|
||||
// If the StudyMetaData cache already has data related to this
|
||||
// studyInstanceUid, then we should fire the doneCallback with this data
|
||||
// and stop here.
|
||||
var study = StudyMetaData[studyInstanceUid];
|
||||
if (study) {
|
||||
doneCallback(study);
|
||||
return;
|
||||
}
|
||||
|
||||
// If no study metadata is in the cache variable, we need to retrieve it from
|
||||
// the server with a call.
|
||||
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
|
||||
// Once we have retrieved the data, we sort the series' by series
|
||||
// and instance number in ascending order
|
||||
sortStudy(study);
|
||||
|
||||
// Then we store this data in the cache variable
|
||||
StudyMetaData[studyInstanceUid] = study;
|
||||
|
||||
// Finally, we fire the doneCallback with this study meta data
|
||||
doneCallback(study);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Switches to a new tab in the tabbed worklist container
|
||||
* This function renders either the Worklist or the Viewer template with new data.
|
||||
*
|
||||
* @param contentId The unique ID of the tab to be switched to
|
||||
*/
|
||||
switchToTab = function(contentId) {
|
||||
log.info("Switching to tab: " + contentId);
|
||||
|
||||
// Use Bootstrap's Tab JavaScript to show the contents of the current tab
|
||||
// Unless it is the worklist, it is currently an empty div
|
||||
$('.tabTitle a[data-target="#' + contentId + '"]').tab('show');
|
||||
|
||||
// Remove any previous Viewers from the DOM
|
||||
$("#viewer").remove();
|
||||
|
||||
// Update the 'activeContentId' variable in Session
|
||||
Session.set("activeContentId", contentId);
|
||||
|
||||
// If we are switching to the Worklist tab, reset any CSS styles
|
||||
// that have been applied to prevent scrolling in the Viewer.
|
||||
// Then stop here, since nothing needs to be re-rendered.
|
||||
if (contentId === 'worklistTab') {
|
||||
console.log('Switching to worklist');
|
||||
document.body.style.overflow = null;
|
||||
document.body.style.height = null;
|
||||
document.body.style.minWidth = null;
|
||||
document.body.style.position = null;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tab content container given the contentId string
|
||||
// If no such container exists, stop here because something is wrong
|
||||
var container = $('.tab-content').find("#" + contentId).get(0);
|
||||
if (!container) {
|
||||
log.warn('No container present with the contentId: ' + contentId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the stored ViewerData global object to retrieve the studyInstanceUid
|
||||
// related to this tab
|
||||
var studyInstanceUid = ViewerData[contentId].studyInstanceUid;
|
||||
|
||||
// Attempt to retrieve the meta data (it might be cached)
|
||||
getStudyMetadata(studyInstanceUid, function(study) {
|
||||
|
||||
// Once we have the study data, store it in a structure with
|
||||
// any other saved data about this tab (e.g. layout structure)
|
||||
var data = {
|
||||
viewportRows: ViewerData[contentId].viewportRows,
|
||||
viewportColumns: ViewerData[contentId].viewportColumns,
|
||||
contentId: contentId,
|
||||
studies: [study]
|
||||
};
|
||||
|
||||
// Update the Session variable with the loaded studies
|
||||
Session.set('studies', data.studies);
|
||||
|
||||
// Remove the loading text template that is inside the tab container by default
|
||||
container.innerHTML = "";
|
||||
|
||||
// Use Blaze to render the Viewer Template into the container
|
||||
UI.renderWithData(Template.viewer, data, container);
|
||||
|
||||
// Retrieve the DOM element of the viewer
|
||||
var imageViewer = $("#viewer");
|
||||
|
||||
// If it is present in the DOM (it should be), then apply
|
||||
// styles to prevent page scrolling and overscrolling on mobile devices
|
||||
if (imageViewer) {
|
||||
document.body.style.overflow = "hidden";
|
||||
document.body.style.height = '100%';
|
||||
@@ -43,33 +126,71 @@ switchToTab = function(contentId) {
|
||||
document.body.style.minWidth = 0;
|
||||
document.body.style.position = 'fixed'; // Prevent overscroll on mobile devices
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens a new tab in the tabbed worklist environment using
|
||||
* a given study and new tab title.
|
||||
*
|
||||
* @param studyInstanceUid The UID of the Study to be opened
|
||||
* @param title The title to be used for the tab heading
|
||||
*/
|
||||
openNewTab = function(studyInstanceUid, title) {
|
||||
// Generate a unique ID to represent this tab
|
||||
// We can't just use the Mongo entry ID because
|
||||
// then it will change after hot-reloading.
|
||||
var contentid = generateUUID();
|
||||
|
||||
// Create a new entry in the WorklistTabs Collection
|
||||
WorklistTabs.insert({
|
||||
title: title,
|
||||
contentid: contentid,
|
||||
active: false
|
||||
});
|
||||
|
||||
// Update the ViewerData global object
|
||||
ViewerData[contentid] = {
|
||||
title: title,
|
||||
contentid: contentid,
|
||||
studyInstanceUid: studyInstanceUid
|
||||
};
|
||||
|
||||
// Switch to the new tab
|
||||
switchToTab(contentid);
|
||||
};
|
||||
|
||||
Template.worklist.onRendered(function() {
|
||||
this.autorun(function() {
|
||||
var data = Session.get('OpenNewTabEvent');
|
||||
|
||||
// If we have no new tab data, stop here
|
||||
// (e.g. if we are rendering the worklist)
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
var contentId = data.contentid;
|
||||
if (ViewerData.hasOwnProperty(contentId)) {
|
||||
console.warn('Contentid already exists?');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the viewer data object
|
||||
ViewerData[contentId] = {
|
||||
contentId: contentId,
|
||||
studies: data.studies,
|
||||
title: data.title
|
||||
};
|
||||
Session.set('ViewerData', ViewerData);
|
||||
|
||||
// If there is a tab set as active in the Session,
|
||||
// switch to that now.
|
||||
var contentId = Session.get("activeContentId");
|
||||
if (contentId) {
|
||||
switchToTab(contentId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Template.worklist.helpers({
|
||||
/**
|
||||
* Returns the current set of Worklist Tabs
|
||||
* @returns Meteor.Collection The current state of the WorklistTabs Collection
|
||||
*/
|
||||
'worklistTabs': function() {
|
||||
return WorklistTabs.find();
|
||||
}
|
||||
});
|
||||
|
||||
Template.worklist.events({
|
||||
'click #tablist a[data-toggle="tab"]': function(e) {
|
||||
// If this tab is already active, do nothing
|
||||
var tabButton = $(e.currentTarget);
|
||||
var tabTitle = tabButton.parents('.tabTitle');
|
||||
if (tabTitle.hasClass("active")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, switch to the tab
|
||||
var contentId = tabButton.data('target').replace("#", "");
|
||||
switchToTab(contentId);
|
||||
}
|
||||
});
|
||||
@@ -46,6 +46,7 @@ body
|
||||
|
||||
&:hover
|
||||
background-color: #424242
|
||||
cursor: none
|
||||
|
||||
a
|
||||
color: white
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template name="worklistResult">
|
||||
<table id="tblStudyList" class="table table-striped">
|
||||
{{ #if studies.count }}
|
||||
<table id="tblStudyList" class="table table-striped noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Patient Name</th>
|
||||
<th>Patient ID</th>
|
||||
<th>Accession#</th>
|
||||
<th>Accession #</th>
|
||||
<th>Study Date</th>
|
||||
<th>Modality</th>
|
||||
<th>Study Description</th>
|
||||
@@ -13,8 +14,11 @@
|
||||
</thead>
|
||||
<tbody id="studyListData">
|
||||
{{#each studies}}
|
||||
{{> worklistStudy }}
|
||||
{{ >worklistStudy }}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ else }}
|
||||
{{ >loadingText }}
|
||||
{{ /if }}
|
||||
</template>
|
||||
@@ -1,7 +1,14 @@
|
||||
// Define the Studies Collection
|
||||
// This is a client-side only Collection which
|
||||
// Stores the list of studies in the Worklist
|
||||
Studies = new Mongo.Collection(null);
|
||||
|
||||
Template.worklistResult.helpers({
|
||||
/**
|
||||
* Returns a sorted instance of the Studies Collection
|
||||
* by Patient name and Study Date in Ascending order.
|
||||
*/
|
||||
studies : function() {
|
||||
return Studies.find();
|
||||
return Studies.find({}, {sort: {patientName : 1, studyDate : 1}});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
//Get all studies
|
||||
// Retrieve all studies
|
||||
search();
|
||||
|
||||
var studyDateFrom;
|
||||
@@ -6,6 +6,13 @@ var studyDateTo;
|
||||
var checkFrom = false;
|
||||
var checkTo = false;
|
||||
|
||||
/**
|
||||
* Transforms an input string into a search filter for
|
||||
* the Worklist Search call
|
||||
*
|
||||
* @param filter The input string to be searched for
|
||||
* @returns {*}
|
||||
*/
|
||||
function getFilter(filter) {
|
||||
if(filter && filter.length && filter.substr(filter.length - 1) !== '*') {
|
||||
filter += '*';
|
||||
@@ -13,16 +20,19 @@ function getFilter(filter) {
|
||||
return filter;
|
||||
}
|
||||
|
||||
//Search value
|
||||
/**
|
||||
* Search for a value in a string
|
||||
*/
|
||||
function isIndexOf(mainVal, searchVal) {
|
||||
if(mainVal === undefined || mainVal === '' || mainVal.indexOf(searchVal) > -1){
|
||||
if (mainVal === undefined || mainVal === '' || mainVal.indexOf(searchVal) > -1){
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Replace object is undefined
|
||||
/**
|
||||
* Replace object if undefined
|
||||
*/
|
||||
function replaceUndefinedColumnValue (text) {
|
||||
if (text == undefined || text === "undefined") {
|
||||
return "";
|
||||
@@ -31,94 +41,96 @@ function replaceUndefinedColumnValue (text) {
|
||||
}
|
||||
}
|
||||
|
||||
//Convert string to study date
|
||||
/**
|
||||
* Convert string to study date
|
||||
*/
|
||||
function convertStringToStudyDate (dateStr) {
|
||||
var y = dateStr.substring(0,4);
|
||||
var m = dateStr.substring(4,6);
|
||||
var d = dateStr.substring(6,8);
|
||||
var newDateStr = y+"/"+m+"/"+d;
|
||||
var date_ = new Date(newDateStr);
|
||||
return date_;
|
||||
return new Date(newDateStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a search for studies matching the worklist query parameters
|
||||
* Inserts the identified studies into the Studies Collection
|
||||
*/
|
||||
function search() {
|
||||
// Create the filters to be used for the Worklist Search
|
||||
var filter = {
|
||||
patientName: getFilter($('#patientName').val()),
|
||||
patientId: getFilter($('#patientId').val()),
|
||||
accessionNumber: getFilter($('#patientAccessionNumber').val())
|
||||
};
|
||||
|
||||
// Make sure that modality has a reasonable value, since it is occasionally
|
||||
// returned as 'undefined'
|
||||
var modality = replaceUndefinedColumnValue($('#modality').val());
|
||||
|
||||
// Clear all current studies
|
||||
Studies.remove({});
|
||||
|
||||
Meteor.call('WorklistSearch', filter, function(error, studies) {
|
||||
if (studies) {
|
||||
studies.forEach(function(study) {
|
||||
|
||||
if(isIndexOf(study.modalities, modality) &&
|
||||
(new Date(studyDateFrom).setHours(0,0,0,0) <= convertStringToStudyDate(study.studyDate) || !checkFrom) &&
|
||||
(convertStringToStudyDate(study.studyDate) <= new Date(studyDateTo).setHours(0,0,0,0) || !checkTo)) {
|
||||
|
||||
Studies.insert(study);
|
||||
}
|
||||
});
|
||||
if (!studies) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through all identified studies
|
||||
studies.forEach(function(study) {
|
||||
|
||||
// Search the rest of the parameters that aren't done via the server call
|
||||
if(isIndexOf(study.modalities, modality) &&
|
||||
(new Date(studyDateFrom).setHours(0,0,0,0) <= convertStringToStudyDate(study.studyDate) || !checkFrom) &&
|
||||
(convertStringToStudyDate(study.studyDate) <= new Date(studyDateTo).setHours(0,0,0,0) || !checkTo)) {
|
||||
|
||||
// Insert any matching studies into the Studies Collection
|
||||
Studies.insert(study);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
Template.worklistSearch.events({
|
||||
|
||||
//Search
|
||||
/**
|
||||
* Searches for studies given the current state of the form controls
|
||||
*/
|
||||
'click button#btnSearch' :function(event) {
|
||||
studyDateFrom = $('#studyDateFrom').val();
|
||||
studyDateTo = $('#studyDateTo').val();
|
||||
checkFrom = $('#checkFrom').is(':checked');
|
||||
checkTo = $('#checkTo').is(':checked');
|
||||
search();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
//Today
|
||||
/**
|
||||
* Searches for studies with a study date equal to Today
|
||||
*/
|
||||
'click button#btnToday' :function(event) {
|
||||
studyDateTo = new Date();
|
||||
studyDateFrom = studyDateTo;
|
||||
checkFrom = true;
|
||||
checkTo = true;
|
||||
|
||||
search();
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
//Clear
|
||||
'click button#btnClear' :function(event) {
|
||||
$("#patientId").val("");
|
||||
$("#patientName").val("");
|
||||
$("#patientAccessionNumber").val("");
|
||||
$("#studyDescription").val("");
|
||||
$("#referringPhysician").val("");
|
||||
$("#studyDateFrom").val("");
|
||||
$("#studyDateTo").val("");
|
||||
$("#modality").val("");
|
||||
$('#checkFrom').prop('checked', false);
|
||||
$('#checkTo').prop('checked', false);
|
||||
|
||||
},
|
||||
|
||||
//Last 7 Days
|
||||
'click button#btnLastSevenDays' :function(event) {
|
||||
/**
|
||||
* Searches for studies in the Last 7 Days
|
||||
*/
|
||||
'click button#btnLastSevenDays' :function() {
|
||||
studyDateTo = new Date();
|
||||
studyDateFrom = new Date();
|
||||
studyDateFrom.setDate(studyDateFrom.getDate()-7);
|
||||
checkFrom = true;
|
||||
checkTo = true;
|
||||
|
||||
search();
|
||||
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Clears all input data in the study search panel
|
||||
*/
|
||||
'click button#btnClear' :function(event) {
|
||||
$("#searchPanel .form-control").val("");
|
||||
$('#checkFrom').prop('checked', false);
|
||||
$('#checkTo').prop('checked', false);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
<template name="worklistStudy">
|
||||
<tr class="worklistStudy">
|
||||
<tr class="worklistStudy noselect">
|
||||
<td>
|
||||
{{patientName}}
|
||||
{{formatPN patientName}}
|
||||
</td>
|
||||
<td>
|
||||
{{patientId}}
|
||||
@@ -10,7 +10,7 @@
|
||||
{{accessionNumber}}
|
||||
</td>
|
||||
<td>
|
||||
{{studyDate}}
|
||||
{{formatDA studyDate}}
|
||||
</td>
|
||||
<td>
|
||||
{{modalities}}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
Template.worklistStudy.events({
|
||||
'click': function (){
|
||||
Router.go('viewer', {_id: this.studyInstanceUid});
|
||||
'click': function () {
|
||||
// Use the formatPN template helper to clean up the patient name
|
||||
var title = Blaze._globalHelpers['formatPN'](this.patientName);
|
||||
openNewTab(this.studyInstanceUid, title);
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,7 @@
|
||||
//Generate UUID to create unique tabs
|
||||
/**
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
generateUUID = function() {
|
||||
var d = new Date().getTime();
|
||||
var uuid = 'xxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
@@ -7,4 +10,4 @@ generateUUID = function() {
|
||||
return(c=='x' ? r :(r&0x3|0x8)).toString(8);
|
||||
});
|
||||
return uuid;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
// Create package logger using loglevel
|
||||
// https://atmospherejs.com/spacejamio/loglevel
|
||||
log = loglevel.createPackageLogger('worklist', defaultLevel = 'info');
|
||||
@@ -10,6 +10,17 @@ Package.onUse(function (api) {
|
||||
api.use('standard-app-packages');
|
||||
api.use('jquery');
|
||||
api.use('stylus');
|
||||
api.use('http');
|
||||
|
||||
api.use('practicalmeteor:loglevel');
|
||||
|
||||
// Our custom packages
|
||||
api.use('dicomweb');
|
||||
|
||||
// This sets the default logging level of the package using the
|
||||
// loglevel package. It can be overridden in the JavaScript
|
||||
// console for debugging purposes
|
||||
api.addFiles('log.js', 'client');
|
||||
|
||||
// Components
|
||||
api.addFiles('components/worklist.html', 'client');
|
||||
@@ -21,6 +32,7 @@ Package.onUse(function (api) {
|
||||
api.addFiles('components/tabTitle/tabTitle.styl', 'client');
|
||||
|
||||
api.addFiles('components/tabContent/tabContent.html', 'client');
|
||||
api.addFiles('components/tabContent/tabContent.styl', 'client');
|
||||
|
||||
api.addFiles('components/worklistStudy/worklistStudy.html', 'client');
|
||||
api.addFiles('components/worklistStudy/worklistStudy.js', 'client');
|
||||
@@ -37,8 +49,15 @@ Package.onUse(function (api) {
|
||||
api.addFiles('lib/generateUUID.js', 'client');
|
||||
api.export('generateUUID', 'client');
|
||||
|
||||
// Export Worklist helper functions for usage in Routes
|
||||
api.export('getStudyMetadata', 'client');
|
||||
api.export('openNewTab', 'client');
|
||||
api.export('switchToTab', 'client');
|
||||
|
||||
|
||||
// Export the global ViewerData object
|
||||
api.export('ViewerData', 'client');
|
||||
|
||||
// Export the WorklistTabs Collection
|
||||
api.export('WorklistTabs', 'client');
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user