Updates for Timepoint assocation (LT-60) and Conformance Checks (LT-92, LT-93, LT-94)

This commit is contained in:
Erik Ziegler 2016-01-13 16:17:32 +01:00
parent 6c4eae3699
commit beaa56c0b3
144 changed files with 5050 additions and 2416 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ docs/
.meteor/local .meteor/local
.meteor/meteorite .meteor/meteorite
node_modules node_modules
Packages/active-entry/helloworld/
LesionTracker/tests/nightwatch/reports/

View File

@ -7,6 +7,7 @@
"disallowSpacesInFunctionExpression": { "disallowSpacesInFunctionExpression": {
"beforeOpeningRoundBrace": true "beforeOpeningRoundBrace": true
}, },
"disallowSpacesInsideParentheses": true,
"disallowKeywordsOnNewLine": ["else"], "disallowKeywordsOnNewLine": ["else"],
"disallowNewlineBeforeBlockStatements": true, "disallowNewlineBeforeBlockStatements": true,
"requirePaddingNewLinesAfterUseStrict": true, "requirePaddingNewLinesAfterUseStrict": true,
@ -17,9 +18,12 @@
"requireObjectKeysOnNewLine": true, "requireObjectKeysOnNewLine": true,
"requireSemicolons": true, "requireSemicolons": true,
"requireSpaceAfterBinaryOperators": true, "requireSpaceAfterBinaryOperators": true,
"requireSpaceAfterComma": true,
"requireSpacesInFunctionExpression": {
"beforeOpeningCurlyBrace": true
},
"requireSpaceBeforeObjectValues": true, "requireSpaceBeforeObjectValues": true,
"requireSpacesInsideObjectBrackets": "all", "requireSpacesInsideObjectBrackets": "all",
"requireSpacesInsideArrayBrackets": "all",
"requireLineBreakAfterVariableAssignment": true, "requireLineBreakAfterVariableAssignment": true,
"requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="],
"disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-"], "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-"],

View File

@ -48,3 +48,4 @@ clinical:hipaa-audit-log
clinical:hipaa-logger clinical:hipaa-logger
anti:gagarin@=0.4.11 anti:gagarin@=0.4.11
check check
aldeed:template-extension@4.0.0

View File

@ -1,5 +1,6 @@
accounts-base@1.2.2 accounts-base@1.2.2
accounts-password@1.1.4 accounts-password@1.1.4
aldeed:template-extension@4.0.0
anti:gagarin@0.4.11 anti:gagarin@0.4.11
anti:i18n@0.4.3 anti:i18n@0.4.3
arsnebula:reactive-promise@0.9.1 arsnebula:reactive-promise@0.9.1
@ -38,6 +39,7 @@ ddp-server@1.2.2
deps@1.0.9 deps@1.0.9
dicomweb@0.0.1 dicomweb@0.0.1
diff-sequence@1.0.1 diff-sequence@1.0.1
dimseservice@0.0.1
ecmascript@0.1.6 ecmascript@0.1.6
ecmascript-runtime@0.2.6 ecmascript-runtime@0.2.6
ejson@1.0.7 ejson@1.0.7
@ -90,6 +92,7 @@ reactive-var@1.0.6
reload@1.1.4 reload@1.1.4
retry@1.0.4 retry@1.0.4
routepolicy@1.0.6 routepolicy@1.0.6
rwatts:uuid@0.0.2
service-configuration@1.0.5 service-configuration@1.0.5
session@1.1.1 session@1.1.1
sha@1.0.4 sha@1.0.4

View File

@ -1,30 +1,62 @@
// TODO: Move all of this into the LesionTracker package
body body
background-color: #202020 background-color: #202020
.tab-content { .tab-content
height: calc(100% - 40px); height: calc(100% - 40px)
}
table#tblStudyList thead > tr { table#tblStudyList thead > tr
background-color: #424242; background-color: #424242
color: white; color: white
}
table#tblStudyList tbody > tr:nth-child(even) { table#tblStudyList tbody > tr:nth-child(even)
background-color: #888; background-color: #888
color: black; color: black
}
table#tblStudyList tbody > tr:nth-child(odd) { table#tblStudyList tbody > tr:nth-child(odd)
background-color: #BBB; background-color: #BBB
color: black; color: black
}
table#tblStudyList tbody > tr:hover{ table#tblStudyList tbody > tr:hover
background-color: #009BD2 background-color: #009BD2
color: white color: white
}
table#tblStudyList tbody > tr > td{ table#tblStudyList tbody > tr > td
word-wrap: break-word word-wrap: break-word
}
/* Mobile devices */
@media only screen and (min-device-width : 375px) and (max-device-width : 768px)
.logoContainer
width: 15%
.navbar-brand
img
height: 20px
margin-top: 10px
margin-bottom: 10px
margin-right: 5px
h4.name
padding: 1px
font-size: 0.3em
width: 50%
table#tblStudyList
table-layout: fixed
font-size: 0.7em
#tablist
width: 85%
overflow-x: auto
#toolbar .btn-group button
width: 40px
height: 40px
.studyDateList
margin-top: 5px
@media (max-device-width: 667px)
#toolbar .btn-group button
width: 35px
height: 35px

View File

@ -5,6 +5,7 @@
{{>nonTargetLesionDialog}} {{>nonTargetLesionDialog}}
{{>nonTargetResponseDialog}} {{>nonTargetResponseDialog}}
{{>timepointTextDialog}} {{>timepointTextDialog}}
{{ >conformanceCheckFeedback }}
{{>hidingPanel}} {{>hidingPanel}}
<div id="viewportAndLesionTable"> <div id="viewportAndLesionTable">

View File

@ -3,10 +3,8 @@ Template.viewer.onCreated(function() {
$(window).on('resize', handleResize); $(window).on('resize', handleResize);
var self = this; var self = this;
var firstMeasurementsActivated = false; var firstMeasurementsActivated = false;
var contentId = this.data.contentId;
log.info('viewer onCreated');
OHIF = OHIF || { OHIF = OHIF || {
viewer: {} viewer: {}
@ -16,7 +14,6 @@ Template.viewer.onCreated(function() {
OHIF.viewer.defaultTool = 'wwwc'; OHIF.viewer.defaultTool = 'wwwc';
OHIF.viewer.refLinesEnabled = true; OHIF.viewer.refLinesEnabled = true;
OHIF.viewer.isPlaying = {}; OHIF.viewer.isPlaying = {};
var contentId = this.data.contentId;
OHIF.viewer.functionList = { OHIF.viewer.functionList = {
invert: function(element) { invert: function(element) {
@ -42,15 +39,17 @@ Template.viewer.onCreated(function() {
toggleLesionTrackerTools: toggleLesionTrackerTools, toggleLesionTrackerTools: toggleLesionTrackerTools,
clearTools: clearTools, clearTools: clearTools,
lesion: function() { lesion: function() {
// Used for hotkeys
toolManager.setActiveTool('lesion'); toolManager.setActiveTool('lesion');
}, },
nonTarget: function() { nonTarget: function() {
// Used for hotkeys
toolManager.setActiveTool('nonTarget'); toolManager.setActiveTool('nonTarget');
} }
}; };
// The hotkey can also be an array (e.g. ["NUMPAD0", "0"]) // The hotkey can also be an array (e.g. ["NUMPAD0", "0"])
OHIF.viewer.defaultHotkeys = OHIF.viewer.defaultHotkeys || {};
OHIF.viewer.defaultHotkeys.toggleLesionTrackerTools = 'O'; OHIF.viewer.defaultHotkeys.toggleLesionTrackerTools = 'O';
OHIF.viewer.defaultHotkeys.lesion = 'T'; // Target OHIF.viewer.defaultHotkeys.lesion = 'T'; // Target
OHIF.viewer.defaultHotkeys.nonTarget = 'N'; // Non-target OHIF.viewer.defaultHotkeys.nonTarget = 'N'; // Non-target
@ -65,6 +64,10 @@ Template.viewer.onCreated(function() {
}; };
} }
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
log.info('viewer onCreated');
if (ViewerData[contentId].loadedSeriesData) { if (ViewerData[contentId].loadedSeriesData) {
log.info('Reloading previous loadedSeriesData'); log.info('Reloading previous loadedSeriesData');
OHIF.viewer.loadedSeriesData = ViewerData[contentId].loadedSeriesData; OHIF.viewer.loadedSeriesData = ViewerData[contentId].loadedSeriesData;
@ -76,8 +79,15 @@ Template.viewer.onCreated(function() {
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData; ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
// Update the viewer data object // Update the viewer data object
if (!this.data.timepointIds || this.data.timepointIds.length <= 1) {
// Update the viewer data object
ViewerData[contentId].viewportColumns = 1;
ViewerData[contentId].viewportRows = 1;
} else if (this.data.timepointIds.length > 1) {
ViewerData[contentId].viewportColumns = 2; ViewerData[contentId].viewportColumns = 2;
ViewerData[contentId].viewportRows = 1; ViewerData[contentId].viewportRows = 1;
}
ViewerData[contentId].activeViewport = 0; ViewerData[contentId].activeViewport = 0;
Session.set('ViewerData', ViewerData); Session.set('ViewerData', ViewerData);
} }
@ -96,43 +106,29 @@ Template.viewer.onCreated(function() {
self.autorun(function() { self.autorun(function() {
var patientId = Session.get('patientId'); var patientId = Session.get('patientId');
self.subscribe('timepoints', patientId); self.subscribe('singlePatientTimepoints', patientId);
self.subscribe('measurements', patientId); self.subscribe('singlePatientMeasurements', patientId);
if (self.subscriptionsReady()) { if (self.subscriptionsReady()) {
ViewerStudies.find().observe({ ViewerStudies.find().observe({
added: function(study) { added: function(study) {
// TODO = Replace this whole section when we have a // Find the relevant timepoint given the newly added study
// timepoint-to-study association modal
// First, check if we have a timepoint related to this
// study date already
var timepoint = Timepoints.findOne({ var timepoint = Timepoints.findOne({
timepointName: study.studyDate studyInstanceUids: {
$in: [study.studyInstanceUid]
}
}); });
// If we do, stop here if (!timepoint) {
if (timepoint) { log.warn('Study added to Viewer has not been associated!');
log.warn('A timepoint with that study date already exists!');
return; return;
} }
// Next, check the first timepoint in the collection // Update the added document with its related timepointId
var testTimepoint = Timepoints.findOne({}); ViewerStudies.update(study._id, {
$set: {
// If it relates to another subject, we need to stop here as well timepointId: timepoint.timepointId
// because the tab may be changing.
if (testTimepoint && testTimepoint.patientId !== study.patientId) {
log.warn('Timepoints collection related to the wrong subject');
return;
} }
// Otherwise, we need to add a timepoint related to this study date
log.info('Inserting a new timepoint');
Timepoints.insert({
patientId: study.patientId,
timepointName: study.studyDate,
timepointID: uuid.v4()
}); });
} }
}); });
@ -159,12 +155,17 @@ Template.viewer.onCreated(function() {
log.info('Measurement added'); log.info('Measurement added');
syncMeasurementAndToolData(data); syncMeasurementAndToolData(data);
updateRelatedElements(data.imageId); updateRelatedElements(data.imageId);
TrialResponseCriteria.validateAll();
},
changed: function(data) {
TrialResponseCriteria.validateAllDelayed();
}, },
removed: function(data) { removed: function(data) {
log.info('Measurement removed'); log.info('Measurement removed');
// Check that this Measurement actually contains timepoint data // Check that this Measurement actually contains timepoint data
if (!data.timepoints) { if (!data || !data.timepoints) {
return; return;
} }
@ -175,9 +176,9 @@ Template.viewer.onCreated(function() {
// Remove the measurement from all the imageIds on which it exists // Remove the measurement from all the imageIds on which it exists
// as toolData // as toolData
Object.keys(data.timepoints).forEach(function(timepointID) { Object.keys(data.timepoints).forEach(function(timepointId) {
// Clear the toolData for this timepoint // Clear the toolData for this timepoint
var imageId = data.timepoints[timepointID].imageId; var imageId = data.timepoints[timepointId].imageId;
removeToolDataWithMeasurementId(imageId, toolType, measurementId); removeToolDataWithMeasurementId(imageId, toolType, measurementId);
}); });
@ -202,146 +203,29 @@ Template.viewer.onCreated(function() {
viewports.each(function(index, element) { viewports.each(function(index, element) {
cornerstone.updateImage(element); cornerstone.updateImage(element);
}); });
}
}); ValidationErrors.remove({
} measurementId: data._id
}); });
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer); TrialResponseCriteria.validateAll();
}
});
}
});
}); });
function updateRelatedElements(imageId) {
// Get all on-screen elements with this imageId
var enabledElements = cornerstone.getEnabledElementsByImageId(imageId);
// TODO=Check original event to prevent duplicate updateImage calls
// Loop through these elements
enabledElements.forEach(function(enabledElement) {
// Update the display so the tool is removed
var element = enabledElement.element;
cornerstone.updateImage(element);
});
}
function syncMeasurementAndToolData(data) {
// Check what toolType we should be adding this to, based on the isTarget value
// of the stored Measurement
var toolType = data.isTarget ? 'lesion' : 'nonTarget';
var toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.toolState;
// Loop through the timepoint data for this measurement
Object.keys(data.timepoints).forEach(function(key) {
var storedData = data.timepoints[key];
var imageId = storedData.imageId;
if (!toolState[imageId]) {
toolState[imageId] = {};
}
// This is probably not the best approach to prevent duplicates
if (toolState[imageId][toolType] && toolState[imageId][toolType].data) {
var measurementHasNoIdYet = false;
toolState[imageId][toolType].data.forEach(function(measurement) {
if (measurement.id === 'notready') {
measurementHasNoIdYet = true;
return false;
}
});
// Stop here if it appears that we are creating this measurement right now,
// and would not like this function to add another copy of it to the toolData
if (measurementHasNoIdYet === true) {
return;
}
}
if (!toolState[imageId][toolType]) {
toolState[imageId][toolType] = {
data: []
};
} else {
var alreadyExists = false;
if (toolState[imageId][toolType].data.length) {
toolState[imageId][toolType].data.forEach(function(measurement) {
if (measurement.id === data._id) {
alreadyExists = true;
// Update the toolData lesionNumber from the Measurement
measurement.lesionNumber = data.lesionNumber;
return false;
}
});
}
if (alreadyExists === true) {
return;
}
}
// Create measurementData structure based on the lesion data at this timepoint
// We will add this into the toolData for this imageId
var measurementData = storedData;
measurementData.isTarget = data.isTarget;
measurementData.lesionNumber = data.lesionNumber;
measurementData.measurementText = data.measurementText;
measurementData.isDeleted = data.isDeleted;
measurementData.location = data.location;
measurementData.locationUID = data.locationUID;
measurementData.patientId = patientId;
measurementData.visible = data.visible;
measurementData.active = data.active;
measurementData.uid = data.uid;
measurementData.id = data._id;
toolState[imageId][toolType].data.push(measurementData);
});
}
Template.viewer.onRendered(function() { Template.viewer.onRendered(function() {
// Enable hotkeys // Enable hotkeys
enableHotkeys(); enableHotkeys();
// Set lesion tool buttons as disable if pixel spacing is not available for active element // Set lesion tool buttons as disabled if pixel spacing is not available for active element
this.autorun(function(){ this.autorun(pixelSpacingAutorunCheck);
if (!Session.get('ViewerData') || Session.get('activeViewport') === undefined) {
return;
}
var activeViewportIndex = Session.get('activeViewport');
var viewports = $(".imageViewerViewport");
var element = viewports.get(activeViewportIndex);
// Check element has .empty class
if (element.classList.contains('empty')) {
setLesionToolButtonsDisable(true);
return;
}
try {
var enabledElement = cornerstone.getEnabledElement(element);
if (!enabledElement || !enabledElement.image ||
!enabledElement.image.rowPixelSpacing ||
!enabledElement.image.columnPixelSpacing) {
// Disable Lesion Tools and Buttons
toolManager.setActiveTool("wwwc", viewports);
cornerstoneTools.lesion.disable(element);
cornerstoneTools.nonTarget.disable(element);
setLesionToolButtonsDisable(true);
} else{
// Enable Lesion Buttons
setLesionToolButtonsDisable(false);
}
} catch(error) {
return;
}
});
}); });
Template.viewer.onDestroyed(function() { Template.viewer.onDestroyed(function() {
log.info('onDestroyed'); log.info('onDestroyed');
console.log("viewer destroyed!"); console.log('viewer destroyed!');
// Remove the Window resize listener // Remove the Window resize listener
$(window).off('resize', handleResize); $(window).off('resize', handleResize);
@ -350,6 +234,9 @@ Template.viewer.onDestroyed(function() {
}); });
Template.viewer.events({ Template.viewer.events({
'CornerstoneToolsMeasurementAdded .imageViewerViewport': function(e, template, eventData) {
handleMeasurementAdded(e, eventData);
},
'CornerstoneToolsMeasurementModified .imageViewerViewport': function(e, template, eventData) { 'CornerstoneToolsMeasurementModified .imageViewerViewport': function(e, template, eventData) {
handleMeasurementModified(e, eventData); handleMeasurementModified(e, eventData);
}, },
@ -357,43 +244,3 @@ Template.viewer.events({
handleMeasurementRemoved(e, eventData); handleMeasurementRemoved(e, eventData);
} }
}); });
function handleMeasurementModified(e, eventData) {
log.info('CornerstoneToolsMeasurementModified');
var measurementData = eventData.measurementData;
switch (eventData.toolType) {
case 'nonTarget':
case 'lesion':
LesionManager.updateLesionData(measurementData);
break;
}
}
function handleMeasurementRemoved(e, eventData) {
log.info('CornerstoneToolsMeasurementRemoved');
var measurementData = eventData.measurementData;
switch (eventData.toolType) {
case 'nonTarget':
case 'lesion':
var measurement = Measurements.findOne(measurementData.id, {
reactive: false
});
if (!measurement) {
return;
}
clearMeasurementTimepointData(measurement._id, measurementData.timepointID);
break;
}
}
// Set enablement of Lesion Tools Buttons
function setLesionToolButtonsDisable (status) {
var buttons = [$("button#lesion"), $("button#nonTarget")];
buttons.forEach(function(btn){
btn.prop("disabled", status);
});
}

View File

@ -1,51 +0,0 @@
/* Mobile devices */
@media only screen and (min-device-width : 375px) and (max-device-width : 768px){
.logoContainer {
width: 15%;
}
.navbar-brand img {
height: 20px;
margin-top: 10px;
margin-bottom: 10px;
margin-right: 5px;
}
.navbar-brand h4.name {
padding: 1px;
font-size: 0.3em;
width: 50%;;
}
table#tblStudyList {
table-layout: fixed;
font-size: 0.7em;
}
#tablist {
width: 85%;
overflow-x: auto;
}
#toolbar .btn-group button {
width: 40px;
height: 40px;
}
.studyDateList {
margin-top: 5px;
}
}
@media (max-device-width: 667px) {
#toolbar .btn-group button {
width: 35px;
height: 35px;
}
}

View File

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

View File

@ -11,15 +11,17 @@ Object.keys(ViewerData).forEach(function(contentId) {
}); });
Router.configure({ Router.configure({
layoutTemplate: 'layoutLesionTracker', layoutTemplate: 'lesionTrackerLayout',
loadingTemplate: 'layoutLesionTracker' loadingTemplate: 'lesionTrackerLayout',
notFoundTemplate: 'notFound'
}); });
Router.onBeforeAction('loading'); Router.onBeforeAction('loading');
var data = { var data = {
additionalTemplates: [ additionalTemplates: [
'associationModal' 'associationModal',
'optionsModal'
] ]
}; };
@ -35,24 +37,22 @@ Router.route('/worklist', function() {
this.render('worklist', routerOptions); this.render('worklist', routerOptions);
}); });
Router.route('/viewer/:_id', { Router.route('/viewer/timepoints/:_id', {
layoutTemplate: 'layoutLesionTracker', layoutTemplate: 'lesionTrackerLayout',
name: 'viewer', name: 'viewer',
onBeforeAction: function() { onBeforeAction: function() {
log.info('Router GetStudyMetadata'); var timepointId = this.params._id;
var studyInstanceUid = this.params._id;
// Check if this study is already loaded in a tab // Check if this study is already loaded in a tab
// If it is, stop here so we don't keep adding tabs on hot-code reloads // If it is, stop here so we don't keep adding tabs on hot-code reloads
var tab = WorklistTabs.find({ var tab = WorklistTabs.findOne({
studyInstanceUid: studyInstanceUid timepointId: timepointId
}).fetch(); });
if (tab) { if (tab) {
return; return;
} }
this.render('worklist', routerOptions); this.render('worklist', routerOptions);
openNewTab(studyInstanceUid); openNewTabWithTimepoint(timepointId);
} }
}); });

