This commit is contained in:
Weiwei committed 2015-11-16 12:49:55 -06:00
commit 4f1fdfab3e
142 files changed
+6190 -3331

No files matched your search

+3 -3
View File
@@ -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
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;
};
+7
View File
@@ -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');
});
+325 -215
View File
@@ -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,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,3 +1,3 @@
<template name="lesionTableTimepointHeader">
<th>{{name}}</th>
<th>{{timepointName}}</th>
</template>
@@ -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();
}
});
+250
View File
@@ -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);
+17 -3
View File
@@ -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;
}
@@ -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 }}
@@ -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
@@ -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>
+20
View File
@@ -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");
+4 -3
View File
@@ -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;
};
+6
View File
@@ -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;
+4 -6
View File
@@ -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);
}
});
+5 -16
View File
@@ -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('^', ', ');
});
+5 -4
View File
@@ -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);
});
+8
View File
@@ -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) ||
+33 -6
View File
@@ -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
+6
View File
@@ -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;
};
+9 -3
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
// Create package logger using loglevel
// https://atmospherejs.com/spacejamio/loglevel
log = loglevel.createPackageLogger('viewerbase', defaultLevel = 'info');
+14 -9
View File
@@ -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
+2 -2
View File
@@ -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>
+169 -48
View File
@@ -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);
}
});
+5 -2
View File
@@ -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;
};
};
+3
View File
@@ -0,0 +1,3 @@
// Create package logger using loglevel
// https://atmospherejs.com/spacejamio/loglevel
log = loglevel.createPackageLogger('worklist', defaultLevel = 'info');
+20 -1
View File
@@ -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');
});