View File

@ -1,55 +0,0 @@
/*****************************/
/* Toggle */
/*****************************/
.dockingContainer, .collapseVertical, .collapseHorizontal {
-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;
}
.dockingContainer {
-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;
}
.collapseVertical {
opacity: 0;
height: 0;
border: none;
}
.collapseHorizontal {
width: 30px;
}
.btnCollapse{
position: absolute;
top: 0;
left: 0;
display: block;
width: 25px;
height: 25px;
line-height: 25px;
padding: 0;
border: 0;
border-top: 1px solid #e6e6e6;
background: #f6f6f6;
color: #666;
font-size: 0.875rem;
text-align: center;
cursor: pointer;
z-index: 100;
}
.btnCollapseIcon-collapse{
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}

View File

@ -1,15 +1,7 @@
Meteor.startup(function() { Meteor.startup(function() {
if (Meteor.settings.dicomWeb) {
console.log('dicomWeb settings defined!');
console.log(Meteor.settings);
return;
}
console.log('Using default LesionTracker dicomWeb settings');
Meteor.settings = { Meteor.settings = {
dicomWeb: { dicomWeb: {
endpoints: [ endpoints: [{
{
name: 'Orthanc', name: 'Orthanc',
wadoUriRootNOTE: 'either this uri is not correct for wado-uri or wado-uri is not configured on orthanc currently', wadoUriRootNOTE: 'either this uri is not correct for wado-uri or wado-uri is not configured on orthanc currently',
wadoUriRoot: 'http://localhost:8043/wado', wadoUriRoot: 'http://localhost:8043/wado',
@ -23,8 +15,16 @@ Meteor.startup(function() {
logResponses: false, logResponses: false,
logTiming: true logTiming: true
} }
} }]
] },
} dimse: {
host: 'localhost',
port: 4242,
hostAE: 'ORTHANC'
},
defaultServiceType: 'dicomWeb'
//defaultServiceType: 'dimse'
}; };
console.log('Using default LesionTracker settings with service: ' + Meteor.settings.defaultServiceType);
}); });

View File

@ -77,6 +77,7 @@ reactive-var@1.0.6
reload@1.1.4 reload@1.1.4
retry@1.0.4 retry@1.0.4
routepolicy@1.0.6 routepolicy@1.0.6
rwatts:uuid@0.0.2
service-configuration@1.0.5 service-configuration@1.0.5
session@1.1.1 session@1.1.1
sha@1.0.4 sha@1.0.4

File diff suppressed because it is too large Load Diff

View File

@ -1,77 +0,0 @@
cornerstoneWADORSImageLoader = {
internal : {
nextIndex: 0,
imageIds : []
}
};
cornerstoneWADORSImageLoader.addImage = function(image) {
var index = cornerstoneWADORSImageLoader.internal.nextIndex++;
cornerstoneWADORSImageLoader.internal.imageIds[index] = image;
var imageId = 'wadors:' + index;
return imageId;
};
function getMinMax(storedPixelData)
{
// we always calculate the min max values since they are not always
// present in DICOM and we don't want to trust them anyway as cornerstone
// depends on us providing reliable values for these
var min = 65535;
var max = -32768;
var numPixels = storedPixelData.length;
var pixelData = storedPixelData;
for(var index = 0; index < numPixels; index++) {
var spv = pixelData[index];
// TODO: test to see if it is faster to use conditional here rather than calling min/max functions
min = Math.min(min, spv);
max = Math.max(max, spv);
}
return {
min: min,
max: max
};
}
cornerstone.registerImageLoader('wadors', function(imageId) {
var index = imageId.substring(7);
var image = cornerstoneWADORSImageLoader.internal.imageIds[index];
var deferred = $.Deferred();
var mediaType;// = 'image/dicom+jp2';
DICOMWeb.getImageFrame(image.uri, mediaType).then(function(result) {
//console.log(result);
// TODO: add support for retrieving compressed pixel data
var storedPixelData;
if(image.instance.bitsAllocated === 16) {
if(image.instance.pixelRepresentation === 0) {
storedPixelData = new Uint16Array(result.arrayBuffer, result.offset, result.length / 2);
} else {
storedPixelData = new Int16Array(result.arrayBuffer, result.offset, result.length / 2);
}
} else if(image.instance.bitsAllocated === 8) {
storedPixelData = new Uint8Array(result.arrayBuffer, result.offset, result.length);
}
// TODO: handle various color space conversions
var minMax = getMinMax(storedPixelData);
image.imageId = imageId;
image.minPixelValue = minMax.min;
image.maxPixelValue = minMax.max;
image.render = cornerstone.renderGrayscaleImage;
image.getPixelData = function() {
return storedPixelData;
};
//console.log(image);
deferred.resolve(image);
}).catch(function(reason) {
deferred.reject(reason);
});
return deferred;
});

View File

@ -1,4 +1,4 @@
/*! dicom-parser - v1.2.0 - 2015-11-02 | (c) 2014 Chris Hafey | https://github.com/chafey/dicomParser */ /*! dicom-parser - v1.2.1 - 2016-02-07 | (c) 2014 Chris Hafey | https://github.com/chafey/dicomParser */
(function (root, factory) { (function (root, factory) {
// node.js // node.js
@ -66,6 +66,7 @@ var dicomParser = (function(dicomParser) {
function getDataSetByteStream(transferSyntax, position) { function getDataSetByteStream(transferSyntax, position) {
if(transferSyntax === '1.2.840.10008.1.2.1.99') if(transferSyntax === '1.2.840.10008.1.2.1.99')
{ {
// https://github.com/nodeca/pako
if(typeof(pako) === "undefined") { if(typeof(pako) === "undefined") {
throw 'dicomParser.parseDicom: deflated transfer syntax encountered but pako not loaded'; throw 'dicomParser.parseDicom: deflated transfer syntax encountered but pako not loaded';
} }
@ -936,8 +937,9 @@ var dicomParser = (function (dicomParser)
{ {
throw "dicomParser.ByteStream: missing required parameter 'byteArray'"; throw "dicomParser.ByteStream: missing required parameter 'byteArray'";
} }
if((byteArray instanceof Uint8Array) === false) { if((byteArray instanceof Uint8Array) === false &&
throw 'dicomParser.ByteStream: parameter byteArray is not of type Uint8Array'; (byteArray instanceof Buffer) === false ) {
throw 'dicomParser.ByteStream: parameter byteArray is not of type Uint8Array or Buffer';
} }
if(position < 0) if(position < 0)
{ {
@ -2253,7 +2255,7 @@ var dicomParser = (function (dicomParser)
dicomParser = {}; dicomParser = {};
} }
dicomParser.version = "1.2.0"; dicomParser.version = "1.2.1";
return dicomParser; return dicomParser;
}(dicomParser)); }(dicomParser));

View File

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

View File

@ -1,52 +0,0 @@
/**
* 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;
};

View File

@ -1,18 +0,0 @@
/**
* 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;
};

View File

@ -9,16 +9,11 @@ Package.onUse(function(api) {
// DICOMWeb API functions // DICOMWeb API functions
api.addFiles('server/namespace.js', 'server'); api.addFiles('server/namespace.js', 'server');
api.addFiles('server/getImageFrame.js', 'server');
api.addFiles('server/getJSON.js', 'server'); api.addFiles('server/getJSON.js', 'server');
api.addFiles('server/getName.js', 'server'); api.addFiles('server/getName.js', 'server');
api.addFiles('server/getNumber.js', 'server'); api.addFiles('server/getNumber.js', 'server');
api.addFiles('server/getString.js', 'server'); api.addFiles('server/getString.js', 'server');
// Helper functions api.export('DICOMWeb', ['client', 'server']);
api.addFiles('lib/findIndexOfString.js', 'server');
api.addFiles('lib/uint8ArrayToString.js', 'server');
api.export('DICOMWeb', 'server');
}); });

View File

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

View File

@ -2,7 +2,7 @@ DICOMWeb.getJSON = function(url, options) {
var getOptions = { var getOptions = {
headers: { headers: {
Accept: 'application/json' Accept: 'application/json'
}, }
}; };
if (options.auth) { if (options.auth) {

View File

@ -1,6 +0,0 @@
// The list of the known DICOM modalities
// If you are running Orthanc with Docker, you need to put the contents of this file in
// /etc/orthanc/orthanc.json
"DicomModalities" : {
"OHIFDCM" : [ "OHIFDCM", "localhost", 3000 ]
},

View File

@ -0,0 +1,5 @@
// The list of the known DICOM modalities
// If you are running Orthanc , you need to put the contents of this file in its Configuration file
"DicomModalities" : {
"OHIFDCM" : [ "OHIFDCM", "localhost", 3000 ]
},

View File

@ -8,11 +8,16 @@ DIMSE.associate = function(contexts, callback) {
port = Meteor.settings.dimse.port, port = Meteor.settings.dimse.port,
ae = Meteor.settings.dimse.hostAE; ae = Meteor.settings.dimse.hostAE;
console.log("Associating via DIMSE");
console.log(Meteor.settings.dimse);
var client = net.connect({ var client = net.connect({
host: host, host: host,
port: port port: port
}, });
function() { //'connect' listener
client.on('connect', function() {
//'connect' listener
console.log('==Connected'); console.log('==Connected');
var conn = new Connection(client, { var conn = new Connection(client, {
@ -20,15 +25,21 @@ DIMSE.associate = function(contexts, callback) {
split: false split: false
} }
}); });
conn.associate({ conn.associate({
contexts: contexts, contexts: contexts,
hostAE: ae hostAE: ae
}, function(pdu) { }, function(pdu) {
// associated // associated
console.log('==Associated'); console.log('==Associated');
callback.call(conn, pdu); callback.call(conn, pdu);
}); });
}); });
client.on('error', function(error) {
throw error;
});
}; };
DIMSE.retrievePatients = function(params) { DIMSE.retrievePatients = function(params) {

View File

@ -1,2 +1,5 @@
Timepoints = new Meteor.Collection('timepoints'); Timepoints = new Meteor.Collection('timepoints');
Studies = new Meteor.Collection('studies');
Measurements = new Meteor.Collection('measurements'); Measurements = new Meteor.Collection('measurements');
WorklistSubscriptions = ['studies', 'timepoints'];

View File

@ -6,7 +6,8 @@ LesionLocations.insert({
location: 'Liver Left', location: 'Liver Left',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -15,7 +16,8 @@ LesionLocations.insert({
location: 'Liver Right', location: 'Liver Right',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -24,7 +26,8 @@ LesionLocations.insert({
location: 'Liver Caudate', location: 'Liver Caudate',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -33,7 +36,8 @@ LesionLocations.insert({
location: 'Lung LLL', location: 'Lung LLL',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -42,7 +46,8 @@ LesionLocations.insert({
location: 'Lung LUL', location: 'Lung LUL',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -51,7 +56,8 @@ LesionLocations.insert({
location: 'Lung RLL', location: 'Lung RLL',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -60,7 +66,8 @@ LesionLocations.insert({
location: 'Lung RML', location: 'Lung RML',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -69,7 +76,8 @@ LesionLocations.insert({
location: 'Lung RUL', location: 'Lung RUL',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -78,7 +86,8 @@ LesionLocations.insert({
location: 'Pleura Left', location: 'Pleura Left',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -87,7 +96,8 @@ LesionLocations.insert({
location: 'Pleura Right', location: 'Pleura Right',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -96,7 +106,8 @@ LesionLocations.insert({
location: 'Kidney Left', location: 'Kidney Left',
hasDescription: false, hasDescription: false,
description: '', description: '',
selected: false selected: false,
isNodal: false
}); });
LesionLocations.insert({ LesionLocations.insert({
@ -104,5 +115,7 @@ LesionLocations.insert({
group: 'kidney', group: 'kidney',
location: 'Kidney Right', location: 'Kidney Right',
hasDescription: false, hasDescription: false,
description: '' description: '',
selected: false,
isNodal: false
}); });

View File

@ -0,0 +1 @@
PatientLocations = new Meteor.Collection(null);

View File

@ -1,190 +0,0 @@
var LesionManager = (function() {
PatientLocations = new Meteor.Collection(null);
/**
* Retrieve a location name (e.g. Liver Right) from the
* PatientLocations Collection by id, if it exists. Otherwise,
* return an empty string.
*
* @param id
* @returns {*|string}
*/
function getLocationName(id) {
var locationObject = PatientLocations.findOne(id);
if (!locationObject || !locationObject.location) {
return '';
}
return locationObject.location;
}
/**
* Update the Timepoint object for a specific Measurement.
* If no measurement exists yet, one will be created.
*
* Input is toolData from the lesion or nonTarget tool
*
* @param lesionData
*/
function updateLesionData(lesionData) {
// Find the related Timepoint from the Timepoints Collection
var timepointID = lesionData.timepointID;
var timepoint = Timepoints.findOne({
timepointID: timepointID
});
if (!timepoint) {
log.warn('Timepoint in an image is not present in the Timepoints Collection?');
return;
}
// Find the specific lesion to be updated
var existingMeasurement;
if (lesionData.id && lesionData.id !== 'notready') {
existingMeasurement = Measurements.findOne(lesionData.id);
} else {
existingMeasurement = Measurements.findOne({
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget
});
}
// Create a structure for the timepointData based
// on this Lesion's toolData
var timepointData = {
seriesInstanceUid: lesionData.seriesInstanceUid,
studyInstanceUid: lesionData.studyInstanceUid,
handles: lesionData.handles,
imageId: lesionData.imageId
};
if (lesionData.isTarget === true) {
timepointData.shortestDiameter = lesionData.widthMeasurement;
timepointData.longestDiameter = lesionData.measurementText;
} else {
timepointData.response = lesionData.response;
}
// If no such lesion exists, we need to add one
if (!existingMeasurement) {
// Create a data structure for the Measurement
// based on the current tool data
var measurement = {
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget,
patientId: lesionData.patientId,
id: lesionData.id
};
// Retrieve the location name given the locationUID
if (lesionData.locationUID !== undefined) {
measurement.location = getLocationName(lesionData.locationUID);
}
// Add toolData parameters to the Measurement at this Timepoint
measurement.timepoints = {};
measurement.timepoints[timepointID] = timepointData;
// Set a flag to prevent duplication of toolData
measurement.toolDataInsertedManually = true;
// Increment and store the absolute Lesion Number for this Measurement
measurement.lesionNumberAbsolute = Measurements.find().count() + 1;
// Insert this into the Measurements Collection
// Save the ID into the toolData (not sure if this works?)
measurement.id = Measurements.insert(measurement);
// Update the database entry so it can be readded next time the study is loaded
Measurements.update(measurement.id, {
$set: {
toolDataInsertedManually: false
}
});
} else {
lesionData.id = existingMeasurement._id;
// Update timepoints from lesion data
existingMeasurement.timepoints[timepointID] = timepointData;
Measurements.update(existingMeasurement._id, {
$set: {
timepoints: existingMeasurement.timepoints
}
});
}
}
/**
* Returns new lesion number according to timepointID
* @param timepointID
* @param isTarget
* @returns {*}
*/
function getNewLesionNumber(timepointID, isTarget) {
// Get all current lesion measurements
var numMeasurements = Measurements.find({
isTarget: isTarget
}).count();
// If no measurements exist yet, start at 1
if (!numMeasurements) {
return 1;
}
// Find related measurements (i.e. target or non-target)
var measurements = Measurements.find({
isTarget: isTarget
}, {
sort: {
lesionNumber: 1
}
}).fetch();
// If measurements exist, find the last lesion number
// from the given timepoint
var lesionNumberCounter = 1;
// Search through every Measurement to see which ones
// already have data for this Timepoint, if we find one that
// doesn't have data, we will stop there and use that as the
// current Measurement
measurements.every(function(measurement) {
// If this measurement has no data for this Timepoint,
// use this as the current Measurement
if (!measurement.timepoints[timepointID]) {
lesionNumberCounter = measurement.lesionNumber;
return false;
}
lesionNumberCounter++;
return true;
});
return lesionNumberCounter;
}
/**
* If the current Lesion Number already exists
* for any other timepoint, returns lesion locationUID
* @param lesionData
* @returns {*}
*/
function lesionNumberExists(lesionData) {
var measurement = Measurements.findOne({
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget
});
if (!measurement) {
return;
}
return measurement.locationUID;
}
return {
updateLesionData: updateLesionData,
getNewLesionNumber: getNewLesionNumber,
lesionNumberExists: lesionNumberExists,
getLocationName: getLocationName
};
})();

View File

@ -50,6 +50,11 @@
}; };
var config = cornerstoneTools.lesion.getConfiguration(); var config = cornerstoneTools.lesion.getConfiguration();
// Set lesion number and lesion name
if (measurementData.lesionNumber === undefined) {
config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
}
// associate this data with this imageId so we can render it and manipulate it // associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(element, toolType, measurementData); cornerstoneTools.addToolState(element, toolType, measurementData);
@ -76,11 +81,6 @@
// Bind a one-time event listener for the Esc key // Bind a one-time event listener for the Esc key
$(element).one('keydown', cancelCallback); $(element).one('keydown', cancelCallback);
// Set lesion number and lesion name
if (measurementData.lesionNumber === undefined) {
config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
}
cornerstone.updateImage(element); cornerstone.updateImage(element);
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() { cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() {
@ -217,8 +217,7 @@
widthMeasurement: 0, widthMeasurement: 0,
perpendicularMeasurement: 0, perpendicularMeasurement: 0,
isDeleted: false, isDeleted: false,
isTarget: true, isTarget: true
uid: uuid.v4()
}; };
return measurementData; return measurementData;
} }

View File

@ -51,6 +51,13 @@
mouseButtonMask: mouseEventData.which mouseButtonMask: mouseEventData.which
}; };
var config = cornerstoneTools.nonTarget.getConfiguration();
// Set lesion number and lesion name
if (measurementData.lesionNumber === undefined) {
config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
}
// associate this data with this imageId so we can render it and manipulate it // associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(mouseEventData.element, toolType, measurementData); cornerstoneTools.addToolState(mouseEventData.element, toolType, measurementData);
@ -77,13 +84,6 @@
// Bind a one-time event listener for the Esc key // Bind a one-time event listener for the Esc key
$(element).one('keydown', cancelCallback); $(element).one('keydown', cancelCallback);
var config = cornerstoneTools.nonTarget.getConfiguration();
// Set lesion number and lesion name
if (measurementData.lesionNumber === undefined) {
config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
}
cornerstone.updateImage(element); cornerstone.updateImage(element);
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() { cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() {
@ -101,6 +101,8 @@
$(element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.nonTarget.mouseDownCallback); $(element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.nonTarget.mouseDownCallback);
$(element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.nonTarget.mouseDownActivateCallback); $(element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.nonTarget.mouseDownActivateCallback);
$(element).off('keydown', cancelCallback);
cornerstone.updateImage(mouseEventData.element); cornerstone.updateImage(mouseEventData.element);
}); });
} }

View File

@ -50,11 +50,10 @@
x: config.horizontalLine.start.x + i * config.horizontalMinorTick, x: config.horizontalLine.start.x + i * config.horizontalMinorTick,
y: 0 y: 0
}; };
if (i% 5 === 0) {
if (i% 5 === 0) {
endPoint.y = config.horizontalLine.start.y - config.majorTickLength; endPoint.y = config.horizontalLine.start.y - config.majorTickLength;
} else { } else {
endPoint.y = config.horizontalLine.start.y - config.minorTickLength; endPoint.y = config.horizontalLine.start.y - config.minorTickLength;
} }

File diff suppressed because it is too large Load Diff

View File

@ -10,8 +10,8 @@
{{ >studyAssociationTable }} {{ >studyAssociationTable }}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-primary" id="saveAssociations" data-dismiss="modal" data-toggle="modal" data-target="#associationModal">Save</button> <button type="button" class="btn btn-primary" id="saveAssociations">Save</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-dismiss="modal" id='cancelAssociation'>Cancel</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,116 @@
Template.associationModal.events({ Template.associationModal.events({
'click #saveAssociations': function(e) { 'click #saveAssociations': function(e) {
log.info("Saving associations"); log.info('Saving associations');
// Close the modal
var saveButton = $(e.currentTarget);
saveButton.attr('disabled', true);
saveButton.addClass('btn-success').removeClass('btn-primary');
// Find the rows of the study association table
var tableRows = $('#studyAssociationTable table tbody tr');
// Create an empty object to group studies into
var studies = {};
// Loop through each row to parse the data
tableRows.each(function() {
// Get a selector for this row
var row = $(this);
// Check the includeStudy checkbox to see if we should parse this row
var includeStudy = row.find('input.includeStudy[type="checkbox"]').eq(0).prop('checked');
if (!includeStudy) {
return;
}
// Find the selected timepoint option for this study
var timepointInput = row.find('input.timepointOption[type="radio"]:checked');
// Find the related label and trim it down to actual label (TODO: do this another way)
var timepointType = timepointInput.val();
// Get the study metaData by checking the row with the template engine Blaze
var data = Blaze.getData(this);
// Concatenate the study data to an array, depending on whether is was marked as baseline
// or follow-up
if (!studies.hasOwnProperty(timepointType)) {
studies[timepointType] = [];
}
studies[timepointType].push(data);
});
Object.keys(studies).forEach(function(timepointType) {
// Get the studies associated with this timepoint
var relatedStudies = studies[timepointType];
// Create an array of all the studyInstanceUids for storage in the Timepoint
var studyInstanceUids = relatedStudies.map(function(study) {
return study.studyInstanceUid;
});
// Create an array of all the studyDates for storage in the Timepoint
var studyDates = relatedStudies.map(function(study) {
return moment(study.studyDate, 'YYYYMMDD');
});
// Sort the study dates, so we can get a range for these values
studyDates = studyDates.sort();
// Create a new timepoint to represent the (baseline or follow-up) studies
var timepoint = {
timepointType: timepointType,
timepointId: uuid.new(),
studyInstanceUids: studyInstanceUids,
patientId: relatedStudies[0].patientId, // TODO: Revisit this (Should timepoints be related to patientId?)
earliestDate: studyDates[0].format('YYYYMMDD'),
latestDate: studyDates[studyDates.length - 1].format('YYYYMMDD')
};
// Insert this timepoint into the Timepoints Collection
Timepoints.insert(timepoint);
// Loop through these studies to associate them with the newly created timepoint
relatedStudies.forEach(function(study) {
// Check if a study already exists in the Studies collection
var existingStudy = Studies.findOne({
studyInstanceUid: study.studyInstanceUid
});
if (existingStudy) {
// If a study already exists, update the entry with the new timepointId
Studies.update(existingStudy._id, {
$set: {
timepointId: timepoint.timepointId
}
});
} else {
// If no such study exists, update the entry with the new timepointId
// Clear the ID from the document
delete study._id;
// Attach the timepointId and insert it into the Studies Collection
study.timepointId = timepoint.timepointId;
Studies.insert(study);
}
});
});
// Hide the modal
$('#associationModal').modal('hide');
// Reset the save button to its normal state
saveButton.removeClass('btn-success').addClass('btn-primary');
saveButton.attr('disabled', false);
},
'click #cancelAssociation': function() {
// When the modal is closed, we should reset
// the save button to its normal state
var saveButton = $('#saveAssociations');
saveButton.removeClass('btn-success').addClass('btn-primary');
saveButton.attr('disabled', false);
} }
}); });

View File

@ -0,0 +1,7 @@
<template name="conformanceCheckFeedback">
<div id="conformanceCheckFeedback">
{{ #each validationErrors }}
<p data-toggle=""><i class="fa fa-exclamation-triangle fa-lg"></i> {{prefix}}{{error}}</p>
{{ /each }}
</div>
</template>

View File

@ -0,0 +1,11 @@
Template.conformanceCheckFeedback.helpers({
validationErrors: function() {
// Return validation errors sorted by last added Target
return ValidationErrors.find({}, {
sort: {
prefix: -1,
type: 1
}
});
}
});

View File

@ -0,0 +1,19 @@
#conformanceCheckFeedback
position: absolute
right: 10px
color: darkorange
cursor: pointer
background: black
width: 30%
height: 30px
overflow: hidden
p
margin: 5px
text-align: center
&:hover
overflow: auto
height: 200px
z-index: 200

View File

@ -11,24 +11,29 @@ function closeHandler(dialog) {
// This event sets lesion number for new lesion // This event sets lesion number for new lesion
function setLesionNumberCallback(measurementData, eventData, doneCallback) { function setLesionNumberCallback(measurementData, eventData, doneCallback) {
// Get the current element's timepointID from the study date metadata // Get the current element's timepointId from the study date metadata
var element = eventData.element; var element = eventData.element;
var enabledElement = cornerstone.getEnabledElement(element); var enabledElement = cornerstone.getEnabledElement(element);
var imageId = enabledElement.image.imageId; var imageId = enabledElement.image.imageId;
var study = cornerstoneTools.metaData.get('study', imageId); var study = cornerstoneTools.metaData.get('study', imageId);
// Find the relevant timepoint given the current study
var timepoint = Timepoints.findOne({ var timepoint = Timepoints.findOne({
timepointName: study.studyDate studyInstanceUids: {
$in: [study.studyInstanceUid]
}
}); });
if (!timepoint) { if (!timepoint) {
return; return;
} }
measurementData.timepointID = timepoint.timepointID; measurementData.timepointId = timepoint.timepointId;
// Get a lesion number for this lesion, depending on whether or not the same lesion previously // Get a lesion number for this lesion, depending on whether or not the same lesion previously
// exists at a different timepoint // exists at a different timepoint
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = true); var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = true);
measurementData.lesionNumber = lesionNumber; measurementData.lesionNumber = lesionNumber;
// Set lesion number // Set lesion number
@ -226,6 +231,9 @@ Template.lesionLocationDialog.events({
/// Set the isTarget value to true, since this is the target-lesion dialog callback /// Set the isTarget value to true, since this is the target-lesion dialog callback
measurementData.isTarget = true; measurementData.isTarget = true;
// Set the isNodal value based on the Location's properties
measurementData.isNodal = locationObj.isNodal;
// Adds lesion data to timepoints array // Adds lesion data to timepoints array
LesionManager.updateLesionData(measurementData); LesionManager.updateLesionData(measurementData);
} else { } else {
@ -233,6 +241,7 @@ Template.lesionLocationDialog.events({
$set: { $set: {
location: locationObj.location, location: locationObj.location,
locationId: locationObj.id, locationId: locationObj.id,
isNodal: locationObj.isNodal,
locationUID: id locationUID: id
} }
}); });

View File

@ -5,18 +5,18 @@ Template.lesionTableTimepointCell.helpers({
var lesionData = Template.parentData(1); var lesionData = Template.parentData(1);
return (lesionData && return (lesionData &&
lesionData.timepoints && lesionData.timepoints &&
lesionData.timepoints[this.timepointID]); lesionData.timepoints[this.timepointId]);
}, },
displayData: function() { displayData: function() {
// Search Measurements by lesion and timepoint // Search Measurements by lesion and timepoint
var lesionData = Template.parentData(1); var lesionData = Template.parentData(1);
if (!lesionData || if (!lesionData ||
!lesionData.timepoints || !lesionData.timepoints ||
!lesionData.timepoints[this.timepointID]) { !lesionData.timepoints[this.timepointId]) {
return; return;
} }
var data = lesionData.timepoints[this.timepointID]; var data = lesionData.timepoints[this.timepointId];
if (lesionData.isTarget === true) { if (lesionData.isTarget === true) {
if (data.shortestDiameter) { if (data.shortestDiameter) {
@ -40,7 +40,7 @@ function doneCallback(measurementData, deleteTool) {
// the specified Timepoint Cell // the specified Timepoint Cell
if (deleteTool === true) { if (deleteTool === true) {
log.info('Confirm clicked!'); log.info('Confirm clicked!');
clearMeasurementTimepointData(measurementData.id, measurementData.timepointID); clearMeasurementTimepointData(measurementData.id, measurementData.timepointId);
} }
} }
@ -57,12 +57,12 @@ Template.lesionTableTimepointCell.events({
var currentMeasurement = Template.parentData(1); var currentMeasurement = Template.parentData(1);
// Create some fake measurement data // Create some fake measurement data
var currentTimepointID = this.timepointID; var currentTimepointID = this.timepointId;
var timepointData = currentMeasurement.timepoints[currentTimepointID]; var timepointData = currentMeasurement.timepoints[currentTimepointID];
var measurementData = { var measurementData = {
id: currentMeasurement._id, id: currentMeasurement._id,
timepointID: currentTimepointID, timepointId: currentTimepointID,
response: timepointData.response, response: timepointData.response,
imageId: timepointData.imageId, imageId: timepointData.imageId,
handles: timepointData.handles, handles: timepointData.handles,
@ -83,7 +83,7 @@ Template.lesionTableTimepointCell.events({
if (keyCode === keys.DELETE || if (keyCode === keys.DELETE ||
(keyCode === keys.D && e.ctrlKey === true)) { (keyCode === keys.D && e.ctrlKey === true)) {
var currentMeasurement = Template.parentData(1); var currentMeasurement = Template.parentData(1);
var currentTimepointID = this.timepointID; var currentTimepointID = this.timepointId;
showConfirmDialog(function() { showConfirmDialog(function() {
clearMeasurementTimepointData(currentMeasurement._id, currentTimepointID); clearMeasurementTimepointData(currentMeasurement._id, currentTimepointID);

View File

@ -1,9 +1,6 @@
<template name="lesionTableTimepointHeader"> <template name="lesionTableTimepointHeader">
<th class="lesionTableTimepointCell"> <th class="lesionTableTimepointCell" title="Latest study date: {{formatDA latestDate "MM/DD/YYYY"}}"
{{formatDA timepointName "MM/DD/YYYY"}} data-toggle="tooltip" data-placement="top">
{{#if timepointTextFound}} {{ timepointName }}
{{timepointText}}
{{/if}}
{{#if timepointLoaded }}<span class="timepointIndicator">&lt;</span> {{/if}}
</th> </th>
</template> </template>

View File

@ -1,105 +1,6 @@
Template.lesionTableTimepointHeader.events({
'click th.lesionTableTimepointCell': function(e, template) {
var parentPosition = getPosition(e.currentTarget);
var cellText = e.currentTarget.innerText;
// Remove spaces in string
cellText = cellText.replace(/\s/g, ''); // Remove spaces
cellText = cellText.replace('<', ''); // Remove <
var splitCellText = cellText.split('/');
var dateStr = splitCellText[2].replace(/\D/g,'') + '' + splitCellText[0].replace(/\D/g,'') + '' + splitCellText[1].replace(/\D/g,''); // Remove non-digit chars
// Check patient has a timepointText as Baseline
var patientId = template.data.patientId;
// Open popup
var timepointTextDialog = $('#timepointTextDialog');
var dialogDisplay = timepointTextDialog.css('display');
if (dialogDisplay === 'none') {
var isBaselineInCollection = Timepoints.findOne({
patientId: patientId,
timepointName: dateStr,
timepointText: 'Baseline'
});
// If isBaselineInCollection is true, "Baseline" is found in collection for patient and set checkbox as checked
if (isBaselineInCollection) {
// Set checkbox as checked
$('#checkBoxBaseline').prop('checked', true);
} else {
// Set checkbox as unchecked
$('#checkBoxBaseline').prop('checked', false);
}
// Open dialog
var dialogProperty = {
top: parentPosition.y - 30,
left: parentPosition.x,
display: 'block'
};
timepointTextDialog.css(dialogProperty);
} else {
// Get timepoints of patient
// Set timepointText as Baseline for selected timepoint
var timepoints = Timepoints.find({
patientId: patientId
}).fetch();
// Check checkbox is selected
var checkboxBaselineChecked = $('#checkBoxBaseline').is(':checked');
timepoints.forEach(function(timepoint) {
// timepointText defines a custom text for timepoint such as Baseline, Nadir, Current
if (timepoint.timepointName === dateStr) {
// If checkbox is selected, set timepointText as Baseline
// Else set timepointText as ""
var timepointText = 'Baseline';
if (!checkboxBaselineChecked) {
timepointText = '';
}
Timepoints.update(timepoint._id,{
$set: {
timepointText: timepointText
}
});
} else {
// Set timepointText as empty
Timepoints.update(timepoint._id,{
$set: {
timepointText: ''
}
});
}
});
// Close dialog
timepointTextDialog.css('display', 'none');
}
}
});
Template.lesionTableTimepointHeader.helpers({ Template.lesionTableTimepointHeader.helpers({
timepointTextFound: function() { 'timepointName': function() {
var timepointText = this.timepointText; var timepoint = this;
return (timepointText && timepointText === 'Baseline'); return getTimepointName(timepoint);
} }
}); });
// Gets parent's position of element which mouse pointer is clicked in
function getPosition(element) {
var xPosition = 0;
var yPosition = 0;
while (element) {
xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft);
yPosition += (element.offsetTop - element.scrollTop + element.clientTop);
element = element.offsetParent;
}
return {
x: xPosition,
y: yPosition
};
}

View File

@ -1,9 +1,10 @@
<template name="layoutLesionTracker"> <template name="lesionTrackerLayout">
<div class="logoContainer"> <div class="logoContainer">
<a class="navbar-brand" href="http://ohif.org"> <a class="navbar-brand" href="http://ohif.org">
<img src="/images/logo.png"> <img src="/images/logo.png">
<h4 class="name">Open Health Imaging Foundation</h4> <h4 class="name">Open Health Imaging Foundation</h4>
</a> </a>
</div> </div>
{{> yield}} {{ >optionsButton }}
{{ >yield }}
</template> </template>

View File

@ -0,0 +1,26 @@
<template name="lesionTrackerViewportOverlay">
<div class="imageViewerViewportOverlay noselect">
<div class="topleft dicomTag">
<div>{{formatPN patientName}}</div>
<div>{{patientId}}</div>
<div class='timepointName'>{{timepointName}}</div>
</div>
<div class="topright dicomTag">
<div>{{studyDescription}}</div>
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
</div>
<div class="bottomright dicomTag">
<div>Zoom: {{formatNumberPrecision zoom 2}}%</div>
<div>{{compression}}</div>
<div>{{wwwc}}</div>
</div>
<div class="bottomleft dicomTag">
<div>Ser: {{seriesNumber}}</div>
<div>Img: {{imageNumber}} ({{imageIndex}}/{{numImages}})</div>
<div>{{frameRate}}</div>
<div>{{imageDimensions}}</div>
<div>{{seriesDescription}}</div>
</div>
{{>imageControls}}
</div>
</template>

View File

@ -0,0 +1,30 @@
// Use Aldeed's meteor-template-extension package to replace the
// default viewportOverlay template.
// See https://github.com/aldeed/meteor-template-extension
var defaultTemplate = 'viewportOverlay';
Template.lesionTrackerViewportOverlay.replaces(defaultTemplate);
// Add the TimepointName helper to the default template. The
// HTML of this template is replaced with that of lesionTrackerViewportOverlay
Template[defaultTemplate].helpers({
timepointName: function() {
var data = this;
var study = Studies.findOne({
studyInstanceUid: data.studyInstanceUid
});
if (!study) {
return;
}
var timepoint = Timepoints.findOne({
timepointId: study.timepointId
});
if (!timepoint) {
return;
}
return getTimepointName(timepoint);
}
});

View File

@ -0,0 +1,3 @@
.imageViewerViewportOverlay
.timepointName
color: #32BFFF

View File

@ -1,11 +1,10 @@
<template name="studyContextMenu"> <template name="lesionTrackerWorklistContextMenu">
<div id="studyContextMenu" class="studyContextMenu noselect" <div id="studyContextMenu" class="studyContextMenu noselect"
oncontextmenu='return false;' oncontextmenu='return false;'
unselectable='on' unselectable='on'
onselectstart='return false;'> onselectstart='return false;'>
<ul> <ul>
<li> <li>
<!--<a id="deleteTool">Delete</a>-->
<!-- TODO: Make this dynamically accept options--> <!-- TODO: Make this dynamically accept options-->
<a id="launchStudyAssociation" type="button" <a id="launchStudyAssociation" type="button"
data-toggle="modal" data-toggle="modal"
@ -14,16 +13,20 @@
<i class="fa fa-calendar-plus-o fa-lg"></i> <i class="fa fa-calendar-plus-o fa-lg"></i>
Associate Associate
</a> </a>
<a><span class="fa-stack fa-lg"> <a id="removeTimepointAssociations" type="button"
title="Remove Timepoint Association">
<i class="fa fa-eraser fa-lg"></i> Remove Association
</a>
<a class="disabled"><span class="fa-stack fa-lg">
<i class="fa fa-user fa-stack-1x"></i> <i class="fa fa-user fa-stack-1x"></i>
<i class="fa fa-ban fa-stack-2x text-danger"></i> <i class="fa fa-ban fa-stack-2x text-danger"></i>
</span> Anonymize </span> Anonymize
</a> </a>
<a><i class="fa fa-trash fa-lg"></i> Delete</a> <a class="disabled"><i class="fa fa-trash fa-lg"></i> Delete</a>
<a><i class="fa fa-send-o fa-lg"></i> Send</a> <a class="disabled"><i class="fa fa-send-o fa-lg"></i> Send</a>
<a><i class="fa fa-exchange fa-lg"></i> Export</a> <a class="disabled"><i class="fa fa-exchange fa-lg"></i> Export</a>
<a><i class="fa fa-download fa-lg"></i> Download</a> <a class="disabled"><i class="fa fa-download fa-lg"></i> Download</a>
<a><i class="fa fa-photo fa-lg"></i> View Series Details</a> <a class="disabled"><i class="fa fa-photo fa-lg"></i> View Series Details</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -0,0 +1,75 @@
// Use Aldeed's meteor-template-extension package to replace the
// default WorklistStudy template.
// See https://github.com/aldeed/meteor-template-extension
var defaultTemplate = 'studyContextMenu';
Template.lesionTrackerWorklistContextMenu.replaces(defaultTemplate);
Worklist.functions['removeTimepointAssociations'] = removeTimepointAssociations;
/**
* Removes all present study / timepoint associations from the Clinical Trial
*/
function removeTimepointAssociations() {
// Get a Cursor pointing to the selected Studies from the Worklist
var selectedStudies = WorklistSelectedStudies.find({}, {
sort: {
studyDate: 1
}
});
// Loop through the Cursor of Selected Studies
selectedStudies.forEach(function(selectedStudy) {
// Find the selected study in question in the Collection of
// Timepoint/Study associated Studies
var study = Studies.findOne({
studyInstanceUid: selectedStudy.studyInstanceUid
});
// If the studies that were selected are not already associated
// with a Timepoint, stop here
if (!study) {
return;
}
// Update the Studies Collection to remove the link to this Timepoint
Studies.update(study._id, {
unset: {
timepointId: ''
}
});
// Find the Timepoint that was previously referenced
var timepoint = Timepoints.findOne({
timepointId: study.timepointId
});
// Find the index of the current studyInstanceUid in the array
// of reference studyInstanceUids
var index = timepoint.studyInstanceUids.indexOf(study.studyInstanceUid);
if (index < 0) {
return;
}
// Remove the specified studyInstanceUid from the array of associated studyInstanceUids
timepoint.studyInstanceUids.splice(index, 1);
// Check if there are still one or more Studies associated with this Timepoint
if (timepoint.studyInstanceUids.length) {
// Update the Timepoints Collection with this modified array for the
// studyInstanceUids attribute
Timepoints.update(timepoint._id, {
$set: {
studyInstanceUids: timepoint.studyInstanceUids
}
});
} else {
// If no more Studies are associated with this Timepoint, we should remove it
// from the Timepoints Collection via a server call
Meteor.call('removeTimepoint', timepoint._id, function(error) {
if (error) {
log.warn(error);
}
});
}
});
}

View File

@ -0,0 +1,31 @@
<template name="lesionTrackerWorklistStudy">
<tr class="worklistStudy noselect">
<td>
{{formatPN patientName}}
</td>
<td>
{{patientId}}
</td>
{{#unless isTouchDevice}}
<td>
{{accessionNumber}}
</td>
{{/unless}}
<td>
{{formatDA studyDate}} {{ #if timepointName }}({{timepointName}}){{ /if }}
</td>
{{#unless isTouchDevice}}
<td>
{{modalities}}
</td>
{{/unless}}
<td>
{{studyDescription}}
</td>
{{#unless isTouchDevice}}
<td>
{{numberOfStudyRelatedInstances}}
</td>
{{/unless}}
</tr>
</template>

View File

@ -0,0 +1,30 @@
// Use Aldeed's meteor-template-extension package to replace the
// default WorklistStudy template.
// See https://github.com/aldeed/meteor-template-extension
var defaultTemplate = 'worklistStudy';
Template.lesionTrackerWorklistStudy.replaces(defaultTemplate);
// Add the TimepointName helper to the default template. The
// HTML of this template is replaced with that of lesionTrackerWorklistStudy
Template[defaultTemplate].helpers({
timepointName: function() {
var data = this;
var study = Studies.findOne({
studyInstanceUid: data.studyInstanceUid
});
if (!study) {
return;
}
var timepoint = Timepoints.findOne({
timepointId: study.timepointId
});
if (!timepoint) {
return;
}
return getTimepointName(timepoint);
}
});

View File

@ -11,24 +11,29 @@ function closeHandler(dialog) {
// This event sets lesion number for new lesion // This event sets lesion number for new lesion
function setLesionNumberCallback(measurementData, eventData, doneCallback) { function setLesionNumberCallback(measurementData, eventData, doneCallback) {
// Get the current element's timepointID from the study date metadata // Get the current element's timepointId from the study date metadata
var element = eventData.element; var element = eventData.element;
var enabledElement = cornerstone.getEnabledElement(element); var enabledElement = cornerstone.getEnabledElement(element);
var imageId = enabledElement.image.imageId; var imageId = enabledElement.image.imageId;
var study = cornerstoneTools.metaData.get('study', imageId); var study = cornerstoneTools.metaData.get('study', imageId);
// Find the relevant timepoint given the current study
var timepoint = Timepoints.findOne({ var timepoint = Timepoints.findOne({
timepointName: study.studyDate studyInstanceUids: {
$in: [study.studyInstanceUid]
}
}); });
if (!timepoint) { if (!timepoint) {
return; return;
} }
measurementData.timepointID = timepoint.timepointID; measurementData.timepointId = timepoint.timepointId;
// Get a lesion number for this lesion, depending on whether or not the same lesion previously // Get a lesion number for this lesion, depending on whether or not the same lesion previously
// exists at a different timepoint // exists at a different timepoint
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = false); var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = false);
measurementData.lesionNumber = lesionNumber; measurementData.lesionNumber = lesionNumber;
// Set lesion number // Set lesion number
@ -211,7 +216,7 @@ changeNonTargetLocationCallback = function(measurementData, eventData, doneCallb
multi: true multi: true
}); });
var response = measurement.timepoints[measurementData.timepointID].response; var response = measurement.timepoints[measurementData.timepointId].response;
// TODO = Standardize this. Searching by code probably isn't the best, we should use // TODO = Standardize this. Searching by code probably isn't the best, we should use
// some sort of UID // some sort of UID

View File

@ -65,7 +65,7 @@ changeNonTargetResponse = function(measurementData, eventData, doneCallback) {
return; return;
} }
var response = measurement.timepoints[measurementData.timepointID].response; var response = measurement.timepoints[measurementData.timepointId].response;
// TODO = Standardize this. Searching by code probably isn't the best, we should use // TODO = Standardize this. Searching by code probably isn't the best, we should use
// some sort of UID // some sort of UID

View File

@ -0,0 +1,12 @@
<template name="optionsButton">
<div class="optionsButton">
<a type="button"
class="btn btn-default"
data-toggle="modal"
data-target="#optionsModal"
title="Trial Options">
<i class="fa fa-cog fa-lg"></i>
Trial Options
</a>
</div>
</template>

View File

@ -0,0 +1,6 @@
// These settings are temporary!
.optionsButton
position: absolute
top: 3px
right: 350px
cursor: pointer

View File

@ -0,0 +1,83 @@
<template name="optionsModal">
<div class="modal" id="optionsModal" tabindex="-1" role="dialog" aria-labelledby="optionsModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="optionsModalLabel">Clinical Trial Options</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<h4>Select a Trial Criteria Type</h4>
<label for="recistCriteria" class="trialCriteriaLabel">
<input type="radio" name="trialCriteria" checked="true" id="recistCriteria" value='RECIST' class="trialCriteria"> RECIST 1.1
</label>
<label for="irRCCriteria" class="trialCriteriaLabel">
<input type="radio" name="trialCriteria" id="irRCCriteria" value='irRC' class="trialCriteria"> irRC
</label>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<h4>Trial Criteria Descriptions</h4>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#RECISTDescription" aria-controls="home" role="tab" data-toggle="tab">RECIST 1.1</a>
</li>
<li role="presentation">
<a href="#irRCDescription" aria-controls="profile" role="tab" data-toggle="tab">irRC</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade in active" id="RECISTDescription">
<h5>Baseline Checks</h5>
<ul>
<li>Extranodal lesions must be >= 10 mm long axis AND >= double the acquisition slice thickness by CT and MR</li>
<li>Extranodal lesions must be >= 20 mm on chest x-ray (although x-rays rarely used for clinical trial assessment)</li>
<li>Nodal lesions must be >= 15 mm short axis AND >= double the acquisition slice thickness by CT and MR</li>
<li>Up to a max of 2 target lesions per organ</li>
<li>Up to a max of 5 target lesions total</li>
<li>Non-targets can only be assessed as 'present'</li>
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
</ul>
<!-- Time Point Measurement Total = Sum of long axis measurements for extranodal target lesion + short axis measurements for nodal lesions</li>-->
</div>
<div role="tabpanel" class="tab-pane fade" id="irRCDescription">
<h5>Baseline Checks</h5>
<ul>
<li>Target lesions must be >= 10 X 10 mm AND >= double the acquisition slice thickness by CT and MR</li>
<li>Up to a max of 5 target lesions per organ</li>
<li>Up to a max of 10 target lesions total</li>
<li>Non-targets can only be assessed as 'present'</li>
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
</ul>
<h5>New Lesion Checks for Follow-ups</h5>
<ul>
<li>New target lesions must be >= 5 X 5 mm AND >= double the acquisition slice thickness by CT and MR</li>
<li>Up to a max of 5 target lesions per organ</li>
<li>Up to a max of 10 target lesions total</li>
</ul>
<!--Time Point Measurement Total = SPD target lesions + SPD new lesions (SPD = sum of product of long axis and short axis diameters)-->
</div>
</div>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<h4>Additional Options</h4>
<a type="button" class="btn btn-danger clearAllStudyTimepointAssociations">
<i class="fa fa-trash"></i> Clear All Study/Timepoint Associations
</a>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,33 @@
Session.setDefault('TrialResponseAssessmentCriteria', 'RECIST');
Template.optionsModal.events({
/**
* When the trial criteria radio buttons are changed, change the
* trial assessment criteria for the Lesion Tracker
*
* @param e The 'change' event on the selected radio button
*/
'change input.trialCriteria': function(e) {
// Get the Trial Criteria type that the selected radio button represents
var radioButton = $(e.currentTarget);
var criteriaType = radioButton.val();
// Set this as the current Trial Response Assessment Criteria
// TODO: Update when we have more trial-level support
// (Currently this information is stored in Session, later this will change)
Session.set('TrialResponseAssessmentCriteria', criteriaType);
log.info('Trial Criteria changed to: ' + criteriaType);
},
/**
* When the Clear Study/Timepoint Associations button is clicked, we
* send a call to the server to erase all entries in the Timepoints Collection.
*/
'click a.clearAllStudyTimepointAssociations': function() {
Meteor.call('clearAllTimepoints', function(error) {
if (error) {
log.warn(error);
}
});
}
});

View File

@ -0,0 +1,2 @@
.trialCriteriaLabel
margin: 0 5px

View File

@ -43,13 +43,15 @@
<td class="studyDataCell {{ #if autoselected }}disabled{{ /if }}"> <td class="studyDataCell {{ #if autoselected }}disabled{{ /if }}">
<p>{{studyDescription}}</p> <p>{{studyDescription}}</p>
</td> </td>
<td class="timepointOptions center studyDataCell {{ #if autoselected }}disabled{{ /if }}"> <td class="timepointOptions center studyDataCell noselect {{ #if autoselected }}disabled{{ /if }}">
{{ #each timepointOptions }} {{ #each timepointOptions }}
<label> <label class="noselect">
<input type="radio" <input type="radio"
name="{{../_id}}" name="{{../_id}}"
value={{type}} value="{{value}}"
{{ inlineIf autoselected true 'disabled'}}> class="timepointOption"
{{ inlineIf checked true 'checked' }}
{{ inlineIf autoselected true 'disabled' }}>
{{name}} {{name}}
</label> </label>
{{ /each }} {{ /each }}

View File

@ -30,8 +30,13 @@ function getDateRange(selectedStudies, range) {
} }
/** /**
* Selects all studies related to the currently input studies,
* based on various criteria. Returns the entire array of related studies.
* *
* @returns {Array} * (at the moment, this is only the date range +/- 14 days, with a matching patientId)
*
* @param selectedStudies A user-selected list of studies
* @returns {*} The entire array of related studies
*/ */
function autoSelectStudies(selectedStudies) { function autoSelectStudies(selectedStudies) {
if (!selectedStudies.length) { if (!selectedStudies.length) {
@ -40,6 +45,9 @@ function autoSelectStudies(selectedStudies) {
var range = getDateRange(selectedStudies); var range = getDateRange(selectedStudies);
// Fetch autoselected studies based on the date range
// Note that we used MongoDB's fetch here so we have a mutable array,
// rather than a Cursor
var autoselected = WorklistStudies.find({ var autoselected = WorklistStudies.find({
studyDate: { studyDate: {
$gte: range.earliestDate.format('YYYYMMDD'), $gte: range.earliestDate.format('YYYYMMDD'),
@ -82,8 +90,8 @@ Template.studyAssociationTable.helpers({
studyDate: 1 studyDate: 1
} }
}).fetch() || []; }).fetch() || [];
var autoselected = autoSelectStudies(selectedStudies);
return autoselected; return autoSelectStudies(selectedStudies);
}, },
/** /**
* This helper returns the list of Timepoint types the user can set for this study * This helper returns the list of Timepoint types the user can set for this study
@ -91,16 +99,15 @@ Template.studyAssociationTable.helpers({
* @returns {Array.<T>} * @returns {Array.<T>}
*/ */
timepointOptions: function() { timepointOptions: function() {
return [ return [{
{
value: 'baseline', value: 'baseline',
name: 'Baseline' name: 'Baseline',
}, checked: true
{ }, {
value: 'followup', value: 'followup',
name: 'Follow-up' name: 'Follow-up',
} checked: false
]; }];
}, },
earliestDate: function() { earliestDate: function() {
var selectedStudies = WorklistSelectedStudies.find({}, { var selectedStudies = WorklistSelectedStudies.find({}, {
@ -108,12 +115,11 @@ Template.studyAssociationTable.helpers({
studyDate: 1 studyDate: 1
} }
}).fetch(); }).fetch();
var range = getDateRange(selectedStudies);
if (!range) {
return;
}
var range = getDateRange(selectedStudies);
if (range) {
return range.earliestDate; return range.earliestDate;
}
}, },
latestDate: function() { latestDate: function() {
var selectedStudies = WorklistSelectedStudies.find({}, { var selectedStudies = WorklistSelectedStudies.find({}, {
@ -121,13 +127,12 @@ Template.studyAssociationTable.helpers({
studyDate: 1 studyDate: 1
} }
}).fetch(); }).fetch();
var range = getDateRange(selectedStudies);
if (!range) {
return;
}
var range = getDateRange(selectedStudies);
if (range) {
return range.latestDate; return range.latestDate;
} }
}
}); });
Template.studyAssociationTable.events({ Template.studyAssociationTable.events({
@ -143,12 +148,3 @@ Template.studyAssociationTable.events({
} }
} }
}); });
//trial criteria!
/*There shall be Associate option in right-click dialog
If associated, double-click shall go to image view.
If not associated, user shall be directed to Associate Time Points Dialog
Use shall also be allowed to select multiple studies from study list to associate
Associate Time Point dialog shall present selected studies and studies within defined time window of =/- 14 days of selected studies
User should only be able to associate one time point at a time (user should not be able to select both BL and F/U for different studies in associate dialog)
*/

View File

@ -4,7 +4,7 @@
<span class="loading"><i class="fa fa-spinner fa-spin"></i></span> <span class="loading"><i class="fa fa-spinner fa-spin"></i></span>
<select id="selectStudyDate"> <select id="selectStudyDate">
{{#each relatedStudies}} {{#each relatedStudies}}
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}}</option> <option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}} ({{timepointName}})</option>
{{/each}} {{/each}}
</select> </select>
</div> </div>

View File

@ -4,21 +4,22 @@ Template.studyDateList.helpers({
* The value for 'selected' for the currently loaded study is set to true, so that * The value for 'selected' for the currently loaded study is set to true, so that
* this becomes the current option in the combo box. * this becomes the current option in the combo box.
* *
* @returns {*} * @returns {*} Array of studies that are related to the current study by patient ID
*/ */
relatedStudies: function() { relatedStudies: function() {
// TODO= Fix this! This won't work to retrieve all studies
// related to this patient. We will need to do a real search
// since the WorklistStudies Collection only contains the studies on-screen
// Check which study is currently loaded into the study browser // Check which study is currently loaded into the study browser
var currentStudyInBrowser = ViewerStudies.findOne({ var currentStudyInBrowser = ViewerStudies.findOne({
selected: true selected: true
}); });
// Find studies which have the same patientId as the currently selected study // Find all Timepoint-associated studies which have the same patientId as the currently selected study
var relatedStudies = WorklistStudies.find({patientId: currentStudyInBrowser.patientId}, var relatedStudies = Studies.find({
{sort: {studyDate: 1}}).fetch(); patientId: currentStudyInBrowser.patientId
}, {
sort: {
studyDate: 1
}
});
// Modify the array of related studies so the default option is the currently selected study // Modify the array of related studies so the default option is the currently selected study
relatedStudies.forEach(function(study) { relatedStudies.forEach(function(study) {
@ -31,6 +32,26 @@ Template.studyDateList.helpers({
// Use this array to populate the combo box // Use this array to populate the combo box
return relatedStudies; return relatedStudies;
},
timepointName: function() {
var data = this;
var study = Studies.findOne({
studyInstanceUid: data.studyInstanceUid
});
if (!study) {
return;
}
var timepoint = Timepoints.findOne({
timepointId: study.timepointId
});
if (!timepoint) {
return;
}
return getTimepointName(timepoint);
} }
}); });
@ -46,7 +67,6 @@ Template.studyDateList.events({
*/ */
'change select#selectStudyDate': function(e) { 'change select#selectStudyDate': function(e) {
var selectBox = $(e.currentTarget); var selectBox = $(e.currentTarget);
var studyInstanceUid = selectBox.val(); var studyInstanceUid = selectBox.val();
// Hide the select box // Hide the select box
@ -57,6 +77,11 @@ Template.studyDateList.events({
loadingIndicator.css('display', 'block'); loadingIndicator.css('display', 'block');
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) { Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
if (error) {
log.warn(error);
return;
}
sortStudy(study); sortStudy(study);
// Hide the loading indicator // Hide the loading indicator
@ -66,13 +91,11 @@ Template.studyDateList.events({
selectBox.css('display', 'block'); selectBox.css('display', 'block');
// Set "Selected" to false for the entire collection // Set "Selected" to false for the entire collection
ViewerStudies.update({}, ViewerStudies.update({}, {
{
$set: { $set: {
selected: false selected: false
} }
}, }, {
{
multi: true multi: true
}); });
@ -81,6 +104,7 @@ Template.studyDateList.events({
var existingStudy = ViewerStudies.findOne({ var existingStudy = ViewerStudies.findOne({
studyInstanceUid: studyInstanceUid studyInstanceUid: studyInstanceUid
}); });
if (existingStudy) { if (existingStudy) {
// Set the current finding in the collection to true // Set the current finding in the collection to true
ViewerStudies.update(existingStudy._id, { ViewerStudies.update(existingStudy._id, {
@ -95,29 +119,6 @@ Template.studyDateList.events({
// with the value True, and insert it into the ViewerStudies Collection // with the value True, and insert it into the ViewerStudies Collection
study.selected = true; study.selected = true;
ViewerStudies.insert(study); ViewerStudies.insert(study);
var timepointID = uuid.v4();
var timepoint = Timepoints.findOne({
timepointName: study.studyDate
});
if (timepoint) {
log.warn('A timepoint with that study date already exists!');
return;
}
var testTimepoint = Timepoints.findOne({});
if (testTimepoint && testTimepoint.patientId !== study.patientId) {
log.warn('Timepoints collection related to the wrong subject');
return;
}
log.info('Inserting a new timepoint');
Timepoints.insert({
patientId: study.patientId,
timepointID: timepointID,
timepointName: study.studyDate
});
}); });
} }
}); });

View File

@ -0,0 +1,202 @@
/**
* Retrieve a location name (e.g. Liver Right) from the
* PatientLocations Collection by id, if it exists. Otherwise,
* return an empty string.
*
* @param id
* @returns {*|string}
*/
function getLocationName(id) {
var locationObject = PatientLocations.findOne(id);
if (!locationObject || !locationObject.location) {
return '';
}
return locationObject.location;
}
/**
* Update the Timepoint object for a specific Measurement.
* If no measurement exists yet, one will be created.
*
* Input is toolData from the lesion or nonTarget tool
*
* @param lesionData
*/
function updateLesionData(lesionData) {
var study = Studies.findOne({
studyInstanceUid: lesionData.studyInstanceUid
});
if (!study) {
log.warn('Study is not associated with a timepoint');
return;
}
var timepoint = Timepoints.findOne({
timepointId: study.timepointId
});
if (!timepoint) {
log.warn('Timepoint in an image is not present in the Timepoints Collection?');
return;
}
// Find the specific lesion to be updated
var existingMeasurement;
if (lesionData.id && lesionData.id !== 'notready') {
existingMeasurement = Measurements.findOne(lesionData.id);
} else {
existingMeasurement = Measurements.findOne({
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget
});
}
// Create a structure for the timepointData based
// on this Lesion's toolData
var timepointData = {
seriesInstanceUid: lesionData.seriesInstanceUid,
studyInstanceUid: lesionData.studyInstanceUid,
sopInstanceUid: lesionData.sopInstanceUid,
handles: lesionData.handles,
imageId: lesionData.imageId
};
if (lesionData.isTarget === true) {
timepointData.shortestDiameter = lesionData.widthMeasurement;
timepointData.longestDiameter = lesionData.measurementText;
} else {
timepointData.response = lesionData.response;
}
// If no such lesion exists, we need to add one
if (!existingMeasurement) {
// Create a data structure for the Measurement
// based on the current tool data
var measurement = {
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget,
patientId: lesionData.patientId,
id: lesionData.id
};
// Retrieve the location name given the locationUID
if (lesionData.locationUID !== undefined) {
var locationObj = PatientLocations.findOne({
locationUID: lesionData.locationUID
});
measurement.location = locationObj.location;
measurement.isNodal = locationObj.isNodal;
}
// Add toolData parameters to the Measurement at this Timepoint
measurement.timepoints = {};
measurement.timepoints[timepoint.timepointId] = timepointData;
// Set a flag to prevent duplication of toolData
measurement.toolDataInsertedManually = true;
// Increment and store the absolute Lesion Number for this Measurement
measurement.lesionNumberAbsolute = Measurements.find().count() + 1;
// Insert this into the Measurements Collection
// Save the ID into the toolData (not sure if this works?)
measurement.id = Measurements.insert(measurement);
// Update the database entry so it can be readded next time the study is loaded
Measurements.update(measurement.id, {
$set: {
toolDataInsertedManually: false
}
});
} else {
lesionData.id = existingMeasurement._id;
lesionData.isNodal = existingMeasurement.isNodal;
// Update timepoints from lesion data
existingMeasurement.timepoints[timepoint.timepointId] = timepointData;
Measurements.update(existingMeasurement._id, {
$set: {
timepoints: existingMeasurement.timepoints
}
});
}
}
/**
* Returns new lesion number according to timepointId
* @param timepointId
* @param isTarget
* @returns {*}
*/
function getNewLesionNumber(timepointId, isTarget) {
// Get all current lesion measurements
var numMeasurements = Measurements.find({
isTarget: isTarget
}).count();
// If no measurements exist yet, start at 1
if (!numMeasurements) {
return 1;
}
// Find related measurements (i.e. target or non-target)
var measurements = Measurements.find({
isTarget: isTarget
}, {
sort: {
lesionNumber: 1
}
}).fetch();
// If measurements exist, find the last lesion number
// from the given timepoint
var lesionNumberCounter = 1;
// Search through every Measurement to see which ones
// already have data for this Timepoint, if we find one that
// doesn't have data, we will stop there and use that as the
// current Measurement
measurements.every(function(measurement) {
// If this measurement has no data for this Timepoint,
// use this as the current Measurement
if (!measurement.timepoints[timepointId]) {
lesionNumberCounter = measurement.lesionNumber;
return false;
}
lesionNumberCounter++;
return true;
});
return lesionNumberCounter;
}
/**
* If the current Lesion Number already exists
* for any other timepoint, returns lesion locationUID
* @param lesionData
* @returns {*}
*/
function lesionNumberExists(lesionData) {
var measurement = Measurements.findOne({
lesionNumber: lesionData.lesionNumber,
isTarget: lesionData.isTarget
});
if (!measurement) {
return;
}
return measurement.locationUID;
}
LesionManager = {
updateLesionData: updateLesionData,
getNewLesionNumber: getNewLesionNumber,
lesionNumberExists: lesionNumberExists,
getLocationName: getLocationName
};

View File

@ -0,0 +1,388 @@
// Define the Trial Criteria Structure
TrialCriteriaConstraints = {
RECIST: RECIST,
irRC: irRC
};
/**
* RECIST 1.1 Trial Criteria Definition
*
* Baseline Checks:
* - Extranodal lesions must be >/= 10 mm long axis AND >/= double the acquisition slice thickness by CT and MR
* - Extranodal lesions must be >/= 20 mm on chest x-ray (although x-rays rarely used for clinical trial assessment)
* - Nodal lesions must be >/= 15 mm short axis AND >/= double the acquisition slice thickness by CT and MR
* - Up to a max of 2 target lesions per organ
* - Up to a max of 5 target lesions total
* - Non-targets can only be assessed as 'present'
* - Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)
* - Time Point Measurement Total = Sum of long axis measurements for extranodal target lesion + short axis measurements for nodal lesions
*/
function RECIST(image) {
var acquisitionSliceThickness;
if (image) {
acquisitionSliceThickness = image.acquisitionSliceThickness;
// TODO: Use metaData to determine if this is a chest X-ray
var isChestXray = false;
}
// Define the RECIST 1.1 structure
var criteria = {
baseline: {
target: {},
nonTarget: {},
group: {}
}
};
if (acquisitionSliceThickness) {
criteria.baseline.target.nodal = {
shortestDiameter: {
numericality: {
greaterThanOrEqualTo: Math.min(15, 2 * acquisitionSliceThickness),
message: '^Nodal lesions must be >= 15 mm short axis AND >= double the acquisition slice thickness (' +
acquisitionSliceThickness + ' mm) for CT and MR.'
}
}
};
} else {
criteria.baseline.target.nodal = {
shortestDiameter: {
numericality: {
greaterThanOrEqualTo: 15,
message: '^Nodal target lesions must be >= %{count} mm short axis'
}
}
};
}
criteria.baseline.target.all = {
// - Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)
response: {
exclusion: {
within: {
CR: 'Complete Response (CR)',
UN: 'Unknown (UN)',
NE: 'Non-evaluable (NE)',
EX: 'Excluded (EX)'
},
message: '^Target lesions must have a length and cannot be marked as %{value} at baseline.'
}
},
totalLesionBurden: {
numericality: {
greaterThanOrEqualTo: 2, // TODO: Check this, the value wasn't specified!
message: '^Total lesion burden (SPD target lesions + SPD new lesions) should be greater than %{count}.'
}
}
};
criteria.baseline.nonTarget.all = {
// - Non-targets can only be assessed as 'present'
response: {
// This is a workaround since Validating equality to something is not implemented yet
// https://github.com/ansman/validate.js/issues/79
presence: {
message: "^Non-target lesions can only be assessed as 'Present' at Baseline"
},
inclusion: {
within: ['Present'],
message: "^Non-target lesions can only be assessed as 'Present' at Baseline"
}
}
};
criteria.baseline.perOrgan = {
numberOfLesionsPerOrgan: {
numericality: {
lessThanOrEqualTo: 2,
//message: '^A maximum of %{count} target lesions per organ are allowed at Baseline.'
}
}
};
criteria.baseline.group = {
totalNumberOfLesions: {
numericality: {
lessThanOrEqualTo: 5,
//message: '^A maximum of %{count} target lesions total are allowed at Baseline.'
}
}
};
if (acquisitionSliceThickness) {
criteria.baseline.target.extraNodal = {
longestDiameter: {
numericality: {
greaterThanOrEqualTo: Math.min(10, 2 * acquisitionSliceThickness),
message: '^Extranodal lesions must be >= 10 mm long axis AND >= double the acquisition slice thickness (' +
acquisitionSliceThickness + ' mm) for CT and MR.'
}
}
};
} else if (isChestXray) {
criteria.baseline.target.extraNodal = {
// -
longestDiameter: {
numericality: {
greaterThanOrEqualTo: 20,
message: '^Extranodal lesions must be >= %{count} mm on chest X-ray'
}
}
};
} else {
criteria.baseline.target.extraNodal = {
longestDiameter: {
numericality: {
greaterThanOrEqualTo: 10,
message: '^Extranodal target lesions must be >= %{count} mm long axis'
}
}
};
}
return criteria;
}
/**
* irRC Trial Criteria Definition
*
* Baseline Checks:
* - Target lesions must be >/= 10 X 10 mm
* - Up to a max of 5 target lesions per organ
* - Up to a max of 10 target lesions total
* - Non-targets can only be assessed as 'present'
* - Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)
*/
function irRC(image) {
var acquisitionSliceThickness;
if (image) {
acquisitionSliceThickness = image.acquisitionSliceThickness;
}
// Define the irRC structure
var criteria = {
baseline: {
target: {},
nonTarget: {}
},
followup: {
newLesions: {
target: {}
},
target: {}
},
all: {}
};
if (acquisitionSliceThickness) {
criteria.baseline.target.all = {
longestDiameter: {
numericality: {
greaterThanOrEqualTo: Math.min(10, acquisitionSliceThickness),
message: '^Target lesions must be >= 10 mm long axis AND >= double the acquisition slice thickness (' +
acquisitionSliceThickness + ' mm) for CT and MR.'
}
},
shortestDiameter: {
numericality: {
greaterThanOrEqualTo: Math.min(10, acquisitionSliceThickness),
message: '^Target lesions must be >= 10 mm short axis AND >= double the acquisition slice thickness (' +
acquisitionSliceThickness + ' mm) for CT and MR.'
}
}
};
} else {
criteria.baseline.target.all = {
longestDiameter: {
numericality: {
greaterThanOrEqualTo: 10,
message: '^Target lesions must be >= %{count} mm long axis.'
}
},
shortestDiameter: {
numericality: {
greaterThanOrEqualTo: 10,
message: '^Target lesions must be >= %{count} mm short axis.'
}
}
};
}
criteria.baseline.target.all.response = {
exclusion: {
within: {
CR: 'Complete Response (CR)',
UN: 'Unknown (UN)',
NE: 'Non-evaluable (NE)',
EX: 'Excluded (EX)'
},
message: '^^Target lesions must have a length and cannot be marked as %{value} at baseline.'
}
};
criteria.baseline.nonTarget.all = {
response: {
// This is a workaround since Validating equality to something is not implemented yet
// https://github.com/ansman/validate.js/issues/79
presence: {
message: "^Non-target lesions can only be assessed as 'Present' at Baseline"
},
inclusion: {
within: ['Present'],
message: "^Non-target lesions can only be assessed as 'Present' at Baseline"
}
}
};
criteria.baseline.perOrgan = {
numberOfLesionsPerOrgan: {
numericality: {
lessThanOrEqualTo: 5,
//message: '^A maximum of %{count} target lesions per organ are allowed at Baseline.'
}
}
};
criteria.baseline.group = {
totalNumberOfLesions: {
numericality: {
lessThanOrEqualTo: 10,
//message: '^A maximum of %{count} target lesions total are allowed at Baseline.'
}
}
};
if (acquisitionSliceThickness) {
criteria.followup.newLesions.target.all = {
// - New target lesions must be >/= 5 X 5 mm AND >/= double the acquisition slice thickness by CT and MR
longestDiameter: {
numericality: {
greaterThanOrEqualTo: Math.min(5, 2 * acquisitionSliceThickness),
message: '^New target lesions must be >= 5 mm long axis AND >= double the acquisition slice thickness (' +
acquisitionSliceThickness + ' mm) for CT and MR.'
}
},
shortestDiameter: {
numericality: {
greaterThanOrEqualTo: Math.min(5, 2 * acquisitionSliceThickness),
message: '^New target lesions must be >= 5 mm short axis AND >= double the acquisition slice thickness (' +
acquisitionSliceThickness + ' mm) for CT and MR.'
}
}
};
} else {
criteria.followup.newLesions.target.all = {
// - New target lesions must be >/= 5 X 5 mm
longestDiameter: {
numericality: {
greaterThanOrEqualTo: 5,
message: '^New target lesions must be >= %{count} mm long axis.'
}
},
shortestDiameter: {
numericality: {
greaterThanOrEqualTo: 5,
message: '^New target lesions must be >= %{count} mm short axis.'
}
}
};
}
criteria.followup.group = {
numberOfLesionsPerOrgan: {
numericality: {
lessThanOrEqualTo: 5,
message: '^A maximum of %{count} target lesions per organ are allowed at Followup.'
}
},
totalNumberOfLesions: {
numericality: {
lessThanOrEqualTo: 10,
message: '^A maximum of %{count} target lesions total are allowed at Followup.'
}
}
};
// TODO: Check the actual requirement for total burden!
criteria.all.group = {
totalLesionBurden: {
numericality: {
greaterThanOrEqualTo: 100,
message: '^Total lesion burden (SPD target lesions + SPD new lesions) should be greater than %{count}.'
}
}
};
return criteria;
}
/**
* Retrieve trial criteria constraints based on the image that measurements appear upon
* If no image is specified, it is assumed that group or per Organ level criteria are desired.
*
* @param criteriaType A valid Trial Criteria set name (e.g. 'RECIST' or 'irRC')
* @param imageId A Cornerstone Image ID
* @returns {*} An Object of Trial Criteria that can be used to validate measurements' conformance
*/
getTrialCriteriaConstraints = function(criteriaType, imageId) {
if (!TrialCriteriaConstraints[criteriaType]) {
throw 'No such Trial Criteria defined: ' + criteriaType;
}
// If no imageId was specified, skip customization of the criteria
// and return the requested criteria right away
var criteria;
if (!imageId) {
criteria = TrialCriteriaConstraints[criteriaType]();
return criteria;
}
// Otherwise, retrieve the series metaData to identify the modality of the image
var seriesMetaData = cornerstoneTools.metaData.get('series', imageId);
// TODO: Get the rest of the metaData that has already been loaded by Cornerstone
var image = {};
// If we are looking at an MR or CT image, we should pass the slice thickness
// to the Trial Criteria functions so that they can customize the validation rules
if (seriesMetaData.modality === 'MR' || seriesMetaData.modality === 'CT') {
var instanceMetaData = cornerstoneTools.metaData.get('instance', imageId);
image.acquisitionSliceThickness = instanceMetaData.sliceThickness;
}
// Retrieve the study metaData in order to find the timepoint type
var studyMetaData = cornerstoneTools.metaData.get('study', imageId);
if (!studyMetaData) {
return;
}
// Retrieve the Study document from the Collection of associated Studies
var study = Studies.findOne({
studyInstanceUid: studyMetaData.studyInstanceUid
});
if (!study) {
log.warn('No study/timepoint association.');
return;
}
// Find the related Timepoint document
var timepoint = Timepoints.findOne({
timepointId: study.timepointId
});
if (!timepoint) {
log.warn('Timepoint related to study is missing.');
return;
}
// Retrieve the Timepoint's type (e.g. 'baseline' or 'followup')
var timepointType = timepoint.timepointType;
// Obtain the customized trial criteria given the image metaData
criteria = TrialCriteriaConstraints[criteriaType](image);
// Return the relevant criteria given the current timepoint type
return criteria[timepointType];
};

View File

@ -0,0 +1,343 @@
// Create a client-only Collection to store our Validation Errors
ValidationErrors = new Meteor.Collection(null);
// Set Validate.js Library's default options
validate.options = {
format: 'detailed'
};
/**
* Creates an array of validation error messages given an Object of validation errors
* and an optional prefix for the messages. An example of a useful prefix would be
* the location of the measurement or something like 'Target 1 '.
*
* @param validationErrors
* @param prefix
*/
function addValidationErrorsToCollection(validationErrors, prefix, type) {
// If no input was given, stop here
if (!validationErrors || !validationErrors.length) {
return;
}
// Loop through each of the entries in the validationErrors Array
validationErrors.forEach(function(validationError) {
var existingError = ValidationErrors.findOne({
attribute: validationError.attribute,
validator: validationError.validator,
error: validationError.error,
prefix: prefix
});
if (existingError) {
ValidationErrors.update(existingError._id, {
$set: {
value: validationError.value
}
});
} else {
validationError.type = type;
validationError.prefix = prefix;
ValidationErrors.insert(validationError);
}
});
}
/**
* Runs conformance checks related to a group of measurements. This function
* searches the input object of Constraints and looks for the 'group' attribute.
*
* It calculates some general group-level values for the current set of Measurements
* and validates these using the input constraints.
*
* @param constraints
* @returns {Array} Array of error messages related to the input conformance checks
*/
function assessGroupOfMeasurements(constraints) {
// Retrieve the group-level constraints
var groupConstraints = constraints.group;
// If no group-level constraints exist, stop here
if (!groupConstraints) {
return;
}
var type = 'group';
ValidationErrors.remove({
type: type
});
// Get the criteria type so we can calculate total lesion burden
var criteriaType = Session.get('TrialResponseAssessmentCriteria');
// Calculate some simple group-level Measurement statistics for validation
var testStructure = {
totalNumberOfLesions: Measurements.find().count(),
totalLesionBurden: calculateTotalLesionBurden(criteriaType)
};
// Run the conformance checks with the validate.js library
var validationErrors = validate(testStructure, groupConstraints);
// Return any error messages as a flattened array of errors
addValidationErrorsToCollection(validationErrors, '', type);
}
/**
* Runs conformance checks related to per-organ sets of measurements.
*
* This function searches the input object of Constraints and looks for the
* 'perOrgan' attribute.
*
* It calculates some general per-organ statistics for the current set of Measurements
* and validates these using the input constraints.
*
* @param constraints
* @returns {Array} Array of error messages related to the input conformance checks
*/
function assessMeasurementPerOrgan(constraints) {
// Retrieve the per-organ constraints
var perOrganConstraints = constraints.perOrgan;
// If no per-organ constraints exist, stop here
if (!perOrganConstraints) {
return;
}
// Create a list of all unique locations that contain measurements
// by looping through the Measurements Collection
var organLocations = [];
Measurements.find().forEach(function(measurement) {
if (organLocations.indexOf(measurement.location) > -1) {
return;
}
organLocations.push(measurement.location);
});
var type = 'perOrgan';
ValidationErrors.remove({
type: type
});
// Loop through each unique organ location in order to validate
// the per-organ constraints for each organ
organLocations.forEach(function(location) {
// Calculate the number of Lesions per Organ
var numberOfLesionsPerOrgan = Measurements.find({
location: location
}).count();
// Store per-organ Measurement statistics for validation
// Right now this is only the numberOfLesionsPerOrgan, but later
// this may include other checks
var testStructure = {
numberOfLesionsPerOrgan: numberOfLesionsPerOrgan
};
// Run the conformance checks with the validate.js library
var validationErrors = validate(testStructure, perOrganConstraints);
// Obtain any error messages as a flattened array of errors, prefixed
// with the Organ name, in the form 'Liver Left: '
addValidationErrorsToCollection(validationErrors, location + ': ', type);
});
}
/**
* Runs conformance checks on a single Measurement given the
* cornerstone toolData related to it.
*
* @param constraints
* @param measurementData CornerstoneTools toolData Object for this specific Measurement
* @returns {Array} Array of error messages related to the input conformance checks
*/
function assessSingleMeasurement(constraints, measurementData) {
// Check whether this is a Target or Non-Target Measurement
var targetType = measurementData.isTarget ? 'target' : 'nonTarget';
// Retrieve any target/non-target-specific single-measurement constraints
// from the input constraint structure
var measurementConstraints = constraints[targetType];
// If no relevant constraints exist, stop here
if (!measurementConstraints) {
return;
}
// Check whether this is a Nodal or Extranodal Measurement
var nodalType = measurementData.isNodal ? 'nodal' : 'extraNodal';
// Retrieve any nodal/extra-nodal-specific constraints to see if we can apply them
var constraintsToApply;
if (measurementData.isNodal !== undefined && measurementConstraints[nodalType]) {
// Check if we have enough information (about nodality of this Measurement,
// and nodality-specific constraints) to apply nodality-specific constraints
constraintsToApply = measurementConstraints[nodalType];
} else if (measurementConstraints.all) {
// If we have no data about the nodality of this Measurement, or no relevant
// specific constraints, we should apply the constraints valid for 'all' nodality
// types
constraintsToApply = measurementConstraints.all;
}
// Calculate a lesion name based on whether or not we have a Target or Non-target
// Measurement, and the lesion number of this Measurement.
var lesionName = measurementData.isTarget ? 'Target' : 'Non-target';
lesionName = lesionName + ' ' + measurementData.lesionNumber + ': ';
ValidationErrors.remove({
prefix: lesionName
});
// Use validate.js to check the criteria
var validationErrors = validate(measurementData, constraintsToApply);
if (validationErrors) {
validationErrors.forEach(function(error) {
error.measurementId = measurementData._id;
});
}
// Use the Lesion Name as a prefix to concatenate any validation error messages into
// an array to return
addValidationErrorsToCollection(validationErrors, lesionName);
}
/**
* Validate from a single Measurement up the chain to include group and perOrgan
* conformance checks
*
* @param measurementData The CornerstoneTools toolData for a single Measurement
*/
function validateSingleMeasurement(measurementData) {
// Obtain the name of the current TrialResponseAssessmentCriteria that
// we are using.
var criteriaType = Session.get('TrialResponseAssessmentCriteria');
var currentConstraints = getTrialCriteriaConstraints(criteriaType, measurementData.imageId);
if (!currentConstraints) {
log.warn('No relevant contraints could be applied');
return;
}
// Find the relevant Measurement in the Measurements Collection
var measurement = Measurements.findOne(measurementData.id);
// If no such Measurement exists, stop here
if (!measurement) {
log.warn('No Measurement found?');
return;
}
// Find the current timepointId that the user was editing the Measurement on
var timepointId = measurementData.timepointId;
// Find the specific measurement data for this Measurement at this Timepoint
var currentMeasurement = measurement.timepoints[timepointId];
// Include target and nodal flags on the timepoint-specific data so it is easier to validate
// TODO: Rethink what to pass to assessSingleMeasurement?
currentMeasurement.isTarget = measurement.isTarget;
currentMeasurement.isNodal = measurement.isNodal;
currentMeasurement.lesionNumber = measurement.lesionNumber;
currentMeasurement._id = measurement._id;
// Run the single-measurement-specific conformance checks
// If any messages exist, add them to the array of messages
assessSingleMeasurement(currentConstraints, currentMeasurement);
validateGroups();
}
function validateGroups() {
// Obtain the name of the current TrialResponseAssessmentCriteria that
// we are using.
var criteriaType = Session.get('TrialResponseAssessmentCriteria');
Timepoints.find().forEach(function(timepoint) {
// TODO: Criteria for the specific image are retrieved from the general set of criteria.
// - The acquisitionSliceThickness, for example, may be pulled from the image metadata
// - The organ in question, e.g. Chest X-ray, may determine the exact specifications for the current trial criteria
var currentConstraints = getTrialCriteriaConstraints(criteriaType);
// Retrieve the current constraints which apply to the specific Timepoint type
// (e.g. baseline, followup) that this Measurement is being edited on.
var timepointConstraints = currentConstraints[timepoint.timepointType];
if (!timepointConstraints) {
return;
}
// Run the group-level conformance checks
assessGroupOfMeasurements(timepointConstraints);
// Run the per-organ conformance checks
assessMeasurementPerOrgan(timepointConstraints);
});
}
function validateAll() {
// Obtain the name of the current TrialResponseAssessmentCriteria that
// we are using.
var criteriaType = Session.get('TrialResponseAssessmentCriteria');
Measurements.find().forEach(function(measurement) {
Object.keys(measurement.timepoints).forEach(function(timepointId) {
var currentMeasurement = measurement.timepoints[timepointId];
currentMeasurement.isTarget = measurement.isTarget;
currentMeasurement.isNodal = measurement.isNodal;
currentMeasurement.lesionNumber = measurement.lesionNumber;
currentMeasurement._id = measurement._id;
// TODO: Criteria for the specific image are retrieved from the general set of criteria.
// - The acquisitionSliceThickness, for example, may be pulled from the image metadata
// - The organ in question, e.g. Chest X-ray, may determine the exact specifications for the current trial criteria
var currentConstraints = getTrialCriteriaConstraints(criteriaType, currentMeasurement.imageId);
// Run the single-measurement-specific conformance checks
// If any messages exist, add them to the array of messages
assessSingleMeasurement(currentConstraints, currentMeasurement);
});
});
validateGroups();
}
var validationTimeout = 400;
/**
* Validate the measurements after a set delay period
*
* @param measurementData Input measurement data from CornerstoneTools
*/
function validateDelayed(measurementData) {
// Erase any currently-waiting validation call
clearTimeout(validationTimeout);
// Set a timeout to run validation after a delay
// Currently this is 400 milliseconds
setTimeout(function() {
validateSingleMeasurement(measurementData);
}, validationTimeout);
}
/**
* Validate all measurements after a set delay period
*/
function validateAllDelayed() {
// Erase any currently-waiting validation call
clearTimeout(validationTimeout);
// Set a timeout to run validation after a delay
// Currently this is 400 milliseconds
setTimeout(function() {
validateAll();
}, validationTimeout);
}
TrialResponseCriteria = {
validateAll: validateAll,
validateAllDelayed: validateAllDelayed,
validateSingleMeasurement: validateSingleMeasurement,
validateDelayed: validateDelayed,
validateGroups: validateGroups
};

View File

@ -12,7 +12,7 @@ activateMeasurements = function(element, measurementId, templateData, viewportIn
var timepointData = getTimepointObject(imageId); var timepointData = getTimepointObject(imageId);
var measurementData = Measurements.findOne(measurementId); var measurementData = Measurements.findOne(measurementId);
var measurementAtTimepoint = measurementData.timepoints[timepointData.timepointID]; var measurementAtTimepoint = measurementData.timepoints[timepointData.timepointId];
if (!measurementAtTimepoint) { if (!measurementAtTimepoint) {
return; return;
} }
@ -35,11 +35,11 @@ activateMeasurements = function(element, measurementId, templateData, viewportIn
} }
if (imageIdIndex === elementCurrentImageIdIndex) { if (imageIdIndex === elementCurrentImageIdIndex) {
activateTool(element, measurementData, timepointData.timepointID); activateTool(element, measurementData, timepointData.timepointId);
} else { } else {
cornerstone.loadAndCacheImage(imageIds[imageIdIndex]).then(function(image) { cornerstone.loadAndCacheImage(imageIds[imageIdIndex]).then(function(image) {
cornerstone.displayImage(element, image); cornerstone.displayImage(element, image);
activateTool(element, measurementData, timepointData.timepointID); activateTool(element, measurementData, timepointData.timepointId);
}); });
} }
}; };
@ -50,9 +50,9 @@ activateMeasurements = function(element, measurementId, templateData, viewportIn
* *
* @param element * @param element
* @param measurementData * @param measurementData
* @param timepointID * @param timepointId
*/ */
function activateTool(element, measurementData, timepointID) { function activateTool(element, measurementData, timepointId) {
deactivateAllToolData(element, 'lesion'); deactivateAllToolData(element, 'lesion');
deactivateAllToolData(element, 'nonTarget'); deactivateAllToolData(element, 'nonTarget');
@ -62,7 +62,7 @@ function activateTool(element, measurementData, timepointID) {
return; return;
} }
var measurementAtTimepoint = measurementData.timepoints[timepointID]; var measurementAtTimepoint = measurementData.timepoints[timepointId];
for (var i = 0; i < toolData.data.length; i++) { for (var i = 0; i < toolData.data.length; i++) {
data = toolData.data[i]; data = toolData.data[i];

View File

@ -0,0 +1,59 @@
/**
* Calculates total lesion burden given the Trial Criteria Type.
* Supports RECIST 1.1 and irRC at present, defaults to RECIST.
*
* @param criteriaType Either 'RECIST' or 'irRC'
* @returns {*}
*/
calculateTotalLesionBurden = function(criteriaType) {
var totalBurden;
var measurements = Measurements.find({
isTarget: true
});
switch (criteriaType) {
default:
case 'RECIST':
// - Time Point Measurement Total =
// Sum of long axis measurements for extranodal target lesion +
// short axis measurements for nodal lesions
var sumLongAxisExtranodal = 0,
sumShortAxisNodal = 0;
measurements.forEach(function(measurement) {
var LD = parseFloat(measurement.longestDiameter);
var SD = parseFloat(measurement.longestDiameter);
if (measurement.nodal === true) {
sumShortAxisNodal += SD;
} else {
sumLongAxisExtranodal += LD;
}
});
totalBurden = sumLongAxisExtranodal + sumShortAxisNodal;
break;
case 'irRC':
// - Time Point Measurement Total = SPD target lesions + SPD new lesions
// (SPD = sum of product of long axis and short axis diameters)
var sumProductLesions = 0,
sumProductNewLesions = 0;
measurements.forEach(function(measurement) {
var LD = parseFloat(measurement.longestDiameter);
var SD = parseFloat(measurement.shortestDiameter);
var product = LD * SD;
if (measurement.newLesion === true) {
sumProductNewLesions += product;
} else {
sumProductLesions += product;
}
});
totalBurden = sumProductLesions + sumProductNewLesions;
break;
}
return totalBurden;
};

View File

@ -19,12 +19,11 @@ clearMeasurementTimepointData = function(measurementId, timepointId) {
delete data.timepoints[timepointId]; delete data.timepoints[timepointId];
if (Object.keys(data.timepoints).length === 0) { if (!Object.keys(data.timepoints).length) {
Meteor.call('removeMeasurement', measurementId, function(error, response) { Meteor.call('removeMeasurement', measurementId, function(error, response) {
if (error) { if (error) {
log.warn(error); log.warn(error);
} }
console.log('Removed!');
}); });
} else { } else {
// Update the Timepoint object of the Measurement document // Update the Timepoint object of the Measurement document

View File

@ -35,4 +35,7 @@ clearTools = function() {
// Remove patient's measurements // Remove patient's measurements
Meteor.call('removeMeasurementsByPatientId', patientId); Meteor.call('removeMeasurementsByPatientId', patientId);
// Clear all validation errors
ValidationErrors.remove({});
}; };

View File

@ -0,0 +1,44 @@
/**
* Calculates a Timepoint's name based on how many timepoints exist between it
* and the latest Baseline. Names returned are in the form of 'Baseline', or
* 'Follow-up 1', 'Follow-up 2', and so on.
*
* @param timepoint
* @returns {*} The timepoint name
*/
getTimepointName = function(timepoint) {
// Check if this is a Baseline timepoint, if it is, return 'Baseline'
if (timepoint.timepointType === 'baseline') {
return 'Baseline';
}
// Retrieve all of the relevant follow-up timepoints for this patient
var followupTimepoints = Timepoints.find({
patientId: timepoint.patientId,
timepointType: timepoint.timepointType
}, {
sort: {
latestDate: 1
}
});
// Create an array of just timepointIds, so we can use indexOf
// on it to find the current timepoint's relative position
var followupTimepointIds = followupTimepoints.map(function(timepoint) {
return timepoint.timepointId;
});
// Calculate the index of the current timepoint in the array of all
// relevant follow-up timepoints
var index = followupTimepointIds.indexOf(timepoint.timepointId) + 1;
// If index is 0, it means that the current timepoint was not in the list
// Log a warning and return here
if (!index) {
log.warn('Current follow-up was not in the list of relevant follow-ups?');
return;
}
// Return the timepoint name as 'Follow-up N'
return 'Follow-up ' + index;
};

View File

@ -11,6 +11,8 @@ getTimepointObject = function(imageId) {
} }
return Timepoints.findOne({ return Timepoints.findOne({
timepointName: study.studyDate studyInstanceUids: {
$in: [study.studyInstanceUid]
}
}); });
}; };

View File

@ -0,0 +1,12 @@
handleMeasurementAdded = function(e, eventData) {
log.info('CornerstoneToolsMeasurementAdded');
var measurementData = eventData.measurementData;
switch (eventData.toolType) {
case 'nonTarget':
case 'lesion':
LesionManager.updateLesionData(measurementData);
TrialResponseCriteria.validateDelayed(measurementData);
break;
}
};

View File

@ -0,0 +1,12 @@
handleMeasurementModified = function(e, eventData) {
log.info('CornerstoneToolsMeasurementModified');
var measurementData = eventData.measurementData;
switch (eventData.toolType) {
case 'nonTarget':
case 'lesion':
LesionManager.updateLesionData(measurementData);
TrialResponseCriteria.validateDelayed(measurementData);
break;
}
};

View File

@ -0,0 +1,19 @@
handleMeasurementRemoved = function(e, eventData) {
log.info('CornerstoneToolsMeasurementRemoved');
var measurementData = eventData.measurementData;
switch (eventData.toolType) {
case 'nonTarget':
case 'lesion':
var measurement = Measurements.findOne(measurementData.id, {
reactive: false
});
if (!measurement) {
return;
}
clearMeasurementTimepointData(measurement._id, measurementData.timepointId);
break;
}
};

View File

@ -0,0 +1,30 @@
pixelSpacingAutorunCheck = function() {
log.info('lesionTool button change autorun');
/*if (!Session.get('ViewerData')) {
return;
}*/
// Get oncology tools
var oncologyTools = $('button#lesion, button#nonTarget');
// TODO: Set activeViewport for empty viewport element
var activeViewportIndex = Session.get('activeViewport');
if (activeViewportIndex === undefined) {
return;
}
var element = $('.imageViewerViewport').get(activeViewportIndex);
var enabledElement = cornerstone.getEnabledElement(element);
// Check value of rowPixelSpacing & columnPixelSpacing to define as unavailable
if (!enabledElement ||
!enabledElement.image ||
!enabledElement.image.rowPixelSpacing ||
!enabledElement.image.columnPixelSpacing) {
// Disable Lesion Buttons
oncologyTools.prop('disabled', true);
} else {
// Enable Lesion Buttons
oncologyTools.prop('disabled', false);
}
};

View File

@ -0,0 +1,75 @@
syncMeasurementAndToolData = function(data) {
// Check what toolType we should be adding this to, based on the isTarget value
// of the stored Measurement
var toolType = data.isTarget ? 'lesion' : 'nonTarget';
var toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.toolState;
// Loop through the timepoint data for this measurement
Object.keys(data.timepoints).forEach(function(key) {
var storedData = data.timepoints[key];
var imageId = storedData.imageId;
if (!toolState[imageId]) {
toolState[imageId] = {};
}
// This is probably not the best approach to prevent duplicates
if (toolState[imageId][toolType] && toolState[imageId][toolType].data) {
var measurementHasNoIdYet = false;
toolState[imageId][toolType].data.forEach(function(measurement) {
if (measurement.id === 'notready') {
measurementHasNoIdYet = true;
return false;
}
});
// Stop here if it appears that we are creating this measurement right now,
// and would not like this function to add another copy of it to the toolData
if (measurementHasNoIdYet === true) {
return;
}
}
if (!toolState[imageId][toolType]) {
toolState[imageId][toolType] = {
data: []
};
} else {
var alreadyExists = false;
if (toolState[imageId][toolType].data.length) {
toolState[imageId][toolType].data.forEach(function(measurement) {
if (measurement.id === data._id) {
alreadyExists = true;
// Update the toolData lesionNumber from the Measurement
measurement.lesionNumber = data.lesionNumber;
return false;
}
});
}
if (alreadyExists === true) {
return;
}
}
// Create measurementData structure based on the lesion data at this timepoint
// We will add this into the toolData for this imageId
var measurementData = storedData;
measurementData.isTarget = data.isTarget;
measurementData.lesionNumber = data.lesionNumber;
measurementData.measurementText = data.measurementText;
measurementData.isDeleted = data.isDeleted;
measurementData.location = data.location;
measurementData.locationUID = data.locationUID;
measurementData.patientId = patientId;
measurementData.visible = data.visible;
measurementData.active = data.active;
measurementData.uid = data.uid;
measurementData.id = data._id;
toolState[imageId][toolType].data.push(measurementData);
TrialResponseCriteria.validateSingleMeasurement(measurementData);
});
};

View File

@ -0,0 +1,13 @@
updateRelatedElements = function(imageId) {
// Get all on-screen elements with this imageId
var enabledElements = cornerstone.getEnabledElementsByImageId(imageId);
// TODO=Check original event to prevent duplicate updateImage calls
// Loop through these elements
enabledElements.forEach(function(enabledElement) {
// Update the display so the tool is removed
var element = enabledElement.element;
cornerstone.updateImage(element);
});
};

View File

@ -1,250 +0,0 @@
// 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);

View File

@ -0,0 +1,108 @@
/**
* Opens a new tab in the tabbed worklist environment using
* a given timepoint and new tab title.
*
* @param timepointId The UID of the Timepoint to be opened
* @param title The title to be used for the tab heading
*/
openNewTabWithTimepoint = function(timepointId, title) {
log.info('openNewTabWithTimepoint: ' + timepointId + ' ' + 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 = uuid.new();
var timepoint = Timepoints.findOne({
timepointId: timepointId
});
if (!timepoint) {
throw 'No such timepoint exists';
}
// Get the relevant studyInstanceUids given the timepoints
var data = getDataFromTimepoint(timepoint);
if (!data.studyInstanceUids) {
throw 'No studies found that are related to this timepoint';
}
// Create a new entry in the WorklistTabs Collection
WorklistTabs.insert({
title: title,
contentid: contentid,
active: false,
timepointId: timepointId
});
// Update the ViewerData global object
ViewerData[contentid] = {
title: title,
contentid: contentid,
studyInstanceUids: data.studyInstanceUids,
timepointIds: data.timepointIds
};
// Switch to the new tab
switchToTab(contentid);
};
/**
* Retrieves related studies given a Baseline or Follow-up Timepoint
*
* @param timepoint
* @returns {Array}
*/
function getDataFromTimepoint(timepoint) {
var relatedStudies = [];
// Include the specified studyInstanceUids
// NOTE: Temporarily added [0], since we only need one study per timepoint to load immediately?
relatedStudies = relatedStudies.concat(timepoint.studyInstanceUids[0]);
// If this is the baseline, we should stop here and return the relevant studies
if (isBaseline(timepoint)) {
return {
studyInstanceUids: relatedStudies,
timepointIds: [timepoint.timepointId]
};
}
// Otherwise, this is a follow-up exam, so we should also find the baseline timepoint,
// and all studies related to it. We also enforce that the Baseline should have a studyDate
// prior to the latest studyDate in the current (Follow-up) Timepoint.
var baseline = Timepoints.findOne({
timepointType: 'baseline',
patientId: timepoint.patientId,
latestDate: {
$lte: timepoint.latestDate
}
});
var timepointIds = [];
if (baseline) {
// NOTE: Temporarily added [0], since we only need one study per timepoint to load immediately?
relatedStudies = relatedStudies.concat(baseline.studyInstanceUids[0]);
timepointIds.push(baseline.timepointId);
} else {
log.warn('No Baseline found while opening a Follow-up Timepoint');
}
timepointIds.push(timepoint.timepointId);
return {
studyInstanceUids: relatedStudies,
timepointIds: timepointIds
};
}
/**
* Checks if a Timepoints is a baseline or not
* (abstracting this for later use, since I expect it to get more complex)
*
* @param timepoint a document from the Timepoints Collection
* @returns {boolean} Whether or not the timepoint is stored as a Baseline
*/
function isBaseline(timepoint) {
return (timepoint.timepointType === 'baseline');
}

View File

@ -0,0 +1,37 @@
Meteor.startup(function() {
Worklist.subscriptions = ['studies', 'timepoints'];
Worklist.callbacks['dblClickOnStudy'] = dblClickOnStudy;
Worklist.callbacks['middleClickOnStudy'] = dblClickOnStudy;
});
/**
* Lesion Tracker method including Timepoints / other studies
*/
function dblClickOnStudy(data) {
// Use the formatPN template helper to clean up the patient name
var title = formatPN(data.patientName);
var study = Studies.findOne({
studyInstanceUid: data.studyInstanceUid
});
// Check if the study has been associated, and if not, just open it on its own
if (!study) {
// Open a new tab with this study
openNewTab(data.studyInstanceUid, title);
return;
}
// Find the relevant timepoint given the clicked-on study
var timepoint = Timepoints.findOne({
studyInstanceUids: {
$in: [data.studyInstanceUid]
}
});
if (!timepoint) {
openNewTab(data.studyInstanceUid, title);
return;
}
openNewTabWithTimepoint(timepoint.timepointId, title);
}

View File

@ -10,16 +10,28 @@ Package.onUse(function(api) {
api.use('standard-app-packages'); api.use('standard-app-packages');
api.use('jquery'); api.use('jquery');
api.use('stylus'); api.use('stylus');
// Control over logging
api.use('practicalmeteor:loglevel'); api.use('practicalmeteor:loglevel');
// Unique IDs
api.use('rwatts:uuid');
// Template overriding
api.use('aldeed:template-extension@4.0.0');
// Our custom package // Our custom package
api.use('worklist');
api.use('cornerstone'); api.use('cornerstone');
api.addFiles('log.js', [ 'client', 'server' ]); api.addFiles('log.js', [ 'client', 'server' ]);
// Client-side collections
api.addFiles('client/collections/LesionLocations.js', 'client'); api.addFiles('client/collections/LesionLocations.js', 'client');
api.addFiles('client/collections/LocationResponses.js', 'client'); api.addFiles('client/collections/LocationResponses.js', 'client');
api.addFiles('client/collections/PatientLocations.js', 'client');
// Additional Custom Cornerstone Tools for Lesion Tracker
api.addFiles('client/compatibility/lesionTool.js', 'client', { api.addFiles('client/compatibility/lesionTool.js', 'client', {
bare: true bare: true
}); });
@ -32,14 +44,27 @@ Package.onUse(function(api) {
api.addFiles('client/compatibility/deleteLesionKeyboardTool.js', 'client', { api.addFiles('client/compatibility/deleteLesionKeyboardTool.js', 'client', {
bare: true bare: true
}); });
api.addFiles('client/compatibility/LesionManager.js', 'client', {
// Trial Criteria data validation (the Meteor package is currently out-of-date)
api.addFiles('client/compatibility/validate.js', 'client', {
bare: true bare: true
}); });
// UI Components
api.addFiles('client/components/lesionTrackerLayout/lesionTrackerLayout.html', 'client');
api.addFiles('client/components/lesionTrackerLayout/lesionTrackerLayout.styl', 'client');
api.addFiles('client/components/associationModal/associationModal.html', 'client'); api.addFiles('client/components/associationModal/associationModal.html', 'client');
api.addFiles('client/components/associationModal/associationModal.styl', 'client'); api.addFiles('client/components/associationModal/associationModal.styl', 'client');
api.addFiles('client/components/associationModal/associationModal.js', 'client'); api.addFiles('client/components/associationModal/associationModal.js', 'client');
api.addFiles('client/components/optionsButton/optionsButton.html', 'client');
api.addFiles('client/components/optionsButton/optionsButton.styl', 'client');
api.addFiles('client/components/optionsModal/optionsModal.html', 'client');
api.addFiles('client/components/optionsModal/optionsModal.styl', 'client');
api.addFiles('client/components/optionsModal/optionsModal.js', 'client');
api.addFiles('client/components/lesionLocationDialog/lesionLocationDialog.html', 'client'); api.addFiles('client/components/lesionLocationDialog/lesionLocationDialog.html', 'client');
api.addFiles('client/components/lesionLocationDialog/lesionLocationDialog.js', 'client'); api.addFiles('client/components/lesionLocationDialog/lesionLocationDialog.js', 'client');
api.addFiles('client/components/lesionLocationDialog/lesionLocationDialog.styl', 'client'); api.addFiles('client/components/lesionLocationDialog/lesionLocationDialog.styl', 'client');
@ -56,8 +81,8 @@ Package.onUse(function(api) {
api.addFiles('client/components/lesionTableTimepointCell/lesionTableTimepointCell.js', 'client'); api.addFiles('client/components/lesionTableTimepointCell/lesionTableTimepointCell.js', 'client');
api.addFiles('client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.html', 'client'); api.addFiles('client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.html', 'client');
api.addFiles('client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.js', 'client');
api.addFiles('client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.styl', 'client'); api.addFiles('client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.styl', 'client');
api.addFiles('client/components/lesionTableTimepointHeader/lesionTableTimepointHeader.js', 'client');
api.addFiles('client/components/nonTargetLesionDialog/nonTargetLesionDialog.html', 'client'); api.addFiles('client/components/nonTargetLesionDialog/nonTargetLesionDialog.html', 'client');
api.addFiles('client/components/nonTargetLesionDialog/nonTargetLesionDialog.styl', 'client'); api.addFiles('client/components/nonTargetLesionDialog/nonTargetLesionDialog.styl', 'client');
@ -75,6 +100,10 @@ Package.onUse(function(api) {
api.addFiles('client/components/confirmDeleteDialog/confirmDeleteDialog.styl', 'client'); api.addFiles('client/components/confirmDeleteDialog/confirmDeleteDialog.styl', 'client');
api.addFiles('client/components/confirmDeleteDialog/confirmDeleteDialog.js', 'client'); api.addFiles('client/components/confirmDeleteDialog/confirmDeleteDialog.js', 'client');
api.addFiles('client/components/conformanceCheckFeedback/conformanceCheckFeedback.html', 'client');
api.addFiles('client/components/conformanceCheckFeedback/conformanceCheckFeedback.styl', 'client');
api.addFiles('client/components/conformanceCheckFeedback/conformanceCheckFeedback.js', 'client');
api.addFiles('client/components/nonTargetResponseDialog/nonTargetResponseDialog.html', 'client'); api.addFiles('client/components/nonTargetResponseDialog/nonTargetResponseDialog.html', 'client');
api.addFiles('client/components/nonTargetResponseDialog/nonTargetResponseDialog.styl', 'client'); api.addFiles('client/components/nonTargetResponseDialog/nonTargetResponseDialog.styl', 'client');
api.addFiles('client/components/nonTargetResponseDialog/nonTargetResponseDialog.js', 'client'); api.addFiles('client/components/nonTargetResponseDialog/nonTargetResponseDialog.js', 'client');
@ -82,6 +111,17 @@ Package.onUse(function(api) {
api.addFiles('client/components/timepointTextDialog/timepointTextDialog.html', 'client'); api.addFiles('client/components/timepointTextDialog/timepointTextDialog.html', 'client');
api.addFiles('client/components/timepointTextDialog/timepointTextDialog.styl', 'client'); api.addFiles('client/components/timepointTextDialog/timepointTextDialog.styl', 'client');
api.addFiles('client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.html', 'client');
api.addFiles('client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.styl', 'client');
api.addFiles('client/components/lesionTrackerWorklistStudy/lesionTrackerWorklistStudy.js', 'client');
api.addFiles('client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.html', 'client');
api.addFiles('client/components/lesionTrackerWorklistContextMenu/lesionTrackerWorklistContextMenu.js', 'client');
api.addFiles('client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.html', 'client');
api.addFiles('client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.styl', 'client');
api.addFiles('client/components/lesionTrackerViewportOverlay/lesionTrackerViewportOverlay.js', 'client');
// Server functions // Server functions
api.addFiles('server/collections.js', 'server'); api.addFiles('server/collections.js', 'server');
api.addFiles('server/removeCollections.js', [ 'server' ]); api.addFiles('server/removeCollections.js', [ 'server' ]);
@ -89,34 +129,67 @@ Package.onUse(function(api) {
// Both client and server functions // Both client and server functions
api.addFiles('both/collections.js', [ 'client', 'server' ]); api.addFiles('both/collections.js', [ 'client', 'server' ]);
// Worklist-related functions
api.addFiles('lib/worklist/openNewTabWithTimepoint.js', 'client');
api.addFiles('lib/worklist/worklistModification.js', 'client');
// Library functions // Library functions
api.addFiles('lib/uuid.js', 'client'); api.addFiles('lib/TrialCriteriaConstraints.js', 'client');
api.addFiles('lib/TrialResponseCriteria.js', 'client');
api.addFiles('lib/LesionManager.js', 'client');
api.addFiles('lib/pixelSpacingAutorunCheck.js', 'client');
api.addFiles('lib/toggleLesionTrackerTools.js', 'client'); api.addFiles('lib/toggleLesionTrackerTools.js', 'client');
api.addFiles('lib/clearMeasurementTimepointData.js', 'client'); api.addFiles('lib/clearMeasurementTimepointData.js', 'client');
api.addFiles('lib/removeToolDataWithMeasurementId.js', 'client'); api.addFiles('lib/removeToolDataWithMeasurementId.js', 'client');
api.addFiles('lib/getTimepointObject.js', 'client'); api.addFiles('lib/getTimepointObject.js', 'client');
api.addFiles('lib/getTimepointName.js', 'client');
api.addFiles('lib/activateMeasurements.js', 'client'); api.addFiles('lib/activateMeasurements.js', 'client');
api.addFiles('lib/activateLesion.js', 'client'); api.addFiles('lib/activateLesion.js', 'client');
api.addFiles('lib/deactivateAllToolData.js', 'client'); api.addFiles('lib/deactivateAllToolData.js', 'client');
api.addFiles('lib/clearTools.js', 'client'); api.addFiles('lib/clearTools.js', 'client');
api.addFiles('lib/calculateTotalLesionBurden.js', 'client');
api.addFiles('lib/syncMeasurementAndToolData.js', 'client');
api.addFiles('lib/updateRelatedElements.js', 'client');
api.addFiles('lib/handleMeasurementAdded.js', 'client');
api.addFiles('lib/handleMeasurementModified.js', 'client');
api.addFiles('lib/handleMeasurementRemoved.js', 'client');
// Export global functions // Export global functions
api.export('activateLesion','client'); api.export('pixelSpacingAutorunCheck', 'client');
api.export('activateMeasurements','client'); api.export('handleMeasurementAdded', 'client');
api.export('deactivateAllToolData','client'); api.export('handleMeasurementModified', 'client');
api.export('handleMeasurementRemoved', 'client');
api.export('syncMeasurementAndToolData', 'client');
api.export('updateRelatedElements', 'client');
api.export('openNewTabWithTimepoint', 'client');
api.export('activateLesion', 'client');
api.export('activateMeasurements', 'client');
api.export('deactivateAllToolData', 'client');
api.export('toggleLesionTrackerTools', 'client'); api.export('toggleLesionTrackerTools', 'client');
api.export('clearMeasurementTimepointData', 'client'); api.export('clearMeasurementTimepointData', 'client');
api.export('removeToolDataWithMeasurementId', 'client'); api.export('removeToolDataWithMeasurementId', 'client');
api.export('getTimepointObject', 'client'); api.export('getTimepointObject', 'client');
api.export('clearTools', 'client'); api.export('clearTools', 'client');
api.export('getTimepointName', 'client');
api.export('getTrialCriteriaConstraints', 'client');
api.export('calculateTotalLesionBurden', 'client');
// Export global objects
api.export('TrialResponseCriteria', 'client');
api.export('TrialCriteriaConstraints', 'client');
api.export('LesionManager', 'client'); api.export('LesionManager', 'client');
// Export client-side collections // Export client-side collections
api.export('ValidationErrors', 'client');
api.export('LesionLocations', 'client'); api.export('LesionLocations', 'client');
api.export('LocationResponses', 'client'); api.export('LocationResponses', 'client');
api.export('PatientLocations', 'client'); api.export('PatientLocations', 'client');
// Export collections spanning both client and server // Export collections spanning both client and server
api.export('Measurements', [ 'client', 'server' ]); api.export('Measurements', [ 'client', 'server' ]);
api.export('Studies', [ 'client', 'server' ]);
api.export('Timepoints', [ 'client', 'server' ]); api.export('Timepoints', [ 'client', 'server' ]);
}); });

View File

@ -1,10 +1,18 @@
Meteor.publish('timepoints', function(patientId) { Meteor.publish('timepoints', function() {
return Timepoints.find();
});
Meteor.publish('singlePatientTimepoints', function(patientId) {
return Timepoints.find({ return Timepoints.find({
patientId: patientId patientId: patientId
}); });
}); });
Meteor.publish('measurements', function(patientId) { Meteor.publish('studies', function() {
return Studies.find();
});
Meteor.publish('singlePatientMeasurements', function(patientId) {
return Measurements.find({ return Measurements.find({
patientId: patientId patientId: patientId
}); });

View File

@ -43,5 +43,11 @@ Meteor.methods({
Measurements.remove({ Measurements.remove({
patientId: patientId patientId: patientId
}); });
},
clearAllTimepoints: function() {
Timepoints.remove({});
},
removeTimepoint: function(id) {
Timepoints.remove(id);
} }
}); });

View File

@ -10,7 +10,7 @@ function getElementIfNotEmpty(viewportIndex) {
element = imageViewerViewports.get(viewportIndex), element = imageViewerViewports.get(viewportIndex),
canvases = imageViewerViewports.eq(viewportIndex).find('canvas'); canvases = imageViewerViewports.eq(viewportIndex).find('canvas');
if (!element || $(element).hasClass("empty") || canvases.length === 0) { if (!element || $(element).hasClass('empty') || canvases.length === 0) {
return; return;
} }
@ -20,6 +20,7 @@ function getElementIfNotEmpty(viewportIndex) {
} catch(error) { } catch(error) {
return; return;
} }
return element; return element;
} }
@ -28,10 +29,12 @@ function getPatient(property) {
if (!this.imageId) { if (!this.imageId) {
return false; return false;
} }
var patient = cornerstoneTools.metaData.get('patient', this.imageId); var patient = cornerstoneTools.metaData.get('patient', this.imageId);
if (!patient) { if (!patient) {
return ''; return '';
} }
return patient[property]; return patient[property];
} }
@ -40,10 +43,12 @@ function getStudy(property) {
if (!this.imageId) { if (!this.imageId) {
return false; return false;
} }
var study = cornerstoneTools.metaData.get('study', this.imageId); var study = cornerstoneTools.metaData.get('study', this.imageId);
if (!study) { if (!study) {
return ''; return '';
} }
return study[property]; return study[property];
} }
@ -52,10 +57,12 @@ function getSeries(property) {
if (!this.imageId) { if (!this.imageId) {
return false; return false;
} }
var series = cornerstoneTools.metaData.get('series', this.imageId); var series = cornerstoneTools.metaData.get('series', this.imageId);
if (!series) { if (!series) {
return ''; return '';
} }
return series[property]; return series[property];
} }
@ -64,10 +71,12 @@ function getInstance(property) {
if (!this.imageId) { if (!this.imageId) {
return false; return false;
} }
var instance = cornerstoneTools.metaData.get('instance', this.imageId); var instance = cornerstoneTools.metaData.get('instance', this.imageId);
if (!instance) { if (!instance) {
return ''; return '';
} }
return instance[property]; return instance[property];
} }
@ -76,15 +85,18 @@ function getImage(viewportIndex) {
if (!element) { if (!element) {
return false; return false;
} }
var enabledElement; var enabledElement;
try { try {
enabledElement = cornerstone.getEnabledElement(element); enabledElement = cornerstone.getEnabledElement(element);
} catch(error) { } catch(error) {
return false; return false;
} }
if (!enabledElement || !enabledElement.image) { if (!enabledElement || !enabledElement.image) {
return false; return false;
} }
return enabledElement.image; return enabledElement.image;
} }
@ -95,10 +107,12 @@ Template.viewportOverlay.helpers({
if (!element) { if (!element) {
return ''; return '';
} }
var viewport = cornerstone.getViewport(element); var viewport = cornerstone.getViewport(element);
if (!viewport) { if (!viewport) {
return ''; return '';
} }
return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0); return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
}, },
zoom: function() { zoom: function() {
@ -107,10 +121,12 @@ Template.viewportOverlay.helpers({
if (!element) { if (!element) {
return ''; return '';
} }
var viewport = cornerstone.getViewport(element); var viewport = cornerstone.getViewport(element);
if (!viewport) { if (!viewport) {
return ''; return '';
} }
return (viewport.scale * 100.0); return (viewport.scale * 100.0);
}, },
imageDimensions: function() { imageDimensions: function() {
@ -120,39 +136,40 @@ Template.viewportOverlay.helpers({
if (!image) { if (!image) {
return ''; return '';
} }
return image.width + ' x ' + image.height; return image.width + ' x ' + image.height;
}, },
patientName : function() { patientName: function() {
return getPatient.call(this, 'name'); return getPatient.call(this, 'name');
}, },
patientId : function() { patientId: function() {
return getPatient.call(this, 'id'); return getPatient.call(this, 'id');
}, },
studyDate : function() { studyDate: function() {
return getStudy.call(this, 'studyDate'); return getStudy.call(this, 'studyDate');
}, },
studyTime : function() { studyTime: function() {
return getStudy.call(this, 'studyTime'); return getStudy.call(this, 'studyTime');
}, },
studyDescription : function() { studyDescription: function() {
return getStudy.call(this, 'studyDescription'); return getStudy.call(this, 'studyDescription');
}, },
seriesDescription : function() { seriesDescription: function() {
return getSeries.call(this, 'seriesDescription'); return getSeries.call(this, 'seriesDescription');
}, },
seriesNumber : function() { seriesNumber: function() {
return getSeries.call(this, 'seriesNumber'); return getSeries.call(this, 'seriesNumber');
}, },
imageNumber : function() { imageNumber: function() {
return getInstance.call(this, 'instanceNumber'); return getInstance.call(this, 'instanceNumber');
}, },
imageIndex : function() { imageIndex: function() {
return getInstance.call(this, 'index'); return getInstance.call(this, 'index');
}, },
numImages : function() { numImages: function() {
return getSeries.call(this, 'numImages'); return getSeries.call(this, 'numImages');
}, },
prior : function() { prior: function() {
// This helper is updated whenever a new image is displayed in the viewport // This helper is updated whenever a new image is displayed in the viewport
Session.get('CornerstoneNewImage' + this.viewportIndex); Session.get('CornerstoneNewImage' + this.viewportIndex);
if (!this.imageId) { if (!this.imageId) {
@ -165,14 +182,18 @@ Template.viewportOverlay.helpers({
// that we can obtain the oldest study as the first element of the array // that we can obtain the oldest study as the first element of the array
// //
// TODO= Find out if we should encode studyDate as a Date in the ViewerStudies Collection // TODO= Find out if we should encode studyDate as a Date in the ViewerStudies Collection
var viewportStudies = ViewerStudies.find({}, {sort: {studyDate: 1}}); var viewportStudies = ViewerStudies.find({}, {
sort: {
studyDate: 1
}
});
if (viewportStudies.count() < 2) { if (viewportStudies.count() < 2) {
return; return;
} }
// Get study data // Get study data
var study = cornerstoneTools.metaData.get('study', this.imageId); var study = cornerstoneTools.metaData.get('study', this.imageId);
if (study.studyInstanceUid === viewportStudies.fetch()[0].studyInstanceUid) { if (study.studyDate === viewportStudies.fetch()[0].studyDate) {
return 'Prior'; return 'Prior';
} }
} }

View File

@ -1,6 +1,7 @@
.imageViewerViewportOverlay .imageViewerViewportOverlay
font-family: Helvetica, OpenSans
font-size: 13px font-size: 13px
color: #e4ad00 color: #979797
line-height: 18px line-height: 18px
display: none // Shown when an image is loaded display: none // Shown when an image is loaded

View File

@ -7,40 +7,55 @@ Meteor.startup(function() {
} }
OHIF.viewer.defaultHotkeys = { OHIF.viewer.defaultHotkeys = {
defaultTool: "ESC", defaultTool: 'ESC',
angle: "A", angle: 'A',
stackScroll: "S", stackScroll: 'S',
pan: "P", pan: 'P',
magnify: "M", magnify: 'M',
scrollDown: ["DOWN", "PAGEDOWN"], scrollDown: ['DOWN', 'PAGEDOWN'],
scrollUp: ["UP", "PAGEUP"], scrollUp: ['UP', 'PAGEUP'],
nextPanel: "RIGHT", nextPanel: 'RIGHT',
previousPanel: "LEFT", previousPanel: 'LEFT',
invert: "I", invert: 'I',
flipV: "V", flipV: 'V',
flipH: "H", flipH: 'H',
wwwc: "W", wwwc: 'W',
zoom: "Z", zoom: 'Z',
cinePlay: "SPACE", cinePlay: 'SPACE',
rotateR: "R", rotateR: 'R',
rotateL: "L", rotateL: 'L',
toggleOverlayTags: 'SHIFT', toggleOverlayTags: 'SHIFT',
WLPresetSoftTissue: ["NUMPAD1", "1"], WLPresetSoftTissue: ['NUMPAD1', '1'],
WLPresetLung: ["NUMPAD2", "2"], WLPresetLung: ['NUMPAD2', '2'],
WLPresetLiver: ["NUMPAD3", "3"], WLPresetLiver: ['NUMPAD3', '3'],
WLPresetBone: ["NUMPAD4", "4"], WLPresetBone: ['NUMPAD4', '4'],
WLPresetBrain: ["NUMPAD5", "5"] WLPresetBrain: ['NUMPAD5', '5']
}; };
// For now // For now
OHIF.viewer.hotkeys = OHIF.viewer.defaultHotkeys; OHIF.viewer.hotkeys = OHIF.viewer.defaultHotkeys;
OHIF.viewer.defaultWLPresets = { OHIF.viewer.defaultWLPresets = {
'SoftTissue' : {wc : 40, ww : 400}, SoftTissue: {
'Lung' : {wc : -600, ww : 1500}, wc: 40,
'Liver' : {wc : 90, ww : 150}, ww: 400
'Bone' : {wc: 480, ww : 2500}, },
'Brain' : {wc : 40, ww : 80} Lung: {
wc: -600,
ww: 1500
},
Liver: {
wc: 90,
ww: 150
},
Bone: {
wc: 480,
ww: 2500
},
Brain: {
wc: 40,
ww: 80
}
}; };
// For now // For now
@ -48,53 +63,53 @@ Meteor.startup(function() {
OHIF.viewer.hotkeyFunctions = { OHIF.viewer.hotkeyFunctions = {
wwwc: function() { wwwc: function() {
toolManager.setActiveTool("wwwc"); toolManager.setActiveTool('wwwc');
}, },
zoom: function() { zoom: function() {
toolManager.setActiveTool("zoom"); toolManager.setActiveTool('zoom');
}, },
angle: function() { angle: function() {
toolManager.setActiveTool("angle"); toolManager.setActiveTool('angle');
}, },
dragProbe: function() { dragProbe: function() {
toolManager.setActiveTool("dragProbe"); toolManager.setActiveTool('dragProbe');
}, },
ellipticalRoi: function() { ellipticalRoi: function() {
toolManager.setActiveTool("ellipticalRoi"); toolManager.setActiveTool('ellipticalRoi');
}, },
magnify: function() { magnify: function() {
toolManager.setActiveTool("magnify"); toolManager.setActiveTool('magnify');
}, },
annotate: function() { annotate: function() {
toolManager.setActiveTool("annotate"); toolManager.setActiveTool('annotate');
}, },
stackScroll: function() { stackScroll: function() {
toolManager.setActiveTool("stackScroll"); toolManager.setActiveTool('stackScroll');
}, },
pan: function() { pan: function() {
toolManager.setActiveTool("pan"); toolManager.setActiveTool('pan');
}, },
length: function() { length: function() {
toolManager.setActiveTool("length"); toolManager.setActiveTool('length');
}, },
spine: function() { spine: function() {
toolManager.setActiveTool("spine"); toolManager.setActiveTool('spine');
}, },
wwwcRegion: function() { wwwcRegion: function() {
toolManager.setActiveTool("wwwcRegion"); toolManager.setActiveTool('wwwcRegion');
}, },
zoomIn: function () { zoomIn: function() {
var button = document.getElementById("zoomIn"); var button = document.getElementById('zoomIn');
flashButton(button); flashButton(button);
zoomIn(); zoomIn();
}, },
zoomOut: function () { zoomOut: function() {
var button = document.getElementById("zoomOut"); var button = document.getElementById('zoomOut');
flashButton(button); flashButton(button);
zoomOut(); zoomOut();
}, },
zoomToFit: function () { zoomToFit: function() {
var button = document.getElementById("zoomToFit"); var button = document.getElementById('zoomToFit');
flashButton(button); flashButton(button);
zoomToFit(); zoomToFit();
}, },
@ -134,27 +149,27 @@ Meteor.startup(function() {
previousActivePanel(); previousActivePanel();
}, },
invert: function() { invert: function() {
var button = document.getElementById("invert"); var button = document.getElementById('invert');
flashButton(button); flashButton(button);
invert(); invert();
}, },
flipV: function() { flipV: function() {
var button = document.getElementById("flipV"); var button = document.getElementById('flipV');
flashButton(button); flashButton(button);
flipV(); flipV();
}, },
flipH: function() { flipH: function() {
var button = document.getElementById("flipH"); var button = document.getElementById('flipH');
flashButton(button); flashButton(button);
flipH(); flipH();
}, },
rotateR: function() { rotateR: function() {
var button = document.getElementById("rotateR"); var button = document.getElementById('rotateR');
flashButton(button); flashButton(button);
rotateR(); rotateR();
}, },
rotateL: function() { rotateL: function() {
var button = document.getElementById("rotateL"); var button = document.getElementById('rotateL');
flashButton(button); flashButton(button);
rotateL(); rotateL();
}, },
@ -174,6 +189,8 @@ Meteor.startup(function() {
} }
} }
}; };
OHIF.viewer.loadedSeriesData = {};
}); });
// Define a jQuery reverse function // Define a jQuery reverse function
@ -198,7 +215,6 @@ function previousActivePanel() {
setActiveViewport(element); setActiveViewport(element);
} }
function nextActivePanel() { function nextActivePanel() {
log.info('nextActivePanel'); log.info('nextActivePanel');
var currentIndex = Session.get('activeViewport'); var currentIndex = Session.get('activeViewport');
@ -224,7 +240,7 @@ function flashButton(button) {
} }
button.classList.add('active'); button.classList.add('active');
setTimeout(function () { setTimeout(function() {
button.classList.remove('active'); button.classList.remove('active');
}, 100); }, 100);
} }
@ -233,13 +249,13 @@ function bindHotkey(hotkey, task) {
var hotkeyFunctions = OHIF.viewer.hotkeyFunctions; var hotkeyFunctions = OHIF.viewer.hotkeyFunctions;
// Only bind defined, non-empty HotKeys // Only bind defined, non-empty HotKeys
if (!hotkey || hotkey === "") { if (!hotkey || hotkey === '') {
return; return;
} }
var fn; var fn;
if (task.indexOf("WLPreset") > -1) { if (task.indexOf('WLPreset') > -1) {
var presetName = task.replace("WLPreset", ""); var presetName = task.replace('WLPreset', '');
fn = function() { fn = function() {
applyWLPresetToActiveElement(presetName); applyWLPresetToActiveElement(presetName);
}; };

View File

@ -29,8 +29,14 @@ getWADORSImageId = function(instance) {
//maxPixelValue : 255, //maxPixelValue : 255,
slope: instance.rescaleSlope, slope: instance.rescaleSlope,
intercept: instance.rescaleIntercept, intercept: instance.rescaleIntercept,
windowCenter : windowCenter, samplesPerPixel: instance.samplesPerPixel,
windowWidth : windowWidth, imageOrientationPatient: instance.imageOrientationPatient,
imagePositionPatient: instance.imagePositionPatient,
sopClassUid: instance.sopClassUid,
instanceNumber: instance.instanceNumber,
frameOfReferenceUID: instance.frameOfReferenceUID,
windowCenter: windowCenter,
windowWidth: windowWidth,
//render: cornerstone.renderColorImage, //render: cornerstone.renderColorImage,
//getPixelData: getPixelData, //getPixelData: getPixelData,
//getImageData: getImageData, //getImageData: getImageData,
@ -47,6 +53,8 @@ getWADORSImageId = function(instance) {
instance: instance instance: instance
}; };
var imageId = cornerstoneWADORSImageLoader.addImage(image); var imageId = cornerstoneWADOImageLoader.imageManager.add(image);
console.log('WADO-RS ImageID: ' + imageId);
return imageId; return imageId;
}; };

View File

@ -1,9 +1,14 @@
/**
* Formats a patient name for display purposes
*/
formatPN = function(context) {
if (!context) {
return;
}
return context.replace('^', ', ');
};
/** /**
* A global Blaze UI helper to format a patient name for display purposes * A global Blaze UI helper to format a patient name for display purposes
*/ */
UI.registerHelper('formatPN', function (context) { UI.registerHelper('formatPN', formatPN);
if (!context) {
return undefined;
}
return context.replace('^', ', ');
});

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