Updates for Timepoint assocation (LT-60) and Conformance Checks (LT-92, LT-93, LT-94)
This commit is contained in:
parent
6c4eae3699
commit
beaa56c0b3
4
.gitignore
vendored
4
.gitignore
vendored
@ -2,4 +2,6 @@
|
||||
docs/
|
||||
.meteor/local
|
||||
.meteor/meteorite
|
||||
node_modules
|
||||
node_modules
|
||||
Packages/active-entry/helloworld/
|
||||
LesionTracker/tests/nightwatch/reports/
|
||||
6
.jscsrc
6
.jscsrc
@ -7,6 +7,7 @@
|
||||
"disallowSpacesInFunctionExpression": {
|
||||
"beforeOpeningRoundBrace": true
|
||||
},
|
||||
"disallowSpacesInsideParentheses": true,
|
||||
"disallowKeywordsOnNewLine": ["else"],
|
||||
"disallowNewlineBeforeBlockStatements": true,
|
||||
"requirePaddingNewLinesAfterUseStrict": true,
|
||||
@ -17,9 +18,12 @@
|
||||
"requireObjectKeysOnNewLine": true,
|
||||
"requireSemicolons": true,
|
||||
"requireSpaceAfterBinaryOperators": true,
|
||||
"requireSpaceAfterComma": true,
|
||||
"requireSpacesInFunctionExpression": {
|
||||
"beforeOpeningCurlyBrace": true
|
||||
},
|
||||
"requireSpaceBeforeObjectValues": true,
|
||||
"requireSpacesInsideObjectBrackets": "all",
|
||||
"requireSpacesInsideArrayBrackets": "all",
|
||||
"requireLineBreakAfterVariableAssignment": true,
|
||||
"requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="],
|
||||
"disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-"],
|
||||
|
||||
@ -48,3 +48,4 @@ clinical:hipaa-audit-log
|
||||
clinical:hipaa-logger
|
||||
anti:gagarin@=0.4.11
|
||||
check
|
||||
aldeed:template-extension@4.0.0
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
accounts-base@1.2.2
|
||||
accounts-password@1.1.4
|
||||
aldeed:template-extension@4.0.0
|
||||
anti:gagarin@0.4.11
|
||||
anti:i18n@0.4.3
|
||||
arsnebula:reactive-promise@0.9.1
|
||||
@ -38,6 +39,7 @@ ddp-server@1.2.2
|
||||
deps@1.0.9
|
||||
dicomweb@0.0.1
|
||||
diff-sequence@1.0.1
|
||||
dimseservice@0.0.1
|
||||
ecmascript@0.1.6
|
||||
ecmascript-runtime@0.2.6
|
||||
ejson@1.0.7
|
||||
@ -90,6 +92,7 @@ reactive-var@1.0.6
|
||||
reload@1.1.4
|
||||
retry@1.0.4
|
||||
routepolicy@1.0.6
|
||||
rwatts:uuid@0.0.2
|
||||
service-configuration@1.0.5
|
||||
session@1.1.1
|
||||
sha@1.0.4
|
||||
|
||||
@ -1,30 +1,62 @@
|
||||
// TODO: Move all of this into the LesionTracker package
|
||||
body
|
||||
background-color: #202020
|
||||
|
||||
.tab-content {
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
.tab-content
|
||||
height: calc(100% - 40px)
|
||||
|
||||
table#tblStudyList thead > tr {
|
||||
background-color: #424242;
|
||||
color: white;
|
||||
}
|
||||
table#tblStudyList tbody > tr:nth-child(even) {
|
||||
background-color: #888;
|
||||
color: black;
|
||||
}
|
||||
table#tblStudyList tbody > tr:nth-child(odd) {
|
||||
background-color: #BBB;
|
||||
color: black;
|
||||
}
|
||||
table#tblStudyList tbody > tr:hover{
|
||||
table#tblStudyList thead > tr
|
||||
background-color: #424242
|
||||
color: white
|
||||
|
||||
table#tblStudyList tbody > tr:nth-child(even)
|
||||
background-color: #888
|
||||
color: black
|
||||
|
||||
table#tblStudyList tbody > tr:nth-child(odd)
|
||||
background-color: #BBB
|
||||
color: black
|
||||
|
||||
table#tblStudyList tbody > tr:hover
|
||||
background-color: #009BD2
|
||||
color: white
|
||||
}
|
||||
|
||||
table#tblStudyList tbody > tr > td{
|
||||
table#tblStudyList tbody > tr > td
|
||||
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
|
||||
@ -5,6 +5,7 @@
|
||||
{{>nonTargetLesionDialog}}
|
||||
{{>nonTargetResponseDialog}}
|
||||
{{>timepointTextDialog}}
|
||||
{{ >conformanceCheckFeedback }}
|
||||
|
||||
{{>hidingPanel}}
|
||||
<div id="viewportAndLesionTable">
|
||||
|
||||
@ -3,10 +3,8 @@ Template.viewer.onCreated(function() {
|
||||
$(window).on('resize', handleResize);
|
||||
|
||||
var self = this;
|
||||
|
||||
var firstMeasurementsActivated = false;
|
||||
|
||||
log.info('viewer onCreated');
|
||||
var contentId = this.data.contentId;
|
||||
|
||||
OHIF = OHIF || {
|
||||
viewer: {}
|
||||
@ -16,7 +14,6 @@ Template.viewer.onCreated(function() {
|
||||
OHIF.viewer.defaultTool = 'wwwc';
|
||||
OHIF.viewer.refLinesEnabled = true;
|
||||
OHIF.viewer.isPlaying = {};
|
||||
var contentId = this.data.contentId;
|
||||
|
||||
OHIF.viewer.functionList = {
|
||||
invert: function(element) {
|
||||
@ -42,15 +39,17 @@ Template.viewer.onCreated(function() {
|
||||
toggleLesionTrackerTools: toggleLesionTrackerTools,
|
||||
clearTools: clearTools,
|
||||
lesion: function() {
|
||||
// Used for hotkeys
|
||||
toolManager.setActiveTool('lesion');
|
||||
},
|
||||
nonTarget: function() {
|
||||
// Used for hotkeys
|
||||
toolManager.setActiveTool('nonTarget');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// 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.lesion = 'T'; // 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) {
|
||||
log.info('Reloading previous loadedSeriesData');
|
||||
OHIF.viewer.loadedSeriesData = ViewerData[contentId].loadedSeriesData;
|
||||
@ -76,8 +79,15 @@ Template.viewer.onCreated(function() {
|
||||
ViewerData[contentId].loadedSeriesData = OHIF.viewer.loadedSeriesData;
|
||||
|
||||
// Update the viewer data object
|
||||
ViewerData[contentId].viewportColumns = 2;
|
||||
ViewerData[contentId].viewportRows = 1;
|
||||
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].viewportRows = 1;
|
||||
}
|
||||
|
||||
ViewerData[contentId].activeViewport = 0;
|
||||
Session.set('ViewerData', ViewerData);
|
||||
}
|
||||
@ -96,43 +106,29 @@ Template.viewer.onCreated(function() {
|
||||
|
||||
self.autorun(function() {
|
||||
var patientId = Session.get('patientId');
|
||||
self.subscribe('timepoints', patientId);
|
||||
self.subscribe('measurements', patientId);
|
||||
self.subscribe('singlePatientTimepoints', patientId);
|
||||
self.subscribe('singlePatientMeasurements', patientId);
|
||||
|
||||
if (self.subscriptionsReady()) {
|
||||
ViewerStudies.find().observe({
|
||||
added: function(study) {
|
||||
// TODO = Replace this whole section when we have a
|
||||
// timepoint-to-study association modal
|
||||
|
||||
// First, check if we have a timepoint related to this
|
||||
// study date already
|
||||
// Find the relevant timepoint given the newly added study
|
||||
var timepoint = Timepoints.findOne({
|
||||
timepointName: study.studyDate
|
||||
studyInstanceUids: {
|
||||
$in: [study.studyInstanceUid]
|
||||
}
|
||||
});
|
||||
|
||||
// If we do, stop here
|
||||
if (timepoint) {
|
||||
log.warn('A timepoint with that study date already exists!');
|
||||
if (!timepoint) {
|
||||
log.warn('Study added to Viewer has not been associated!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Next, check the first timepoint in the collection
|
||||
var testTimepoint = Timepoints.findOne({});
|
||||
|
||||
// If it relates to another subject, we need to stop here as well
|
||||
// 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()
|
||||
|
||||
// Update the added document with its related timepointId
|
||||
ViewerStudies.update(study._id, {
|
||||
$set: {
|
||||
timepointId: timepoint.timepointId
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -159,12 +155,17 @@ Template.viewer.onCreated(function() {
|
||||
log.info('Measurement added');
|
||||
syncMeasurementAndToolData(data);
|
||||
updateRelatedElements(data.imageId);
|
||||
|
||||
TrialResponseCriteria.validateAll();
|
||||
},
|
||||
changed: function(data) {
|
||||
TrialResponseCriteria.validateAllDelayed();
|
||||
},
|
||||
removed: function(data) {
|
||||
log.info('Measurement removed');
|
||||
|
||||
// Check that this Measurement actually contains timepoint data
|
||||
if (!data.timepoints) {
|
||||
if (!data || !data.timepoints) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -175,9 +176,9 @@ Template.viewer.onCreated(function() {
|
||||
|
||||
// Remove the measurement from all the imageIds on which it exists
|
||||
// as toolData
|
||||
Object.keys(data.timepoints).forEach(function(timepointID) {
|
||||
Object.keys(data.timepoints).forEach(function(timepointId) {
|
||||
// Clear the toolData for this timepoint
|
||||
var imageId = data.timepoints[timepointID].imageId;
|
||||
var imageId = data.timepoints[timepointId].imageId;
|
||||
removeToolDataWithMeasurementId(imageId, toolType, measurementId);
|
||||
});
|
||||
|
||||
@ -202,146 +203,29 @@ Template.viewer.onCreated(function() {
|
||||
viewports.each(function(index, element) {
|
||||
cornerstone.updateImage(element);
|
||||
});
|
||||
|
||||
ValidationErrors.remove({
|
||||
measurementId: data._id
|
||||
});
|
||||
|
||||
TrialResponseCriteria.validateAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('CornerstoneNewImage', cornerstoneTools.updateImageSynchronizer);
|
||||
});
|
||||
|
||||
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() {
|
||||
// Enable hotkeys
|
||||
enableHotkeys();
|
||||
|
||||
// Set lesion tool buttons as disable if pixel spacing is not available for active element
|
||||
this.autorun(function(){
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// Set lesion tool buttons as disabled if pixel spacing is not available for active element
|
||||
this.autorun(pixelSpacingAutorunCheck);
|
||||
});
|
||||
|
||||
Template.viewer.onDestroyed(function() {
|
||||
log.info('onDestroyed');
|
||||
console.log("viewer destroyed!");
|
||||
console.log('viewer destroyed!');
|
||||
|
||||
// Remove the Window resize listener
|
||||
$(window).off('resize', handleResize);
|
||||
@ -350,6 +234,9 @@ Template.viewer.onDestroyed(function() {
|
||||
});
|
||||
|
||||
Template.viewer.events({
|
||||
'CornerstoneToolsMeasurementAdded .imageViewerViewport': function(e, template, eventData) {
|
||||
handleMeasurementAdded(e, eventData);
|
||||
},
|
||||
'CornerstoneToolsMeasurementModified .imageViewerViewport': function(e, template, eventData) {
|
||||
handleMeasurementModified(e, eventData);
|
||||
},
|
||||
@ -357,43 +244,3 @@ Template.viewer.events({
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
Router.onBeforeAction(function() {
|
||||
|
||||
// User is logged in, go ahead and route them
|
||||
this.next();
|
||||
});
|
||||
@ -11,15 +11,17 @@ Object.keys(ViewerData).forEach(function(contentId) {
|
||||
});
|
||||
|
||||
Router.configure({
|
||||
layoutTemplate: 'layoutLesionTracker',
|
||||
loadingTemplate: 'layoutLesionTracker'
|
||||
layoutTemplate: 'lesionTrackerLayout',
|
||||
loadingTemplate: 'lesionTrackerLayout',
|
||||
notFoundTemplate: 'notFound'
|
||||
});
|
||||
|
||||
Router.onBeforeAction('loading');
|
||||
|
||||
var data = {
|
||||
additionalTemplates: [
|
||||
'associationModal'
|
||||
'associationModal',
|
||||
'optionsModal'
|
||||
]
|
||||
};
|
||||
|
||||
@ -35,24 +37,22 @@ Router.route('/worklist', function() {
|
||||
this.render('worklist', routerOptions);
|
||||
});
|
||||
|
||||
Router.route('/viewer/:_id', {
|
||||
layoutTemplate: 'layoutLesionTracker',
|
||||
Router.route('/viewer/timepoints/:_id', {
|
||||
layoutTemplate: 'lesionTrackerLayout',
|
||||
name: 'viewer',
|
||||
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
|
||||
// If it is, stop here so we don't keep adding tabs on hot-code reloads
|
||||
var tab = WorklistTabs.find({
|
||||
studyInstanceUid: studyInstanceUid
|
||||
}).fetch();
|
||||
var tab = WorklistTabs.findOne({
|
||||
timepointId: timepointId
|
||||
});
|
||||
if (tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.render('worklist', routerOptions);
|
||||
openNewTab(studyInstanceUid);
|
||||
openNewTabWithTimepoint(timepointId);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -1,30 +1,30 @@
|
||||
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 = {
|
||||
dicomWeb: {
|
||||
endpoints: [
|
||||
{
|
||||
name: 'Orthanc',
|
||||
wadoUriRootNOTE: 'either this uri is not correct for wado-uri or wado-uri is not configured on orthanc currently',
|
||||
wadoUriRoot: 'http://localhost:8043/wado',
|
||||
qidoRoot: 'http://localhost:8042/dicom-web',
|
||||
wadoRoot: 'http://localhost:8042/dicom-web',
|
||||
qidoSupportsIncludeField: false,
|
||||
imageRendering: 'wadouri',
|
||||
requestOptions: {
|
||||
endpoints: [{
|
||||
name: 'Orthanc',
|
||||
wadoUriRootNOTE: 'either this uri is not correct for wado-uri or wado-uri is not configured on orthanc currently',
|
||||
wadoUriRoot: 'http://localhost:8043/wado',
|
||||
qidoRoot: 'http://localhost:8042/dicom-web',
|
||||
wadoRoot: 'http://localhost:8042/dicom-web',
|
||||
qidoSupportsIncludeField: false,
|
||||
imageRendering: 'wadouri',
|
||||
requestOptions: {
|
||||
auth: 'orthanc:orthanc',
|
||||
logRequests: true,
|
||||
logResponses: false,
|
||||
logTiming: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
},
|
||||
dimse: {
|
||||
host: 'localhost',
|
||||
port: 4242,
|
||||
hostAE: 'ORTHANC'
|
||||
},
|
||||
defaultServiceType: 'dicomWeb'
|
||||
//defaultServiceType: 'dimse'
|
||||
};
|
||||
|
||||
console.log('Using default LesionTracker settings with service: ' + Meteor.settings.defaultServiceType);
|
||||
});
|
||||
|
||||
@ -77,6 +77,7 @@ reactive-var@1.0.6
|
||||
reload@1.1.4
|
||||
retry@1.0.4
|
||||
routepolicy@1.0.6
|
||||
rwatts:uuid@0.0.2
|
||||
service-configuration@1.0.5
|
||||
session@1.1.1
|
||||
sha@1.0.4
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
// node.js
|
||||
@ -66,6 +66,7 @@ var dicomParser = (function(dicomParser) {
|
||||
function getDataSetByteStream(transferSyntax, position) {
|
||||
if(transferSyntax === '1.2.840.10008.1.2.1.99')
|
||||
{
|
||||
// https://github.com/nodeca/pako
|
||||
if(typeof(pako) === "undefined") {
|
||||
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'";
|
||||
}
|
||||
if((byteArray instanceof Uint8Array) === false) {
|
||||
throw 'dicomParser.ByteStream: parameter byteArray is not of type Uint8Array';
|
||||
if((byteArray instanceof Uint8Array) === false &&
|
||||
(byteArray instanceof Buffer) === false ) {
|
||||
throw 'dicomParser.ByteStream: parameter byteArray is not of type Uint8Array or Buffer';
|
||||
}
|
||||
if(position < 0)
|
||||
{
|
||||
@ -2253,7 +2255,7 @@ var dicomParser = (function (dicomParser)
|
||||
dicomParser = {};
|
||||
}
|
||||
|
||||
dicomParser.version = "1.2.0";
|
||||
dicomParser.version = "1.2.1";
|
||||
|
||||
return dicomParser;
|
||||
}(dicomParser));
|
||||
|
||||
@ -1,30 +1,45 @@
|
||||
Package.describe({
|
||||
name: "cornerstone",
|
||||
summary: "Cornerstone Web-based Medical Imaging libraries",
|
||||
version: '0.0.1'
|
||||
name: 'cornerstone',
|
||||
summary: 'Cornerstone Web-based Medical Imaging libraries',
|
||||
version: '0.0.1'
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
Package.onUse(function(api) {
|
||||
api.versionsFrom('1.2.0.2');
|
||||
|
||||
api.use('jquery');
|
||||
api.use('dicomweb');
|
||||
|
||||
api.addFiles('client/cornerstone.js', 'client', {bare: true});
|
||||
api.addFiles('client/cornerstoneMath.js', 'client', {bare: true});
|
||||
api.addFiles('client/cornerstoneTools.js', 'client', {bare: true});
|
||||
api.addFiles('client/cornerstoneWADOImageLoader.js', 'client', {bare: true});
|
||||
api.addFiles('client/cornerstoneWADORSImageLoader.js', 'client', {bare: true});
|
||||
api.addFiles('client/dicomParser.js', 'client', {bare: true});
|
||||
api.addFiles('client/hammer.js', 'client', {bare: true});
|
||||
api.addFiles('client/cornerstone.js', 'client', {
|
||||
bare: true
|
||||
});
|
||||
api.addFiles('client/cornerstoneMath.js', 'client', {
|
||||
bare: true
|
||||
});
|
||||
api.addFiles('client/cornerstoneTools.js', 'client', {
|
||||
bare: true
|
||||
});
|
||||
api.addFiles('client/cornerstoneWADOImageLoader.js', 'client', {
|
||||
bare: true
|
||||
});
|
||||
api.addFiles('client/dicomParser.js', 'client', {
|
||||
bare: true
|
||||
});
|
||||
api.addFiles('client/hammer.js', 'client', {
|
||||
bare: true
|
||||
});
|
||||
|
||||
api.addFiles('client/measurementManager.js', 'client', {bare: true});
|
||||
api.addFiles('client/measurementManagerExample.js', 'client', {bare: true});
|
||||
api.addFiles('client/measurementManager.js', 'client', {
|
||||
bare: true
|
||||
});
|
||||
api.addFiles('client/measurementManagerExample.js', 'client', {
|
||||
bare: true
|
||||
});
|
||||
|
||||
api.export("cornerstone", 'client');
|
||||
api.export("cornerstoneMath", 'client');
|
||||
api.export("cornerstoneTools", 'client');
|
||||
api.export("cornerstoneWADOImageLoader", 'client');
|
||||
api.export("cornerstoneWADORSImageLoader", 'client');
|
||||
api.export("dicomParser", ['client', 'server']);
|
||||
api.export('cornerstone', 'client');
|
||||
api.export('cornerstoneMath', 'client');
|
||||
api.export('cornerstoneTools', 'client');
|
||||
api.export('cornerstoneWADOImageLoader', 'client');
|
||||
api.export('dicomParser', ['client', 'server']);
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -9,16 +9,11 @@ Package.onUse(function(api) {
|
||||
|
||||
// DICOMWeb API functions
|
||||
api.addFiles('server/namespace.js', 'server');
|
||||
api.addFiles('server/getImageFrame.js', 'server');
|
||||
api.addFiles('server/getJSON.js', 'server');
|
||||
api.addFiles('server/getName.js', 'server');
|
||||
api.addFiles('server/getNumber.js', 'server');
|
||||
api.addFiles('server/getString.js', 'server');
|
||||
|
||||
// Helper functions
|
||||
api.addFiles('lib/findIndexOfString.js', 'server');
|
||||
api.addFiles('lib/uint8ArrayToString.js', 'server');
|
||||
|
||||
api.export('DICOMWeb', 'server');
|
||||
api.export('DICOMWeb', ['client', 'server']);
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
};
|
||||
@ -1,8 +1,8 @@
|
||||
DICOMWeb.getJSON = function(url, options) {
|
||||
var getOptions = {
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
Accept: 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (options.auth) {
|
||||
|
||||
@ -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 ]
|
||||
},
|
||||
5
Packages/dimseservice/etc/sampleOrthancConfig.json
Normal file
5
Packages/dimseservice/etc/sampleOrthancConfig.json
Normal 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 ]
|
||||
},
|
||||
@ -8,27 +8,38 @@ DIMSE.associate = function(contexts, callback) {
|
||||
port = Meteor.settings.dimse.port,
|
||||
ae = Meteor.settings.dimse.hostAE;
|
||||
|
||||
var client = net.connect({
|
||||
host: host,
|
||||
port: port
|
||||
},
|
||||
function() { //'connect' listener
|
||||
console.log('==Connected');
|
||||
console.log("Associating via DIMSE");
|
||||
console.log(Meteor.settings.dimse);
|
||||
|
||||
var conn = new Connection(client, {
|
||||
vr: {
|
||||
split: false
|
||||
}
|
||||
});
|
||||
conn.associate({
|
||||
contexts: contexts,
|
||||
hostAE: ae
|
||||
}, function(pdu) {
|
||||
// associated
|
||||
console.log('==Associated');
|
||||
callback.call(conn, pdu);
|
||||
});
|
||||
var client = net.connect({
|
||||
host: host,
|
||||
port: port
|
||||
});
|
||||
|
||||
client.on('connect', function() {
|
||||
//'connect' listener
|
||||
console.log('==Connected');
|
||||
|
||||
var conn = new Connection(client, {
|
||||
vr: {
|
||||
split: false
|
||||
}
|
||||
});
|
||||
|
||||
conn.associate({
|
||||
contexts: contexts,
|
||||
hostAE: ae
|
||||
}, function(pdu) {
|
||||
// associated
|
||||
console.log('==Associated');
|
||||
|
||||
callback.call(conn, pdu);
|
||||
});
|
||||
});
|
||||
|
||||
client.on('error', function(error) {
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
DIMSE.retrievePatients = function(params) {
|
||||
@ -1,2 +1,5 @@
|
||||
Timepoints = new Meteor.Collection('timepoints');
|
||||
Studies = new Meteor.Collection('studies');
|
||||
Measurements = new Meteor.Collection('measurements');
|
||||
|
||||
WorklistSubscriptions = ['studies', 'timepoints'];
|
||||
@ -6,7 +6,8 @@ LesionLocations.insert({
|
||||
location: 'Liver Left',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -15,7 +16,8 @@ LesionLocations.insert({
|
||||
location: 'Liver Right',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -24,7 +26,8 @@ LesionLocations.insert({
|
||||
location: 'Liver Caudate',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -33,7 +36,8 @@ LesionLocations.insert({
|
||||
location: 'Lung LLL',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -42,7 +46,8 @@ LesionLocations.insert({
|
||||
location: 'Lung LUL',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -51,7 +56,8 @@ LesionLocations.insert({
|
||||
location: 'Lung RLL',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -60,7 +66,8 @@ LesionLocations.insert({
|
||||
location: 'Lung RML',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -69,7 +76,8 @@ LesionLocations.insert({
|
||||
location: 'Lung RUL',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -78,7 +86,8 @@ LesionLocations.insert({
|
||||
location: 'Pleura Left',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -87,7 +96,8 @@ LesionLocations.insert({
|
||||
location: 'Pleura Right',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -96,7 +106,8 @@ LesionLocations.insert({
|
||||
location: 'Kidney Left',
|
||||
hasDescription: false,
|
||||
description: '',
|
||||
selected: false
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
LesionLocations.insert({
|
||||
@ -104,5 +115,7 @@ LesionLocations.insert({
|
||||
group: 'kidney',
|
||||
location: 'Kidney Right',
|
||||
hasDescription: false,
|
||||
description: ''
|
||||
description: '',
|
||||
selected: false,
|
||||
isNodal: false
|
||||
});
|
||||
|
||||
@ -0,0 +1 @@
|
||||
PatientLocations = new Meteor.Collection(null);
|
||||
@ -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
|
||||
};
|
||||
})();
|
||||
@ -50,6 +50,11 @@
|
||||
};
|
||||
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
|
||||
cornerstoneTools.addToolState(element, toolType, measurementData);
|
||||
|
||||
@ -76,11 +81,6 @@
|
||||
// Bind a one-time event listener for the Esc key
|
||||
$(element).one('keydown', cancelCallback);
|
||||
|
||||
// Set lesion number and lesion name
|
||||
if (measurementData.lesionNumber === undefined) {
|
||||
config.setLesionNumberCallback(measurementData, mouseEventData, doneCallback);
|
||||
}
|
||||
|
||||
cornerstone.updateImage(element);
|
||||
|
||||
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() {
|
||||
@ -217,8 +217,7 @@
|
||||
widthMeasurement: 0,
|
||||
perpendicularMeasurement: 0,
|
||||
isDeleted: false,
|
||||
isTarget: true,
|
||||
uid: uuid.v4()
|
||||
isTarget: true
|
||||
};
|
||||
return measurementData;
|
||||
}
|
||||
|
||||
@ -51,6 +51,13 @@
|
||||
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
|
||||
cornerstoneTools.addToolState(mouseEventData.element, toolType, measurementData);
|
||||
|
||||
@ -77,13 +84,6 @@
|
||||
// Bind a one-time event listener for the Esc key
|
||||
$(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);
|
||||
|
||||
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() {
|
||||
@ -101,6 +101,8 @@
|
||||
$(element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.nonTarget.mouseDownCallback);
|
||||
$(element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.nonTarget.mouseDownActivateCallback);
|
||||
|
||||
$(element).off('keydown', cancelCallback);
|
||||
|
||||
cornerstone.updateImage(mouseEventData.element);
|
||||
});
|
||||
}
|
||||
|
||||
@ -50,11 +50,10 @@
|
||||
x: config.horizontalLine.start.x + i * config.horizontalMinorTick,
|
||||
y: 0
|
||||
};
|
||||
if (i% 5 === 0) {
|
||||
|
||||
if (i% 5 === 0) {
|
||||
endPoint.y = config.horizontalLine.start.y - config.majorTickLength;
|
||||
} else {
|
||||
|
||||
endPoint.y = config.horizontalLine.start.y - config.minorTickLength;
|
||||
}
|
||||
|
||||
|
||||
1086
Packages/lesiontracker/client/compatibility/validate.js
Normal file
1086
Packages/lesiontracker/client/compatibility/validate.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,8 +10,8 @@
|
||||
{{ >studyAssociationTable }}
|
||||
</div>
|
||||
<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-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveAssociations">Save</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id='cancelAssociation'>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,116 @@
|
||||
Template.associationModal.events({
|
||||
'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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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
|
||||
@ -11,24 +11,29 @@ function closeHandler(dialog) {
|
||||
|
||||
// This event sets lesion number for new lesion
|
||||
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 enabledElement = cornerstone.getEnabledElement(element);
|
||||
var imageId = enabledElement.image.imageId;
|
||||
|
||||
var study = cornerstoneTools.metaData.get('study', imageId);
|
||||
|
||||
// Find the relevant timepoint given the current study
|
||||
var timepoint = Timepoints.findOne({
|
||||
timepointName: study.studyDate
|
||||
studyInstanceUids: {
|
||||
$in: [study.studyInstanceUid]
|
||||
}
|
||||
});
|
||||
|
||||
if (!timepoint) {
|
||||
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
|
||||
// exists at a different timepoint
|
||||
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = true);
|
||||
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = true);
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
|
||||
// Set lesion number
|
||||
@ -226,6 +231,9 @@ Template.lesionLocationDialog.events({
|
||||
/// Set the isTarget value to true, since this is the target-lesion dialog callback
|
||||
measurementData.isTarget = true;
|
||||
|
||||
// Set the isNodal value based on the Location's properties
|
||||
measurementData.isNodal = locationObj.isNodal;
|
||||
|
||||
// Adds lesion data to timepoints array
|
||||
LesionManager.updateLesionData(measurementData);
|
||||
} else {
|
||||
@ -233,6 +241,7 @@ Template.lesionLocationDialog.events({
|
||||
$set: {
|
||||
location: locationObj.location,
|
||||
locationId: locationObj.id,
|
||||
isNodal: locationObj.isNodal,
|
||||
locationUID: id
|
||||
}
|
||||
});
|
||||
|
||||
@ -5,18 +5,18 @@ Template.lesionTableTimepointCell.helpers({
|
||||
var lesionData = Template.parentData(1);
|
||||
return (lesionData &&
|
||||
lesionData.timepoints &&
|
||||
lesionData.timepoints[this.timepointID]);
|
||||
lesionData.timepoints[this.timepointId]);
|
||||
},
|
||||
displayData: function() {
|
||||
// Search Measurements by lesion and timepoint
|
||||
var lesionData = Template.parentData(1);
|
||||
if (!lesionData ||
|
||||
!lesionData.timepoints ||
|
||||
!lesionData.timepoints[this.timepointID]) {
|
||||
!lesionData.timepoints[this.timepointId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = lesionData.timepoints[this.timepointID];
|
||||
var data = lesionData.timepoints[this.timepointId];
|
||||
|
||||
if (lesionData.isTarget === true) {
|
||||
if (data.shortestDiameter) {
|
||||
@ -40,7 +40,7 @@ function doneCallback(measurementData, deleteTool) {
|
||||
// the specified Timepoint Cell
|
||||
if (deleteTool === true) {
|
||||
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);
|
||||
|
||||
// Create some fake measurement data
|
||||
var currentTimepointID = this.timepointID;
|
||||
var currentTimepointID = this.timepointId;
|
||||
|
||||
var timepointData = currentMeasurement.timepoints[currentTimepointID];
|
||||
var measurementData = {
|
||||
id: currentMeasurement._id,
|
||||
timepointID: currentTimepointID,
|
||||
timepointId: currentTimepointID,
|
||||
response: timepointData.response,
|
||||
imageId: timepointData.imageId,
|
||||
handles: timepointData.handles,
|
||||
@ -83,7 +83,7 @@ Template.lesionTableTimepointCell.events({
|
||||
if (keyCode === keys.DELETE ||
|
||||
(keyCode === keys.D && e.ctrlKey === true)) {
|
||||
var currentMeasurement = Template.parentData(1);
|
||||
var currentTimepointID = this.timepointID;
|
||||
var currentTimepointID = this.timepointId;
|
||||
|
||||
showConfirmDialog(function() {
|
||||
clearMeasurementTimepointData(currentMeasurement._id, currentTimepointID);
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
<template name="lesionTableTimepointHeader">
|
||||
<th class="lesionTableTimepointCell">
|
||||
{{formatDA timepointName "MM/DD/YYYY"}}
|
||||
{{#if timepointTextFound}}
|
||||
{{timepointText}}
|
||||
{{/if}}
|
||||
{{#if timepointLoaded }}<span class="timepointIndicator"><</span> {{/if}}
|
||||
<th class="lesionTableTimepointCell" title="Latest study date: {{formatDA latestDate "MM/DD/YYYY"}}"
|
||||
data-toggle="tooltip" data-placement="top">
|
||||
{{ timepointName }}
|
||||
</th>
|
||||
</template>
|
||||
@ -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({
|
||||
timepointTextFound: function() {
|
||||
var timepointText = this.timepointText;
|
||||
return (timepointText && timepointText === 'Baseline');
|
||||
'timepointName': function() {
|
||||
var timepoint = this;
|
||||
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
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -1,9 +1,10 @@
|
||||
<template name="layoutLesionTracker">
|
||||
<template name="lesionTrackerLayout">
|
||||
<div class="logoContainer">
|
||||
<a class="navbar-brand" href="http://ohif.org">
|
||||
<img src="/images/logo.png">
|
||||
<h4 class="name">Open Health Imaging Foundation</h4>
|
||||
</a>
|
||||
</div>
|
||||
{{> yield}}
|
||||
{{ >optionsButton }}
|
||||
{{ >yield }}
|
||||
</template>
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
.imageViewerViewportOverlay
|
||||
.timepointName
|
||||
color: #32BFFF
|
||||
@ -1,11 +1,10 @@
|
||||
<template name="studyContextMenu">
|
||||
<template name="lesionTrackerWorklistContextMenu">
|
||||
<div id="studyContextMenu" class="studyContextMenu noselect"
|
||||
oncontextmenu='return false;'
|
||||
unselectable='on'
|
||||
onselectstart='return false;'>
|
||||
<ul>
|
||||
<li>
|
||||
<!--<a id="deleteTool">Delete</a>-->
|
||||
<!-- TODO: Make this dynamically accept options-->
|
||||
<a id="launchStudyAssociation" type="button"
|
||||
data-toggle="modal"
|
||||
@ -14,16 +13,20 @@
|
||||
<i class="fa fa-calendar-plus-o fa-lg"></i>
|
||||
Associate
|
||||
</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-ban fa-stack-2x text-danger"></i>
|
||||
</span> Anonymize
|
||||
</a>
|
||||
<a><i class="fa fa-trash fa-lg"></i> Delete</a>
|
||||
<a><i class="fa fa-send-o fa-lg"></i> Send</a>
|
||||
<a><i class="fa fa-exchange fa-lg"></i> Export</a>
|
||||
<a><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-trash fa-lg"></i> Delete</a>
|
||||
<a class="disabled"><i class="fa fa-send-o fa-lg"></i> Send</a>
|
||||
<a class="disabled"><i class="fa fa-exchange fa-lg"></i> Export</a>
|
||||
<a class="disabled"><i class="fa fa-download fa-lg"></i> Download</a>
|
||||
<a class="disabled"><i class="fa fa-photo fa-lg"></i> View Series Details</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -11,24 +11,29 @@ function closeHandler(dialog) {
|
||||
|
||||
// This event sets lesion number for new lesion
|
||||
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 enabledElement = cornerstone.getEnabledElement(element);
|
||||
var imageId = enabledElement.image.imageId;
|
||||
|
||||
var study = cornerstoneTools.metaData.get('study', imageId);
|
||||
|
||||
// Find the relevant timepoint given the current study
|
||||
var timepoint = Timepoints.findOne({
|
||||
timepointName: study.studyDate
|
||||
studyInstanceUids: {
|
||||
$in: [study.studyInstanceUid]
|
||||
}
|
||||
});
|
||||
|
||||
if (!timepoint) {
|
||||
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
|
||||
// exists at a different timepoint
|
||||
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = false);
|
||||
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = false);
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
|
||||
// Set lesion number
|
||||
@ -211,7 +216,7 @@ changeNonTargetLocationCallback = function(measurementData, eventData, doneCallb
|
||||
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
|
||||
// some sort of UID
|
||||
|
||||
@ -65,7 +65,7 @@ changeNonTargetResponse = function(measurementData, eventData, doneCallback) {
|
||||
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
|
||||
// some sort of UID
|
||||
|
||||
@ -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>
|
||||
@ -0,0 +1,6 @@
|
||||
// These settings are temporary!
|
||||
.optionsButton
|
||||
position: absolute
|
||||
top: 3px
|
||||
right: 350px
|
||||
cursor: pointer
|
||||
@ -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">×</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>
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,2 @@
|
||||
.trialCriteriaLabel
|
||||
margin: 0 5px
|
||||
@ -43,13 +43,15 @@
|
||||
<td class="studyDataCell {{ #if autoselected }}disabled{{ /if }}">
|
||||
<p>{{studyDescription}}</p>
|
||||
</td>
|
||||
<td class="timepointOptions center studyDataCell {{ #if autoselected }}disabled{{ /if }}">
|
||||
<td class="timepointOptions center studyDataCell noselect {{ #if autoselected }}disabled{{ /if }}">
|
||||
{{ #each timepointOptions }}
|
||||
<label>
|
||||
<label class="noselect">
|
||||
<input type="radio"
|
||||
name="{{../_id}}"
|
||||
value={{type}}
|
||||
{{ inlineIf autoselected true 'disabled'}}>
|
||||
name="{{../_id}}"
|
||||
value="{{value}}"
|
||||
class="timepointOption"
|
||||
{{ inlineIf checked true 'checked' }}
|
||||
{{ inlineIf autoselected true 'disabled' }}>
|
||||
{{name}}
|
||||
</label>
|
||||
{{ /each }}
|
||||
|
||||
@ -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) {
|
||||
if (!selectedStudies.length) {
|
||||
@ -40,6 +45,9 @@ function autoSelectStudies(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({
|
||||
studyDate: {
|
||||
$gte: range.earliestDate.format('YYYYMMDD'),
|
||||
@ -82,8 +90,8 @@ Template.studyAssociationTable.helpers({
|
||||
studyDate: 1
|
||||
}
|
||||
}).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
|
||||
@ -91,16 +99,15 @@ Template.studyAssociationTable.helpers({
|
||||
* @returns {Array.<T>}
|
||||
*/
|
||||
timepointOptions: function() {
|
||||
return [
|
||||
{
|
||||
return [{
|
||||
value: 'baseline',
|
||||
name: 'Baseline'
|
||||
},
|
||||
{
|
||||
name: 'Baseline',
|
||||
checked: true
|
||||
}, {
|
||||
value: 'followup',
|
||||
name: 'Follow-up'
|
||||
}
|
||||
];
|
||||
name: 'Follow-up',
|
||||
checked: false
|
||||
}];
|
||||
},
|
||||
earliestDate: function() {
|
||||
var selectedStudies = WorklistSelectedStudies.find({}, {
|
||||
@ -108,12 +115,11 @@ Template.studyAssociationTable.helpers({
|
||||
studyDate: 1
|
||||
}
|
||||
}).fetch();
|
||||
var range = getDateRange(selectedStudies);
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
return range.earliestDate;
|
||||
var range = getDateRange(selectedStudies);
|
||||
if (range) {
|
||||
return range.earliestDate;
|
||||
}
|
||||
},
|
||||
latestDate: function() {
|
||||
var selectedStudies = WorklistSelectedStudies.find({}, {
|
||||
@ -121,12 +127,11 @@ Template.studyAssociationTable.helpers({
|
||||
studyDate: 1
|
||||
}
|
||||
}).fetch();
|
||||
var range = getDateRange(selectedStudies);
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
return range.latestDate;
|
||||
var range = getDateRange(selectedStudies);
|
||||
if (range) {
|
||||
return range.latestDate;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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)
|
||||
*/
|
||||
@ -4,7 +4,7 @@
|
||||
<span class="loading"><i class="fa fa-spinner fa-spin"></i></span>
|
||||
<select id="selectStudyDate">
|
||||
{{#each relatedStudies}}
|
||||
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}}</option>
|
||||
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}} ({{timepointName}})</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -4,21 +4,22 @@ Template.studyDateList.helpers({
|
||||
* The value for 'selected' for the currently loaded study is set to true, so that
|
||||
* 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() {
|
||||
// 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
|
||||
var currentStudyInBrowser = ViewerStudies.findOne({
|
||||
selected: true
|
||||
});
|
||||
|
||||
// Find studies which have the same patientId as the currently selected study
|
||||
var relatedStudies = WorklistStudies.find({patientId: currentStudyInBrowser.patientId},
|
||||
{sort: {studyDate: 1}}).fetch();
|
||||
// Find all Timepoint-associated studies which have the same patientId as the currently selected study
|
||||
var relatedStudies = Studies.find({
|
||||
patientId: currentStudyInBrowser.patientId
|
||||
}, {
|
||||
sort: {
|
||||
studyDate: 1
|
||||
}
|
||||
});
|
||||
|
||||
// Modify the array of related studies so the default option is the currently selected study
|
||||
relatedStudies.forEach(function(study) {
|
||||
@ -31,6 +32,26 @@ Template.studyDateList.helpers({
|
||||
|
||||
// Use this array to populate the combo box
|
||||
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) {
|
||||
var selectBox = $(e.currentTarget);
|
||||
|
||||
var studyInstanceUid = selectBox.val();
|
||||
|
||||
// Hide the select box
|
||||
@ -57,6 +77,11 @@ Template.studyDateList.events({
|
||||
loadingIndicator.css('display', 'block');
|
||||
|
||||
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
|
||||
if (error) {
|
||||
log.warn(error);
|
||||
return;
|
||||
}
|
||||
|
||||
sortStudy(study);
|
||||
|
||||
// Hide the loading indicator
|
||||
@ -66,21 +91,20 @@ Template.studyDateList.events({
|
||||
selectBox.css('display', 'block');
|
||||
|
||||
// Set "Selected" to false for the entire collection
|
||||
ViewerStudies.update({},
|
||||
{
|
||||
$set: {
|
||||
selected: false
|
||||
}
|
||||
},
|
||||
{
|
||||
multi: true
|
||||
});
|
||||
ViewerStudies.update({}, {
|
||||
$set: {
|
||||
selected: false
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// Check if this study already exists in the ViewerStudies collection
|
||||
// of loaded studies. If it does, set it's 'selected' value to true.
|
||||
var existingStudy = ViewerStudies.findOne({
|
||||
studyInstanceUid: studyInstanceUid
|
||||
});
|
||||
|
||||
if (existingStudy) {
|
||||
// Set the current finding in the collection to true
|
||||
ViewerStudies.update(existingStudy._id, {
|
||||
@ -95,29 +119,6 @@ Template.studyDateList.events({
|
||||
// with the value True, and insert it into the ViewerStudies Collection
|
||||
study.selected = true;
|
||||
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
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
202
Packages/lesiontracker/lib/LesionManager.js
Normal file
202
Packages/lesiontracker/lib/LesionManager.js
Normal 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
|
||||
};
|
||||
388
Packages/lesiontracker/lib/TrialCriteriaConstraints.js
Normal file
388
Packages/lesiontracker/lib/TrialCriteriaConstraints.js
Normal 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];
|
||||
};
|
||||
343
Packages/lesiontracker/lib/TrialResponseCriteria.js
Normal file
343
Packages/lesiontracker/lib/TrialResponseCriteria.js
Normal 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
|
||||
};
|
||||
@ -12,7 +12,7 @@ activateMeasurements = function(element, measurementId, templateData, viewportIn
|
||||
var timepointData = getTimepointObject(imageId);
|
||||
var measurementData = Measurements.findOne(measurementId);
|
||||
|
||||
var measurementAtTimepoint = measurementData.timepoints[timepointData.timepointID];
|
||||
var measurementAtTimepoint = measurementData.timepoints[timepointData.timepointId];
|
||||
if (!measurementAtTimepoint) {
|
||||
return;
|
||||
}
|
||||
@ -35,11 +35,11 @@ activateMeasurements = function(element, measurementId, templateData, viewportIn
|
||||
}
|
||||
|
||||
if (imageIdIndex === elementCurrentImageIdIndex) {
|
||||
activateTool(element, measurementData, timepointData.timepointID);
|
||||
activateTool(element, measurementData, timepointData.timepointId);
|
||||
} else {
|
||||
cornerstone.loadAndCacheImage(imageIds[imageIdIndex]).then(function(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 measurementData
|
||||
* @param timepointID
|
||||
* @param timepointId
|
||||
*/
|
||||
function activateTool(element, measurementData, timepointID) {
|
||||
function activateTool(element, measurementData, timepointId) {
|
||||
deactivateAllToolData(element, 'lesion');
|
||||
deactivateAllToolData(element, 'nonTarget');
|
||||
|
||||
@ -62,7 +62,7 @@ function activateTool(element, measurementData, timepointID) {
|
||||
return;
|
||||
}
|
||||
|
||||
var measurementAtTimepoint = measurementData.timepoints[timepointID];
|
||||
var measurementAtTimepoint = measurementData.timepoints[timepointId];
|
||||
|
||||
for (var i = 0; i < toolData.data.length; i++) {
|
||||
data = toolData.data[i];
|
||||
|
||||
59
Packages/lesiontracker/lib/calculateTotalLesionBurden.js
Normal file
59
Packages/lesiontracker/lib/calculateTotalLesionBurden.js
Normal 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;
|
||||
};
|
||||
@ -19,12 +19,11 @@ clearMeasurementTimepointData = function(measurementId, 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) {
|
||||
if (error) {
|
||||
log.warn(error);
|
||||
}
|
||||
console.log('Removed!');
|
||||
});
|
||||
} else {
|
||||
// Update the Timepoint object of the Measurement document
|
||||
|
||||
@ -35,4 +35,7 @@ clearTools = function() {
|
||||
|
||||
// Remove patient's measurements
|
||||
Meteor.call('removeMeasurementsByPatientId', patientId);
|
||||
|
||||
// Clear all validation errors
|
||||
ValidationErrors.remove({});
|
||||
};
|
||||
|
||||
44
Packages/lesiontracker/lib/getTimepointName.js
Normal file
44
Packages/lesiontracker/lib/getTimepointName.js
Normal 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;
|
||||
};
|
||||
@ -11,6 +11,8 @@ getTimepointObject = function(imageId) {
|
||||
}
|
||||
|
||||
return Timepoints.findOne({
|
||||
timepointName: study.studyDate
|
||||
studyInstanceUids: {
|
||||
$in: [study.studyInstanceUid]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
12
Packages/lesiontracker/lib/handleMeasurementAdded.js
Normal file
12
Packages/lesiontracker/lib/handleMeasurementAdded.js
Normal 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;
|
||||
}
|
||||
};
|
||||
12
Packages/lesiontracker/lib/handleMeasurementModified.js
Normal file
12
Packages/lesiontracker/lib/handleMeasurementModified.js
Normal 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;
|
||||
}
|
||||
};
|
||||
19
Packages/lesiontracker/lib/handleMeasurementRemoved.js
Normal file
19
Packages/lesiontracker/lib/handleMeasurementRemoved.js
Normal 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;
|
||||
}
|
||||
};
|
||||
30
Packages/lesiontracker/lib/pixelSpacingAutorunCheck.js
Normal file
30
Packages/lesiontracker/lib/pixelSpacingAutorunCheck.js
Normal 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);
|
||||
}
|
||||
};
|
||||
75
Packages/lesiontracker/lib/syncMeasurementAndToolData.js
Normal file
75
Packages/lesiontracker/lib/syncMeasurementAndToolData.js
Normal 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);
|
||||
});
|
||||
};
|
||||
13
Packages/lesiontracker/lib/updateRelatedElements.js
Normal file
13
Packages/lesiontracker/lib/updateRelatedElements.js
Normal 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);
|
||||
});
|
||||
};
|
||||
@ -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);
|
||||
108
Packages/lesiontracker/lib/worklist/openNewTabWithTimepoint.js
Normal file
108
Packages/lesiontracker/lib/worklist/openNewTabWithTimepoint.js
Normal 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');
|
||||
}
|
||||
37
Packages/lesiontracker/lib/worklist/worklistModification.js
Normal file
37
Packages/lesiontracker/lib/worklist/worklistModification.js
Normal 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);
|
||||
}
|
||||
@ -10,16 +10,28 @@ Package.onUse(function(api) {
|
||||
api.use('standard-app-packages');
|
||||
api.use('jquery');
|
||||
api.use('stylus');
|
||||
|
||||
// Control over logging
|
||||
api.use('practicalmeteor:loglevel');
|
||||
|
||||
// Unique IDs
|
||||
api.use('rwatts:uuid');
|
||||
|
||||
// Template overriding
|
||||
api.use('aldeed:template-extension@4.0.0');
|
||||
|
||||
// Our custom package
|
||||
api.use('worklist');
|
||||
api.use('cornerstone');
|
||||
|
||||
api.addFiles('log.js', [ 'client', 'server' ]);
|
||||
|
||||
// Client-side collections
|
||||
api.addFiles('client/collections/LesionLocations.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', {
|
||||
bare: true
|
||||
});
|
||||
@ -32,14 +44,27 @@ Package.onUse(function(api) {
|
||||
api.addFiles('client/compatibility/deleteLesionKeyboardTool.js', 'client', {
|
||||
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
|
||||
});
|
||||
|
||||
// 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.styl', '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.js', '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/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.js', 'client');
|
||||
|
||||
api.addFiles('client/components/nonTargetLesionDialog/nonTargetLesionDialog.html', '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.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.styl', '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.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
|
||||
api.addFiles('server/collections.js', 'server');
|
||||
api.addFiles('server/removeCollections.js', [ 'server' ]);
|
||||
@ -89,34 +129,67 @@ Package.onUse(function(api) {
|
||||
// Both client and server functions
|
||||
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
|
||||
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/clearMeasurementTimepointData.js', 'client');
|
||||
api.addFiles('lib/removeToolDataWithMeasurementId.js', 'client');
|
||||
api.addFiles('lib/getTimepointObject.js', 'client');
|
||||
api.addFiles('lib/getTimepointName.js', 'client');
|
||||
api.addFiles('lib/activateMeasurements.js', 'client');
|
||||
api.addFiles('lib/activateLesion.js', 'client');
|
||||
api.addFiles('lib/deactivateAllToolData.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
|
||||
api.export('activateLesion','client');
|
||||
api.export('activateMeasurements','client');
|
||||
api.export('deactivateAllToolData','client');
|
||||
api.export('pixelSpacingAutorunCheck', 'client');
|
||||
api.export('handleMeasurementAdded', '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('clearMeasurementTimepointData', 'client');
|
||||
api.export('removeToolDataWithMeasurementId', 'client');
|
||||
api.export('getTimepointObject', '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');
|
||||
|
||||
// Export client-side collections
|
||||
api.export('ValidationErrors', 'client');
|
||||
api.export('LesionLocations', 'client');
|
||||
api.export('LocationResponses', 'client');
|
||||
api.export('PatientLocations', 'client');
|
||||
|
||||
// Export collections spanning both client and server
|
||||
api.export('Measurements', [ 'client', 'server' ]);
|
||||
api.export('Studies', [ 'client', 'server' ]);
|
||||
api.export('Timepoints', [ 'client', 'server' ]);
|
||||
});
|
||||
|
||||
@ -1,10 +1,18 @@
|
||||
Meteor.publish('timepoints', function(patientId) {
|
||||
Meteor.publish('timepoints', function() {
|
||||
return Timepoints.find();
|
||||
});
|
||||
|
||||
Meteor.publish('singlePatientTimepoints', function(patientId) {
|
||||
return Timepoints.find({
|
||||
patientId: patientId
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.publish('measurements', function(patientId) {
|
||||
Meteor.publish('studies', function() {
|
||||
return Studies.find();
|
||||
});
|
||||
|
||||
Meteor.publish('singlePatientMeasurements', function(patientId) {
|
||||
return Measurements.find({
|
||||
patientId: patientId
|
||||
});
|
||||
|
||||
@ -43,5 +43,11 @@ Meteor.methods({
|
||||
Measurements.remove({
|
||||
patientId: patientId
|
||||
});
|
||||
},
|
||||
clearAllTimepoints: function() {
|
||||
Timepoints.remove({});
|
||||
},
|
||||
removeTimepoint: function(id) {
|
||||
Timepoints.remove(id);
|
||||
}
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ function getElementIfNotEmpty(viewportIndex) {
|
||||
element = imageViewerViewports.get(viewportIndex),
|
||||
canvases = imageViewerViewports.eq(viewportIndex).find('canvas');
|
||||
|
||||
if (!element || $(element).hasClass("empty") || canvases.length === 0) {
|
||||
if (!element || $(element).hasClass('empty') || canvases.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ function getElementIfNotEmpty(viewportIndex) {
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
@ -28,10 +29,12 @@ function getPatient(property) {
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var patient = cornerstoneTools.metaData.get('patient', this.imageId);
|
||||
if (!patient) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return patient[property];
|
||||
}
|
||||
|
||||
@ -40,10 +43,12 @@ function getStudy(property) {
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var study = cornerstoneTools.metaData.get('study', this.imageId);
|
||||
if (!study) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return study[property];
|
||||
}
|
||||
|
||||
@ -52,10 +57,12 @@ function getSeries(property) {
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var series = cornerstoneTools.metaData.get('series', this.imageId);
|
||||
if (!series) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return series[property];
|
||||
}
|
||||
|
||||
@ -64,10 +71,12 @@ function getInstance(property) {
|
||||
if (!this.imageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var instance = cornerstoneTools.metaData.get('instance', this.imageId);
|
||||
if (!instance) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return instance[property];
|
||||
}
|
||||
|
||||
@ -76,15 +85,18 @@ function getImage(viewportIndex) {
|
||||
if (!element) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var enabledElement;
|
||||
try {
|
||||
enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!enabledElement || !enabledElement.image) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return enabledElement.image;
|
||||
}
|
||||
|
||||
@ -95,10 +107,12 @@ Template.viewportOverlay.helpers({
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'W ' + viewport.voi.windowWidth.toFixed(0) + ' L ' + viewport.voi.windowCenter.toFixed(0);
|
||||
},
|
||||
zoom: function() {
|
||||
@ -107,10 +121,12 @@ Template.viewportOverlay.helpers({
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (viewport.scale * 100.0);
|
||||
},
|
||||
imageDimensions: function() {
|
||||
@ -118,41 +134,42 @@ Template.viewportOverlay.helpers({
|
||||
|
||||
var image = getImage(this.viewportIndex);
|
||||
if (!image) {
|
||||
return '';
|
||||
return '';
|
||||
}
|
||||
|
||||
return image.width + ' x ' + image.height;
|
||||
},
|
||||
patientName : function() {
|
||||
patientName: function() {
|
||||
return getPatient.call(this, 'name');
|
||||
},
|
||||
patientId : function() {
|
||||
patientId: function() {
|
||||
return getPatient.call(this, 'id');
|
||||
},
|
||||
studyDate : function() {
|
||||
studyDate: function() {
|
||||
return getStudy.call(this, 'studyDate');
|
||||
},
|
||||
studyTime : function() {
|
||||
studyTime: function() {
|
||||
return getStudy.call(this, 'studyTime');
|
||||
},
|
||||
studyDescription : function() {
|
||||
studyDescription: function() {
|
||||
return getStudy.call(this, 'studyDescription');
|
||||
},
|
||||
seriesDescription : function() {
|
||||
seriesDescription: function() {
|
||||
return getSeries.call(this, 'seriesDescription');
|
||||
},
|
||||
seriesNumber : function() {
|
||||
seriesNumber: function() {
|
||||
return getSeries.call(this, 'seriesNumber');
|
||||
},
|
||||
imageNumber : function() {
|
||||
imageNumber: function() {
|
||||
return getInstance.call(this, 'instanceNumber');
|
||||
},
|
||||
imageIndex : function() {
|
||||
imageIndex: function() {
|
||||
return getInstance.call(this, 'index');
|
||||
},
|
||||
numImages : function() {
|
||||
numImages: function() {
|
||||
return getSeries.call(this, 'numImages');
|
||||
},
|
||||
prior : function() {
|
||||
prior: function() {
|
||||
// This helper is updated whenever a new image is displayed in the viewport
|
||||
Session.get('CornerstoneNewImage' + this.viewportIndex);
|
||||
if (!this.imageId) {
|
||||
@ -165,15 +182,19 @@ Template.viewportOverlay.helpers({
|
||||
// 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
|
||||
var viewportStudies = ViewerStudies.find({}, {sort: {studyDate: 1}});
|
||||
var viewportStudies = ViewerStudies.find({}, {
|
||||
sort: {
|
||||
studyDate: 1
|
||||
}
|
||||
});
|
||||
if (viewportStudies.count() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get study data
|
||||
var study = cornerstoneTools.metaData.get('study', this.imageId);
|
||||
if (study.studyInstanceUid === viewportStudies.fetch()[0].studyInstanceUid) {
|
||||
if (study.studyDate === viewportStudies.fetch()[0].studyDate) {
|
||||
return 'Prior';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
.imageViewerViewportOverlay
|
||||
font-family: Helvetica, OpenSans
|
||||
font-size: 13px
|
||||
color: #e4ad00
|
||||
color: #979797
|
||||
line-height: 18px
|
||||
display: none // Shown when an image is loaded
|
||||
|
||||
|
||||
@ -7,40 +7,55 @@ Meteor.startup(function() {
|
||||
}
|
||||
|
||||
OHIF.viewer.defaultHotkeys = {
|
||||
defaultTool: "ESC",
|
||||
angle: "A",
|
||||
stackScroll: "S",
|
||||
pan: "P",
|
||||
magnify: "M",
|
||||
scrollDown: ["DOWN", "PAGEDOWN"],
|
||||
scrollUp: ["UP", "PAGEUP"],
|
||||
nextPanel: "RIGHT",
|
||||
previousPanel: "LEFT",
|
||||
invert: "I",
|
||||
flipV: "V",
|
||||
flipH: "H",
|
||||
wwwc: "W",
|
||||
zoom: "Z",
|
||||
cinePlay: "SPACE",
|
||||
rotateR: "R",
|
||||
rotateL: "L",
|
||||
defaultTool: 'ESC',
|
||||
angle: 'A',
|
||||
stackScroll: 'S',
|
||||
pan: 'P',
|
||||
magnify: 'M',
|
||||
scrollDown: ['DOWN', 'PAGEDOWN'],
|
||||
scrollUp: ['UP', 'PAGEUP'],
|
||||
nextPanel: 'RIGHT',
|
||||
previousPanel: 'LEFT',
|
||||
invert: 'I',
|
||||
flipV: 'V',
|
||||
flipH: 'H',
|
||||
wwwc: 'W',
|
||||
zoom: 'Z',
|
||||
cinePlay: 'SPACE',
|
||||
rotateR: 'R',
|
||||
rotateL: 'L',
|
||||
toggleOverlayTags: 'SHIFT',
|
||||
WLPresetSoftTissue: ["NUMPAD1", "1"],
|
||||
WLPresetLung: ["NUMPAD2", "2"],
|
||||
WLPresetLiver: ["NUMPAD3", "3"],
|
||||
WLPresetBone: ["NUMPAD4", "4"],
|
||||
WLPresetBrain: ["NUMPAD5", "5"]
|
||||
WLPresetSoftTissue: ['NUMPAD1', '1'],
|
||||
WLPresetLung: ['NUMPAD2', '2'],
|
||||
WLPresetLiver: ['NUMPAD3', '3'],
|
||||
WLPresetBone: ['NUMPAD4', '4'],
|
||||
WLPresetBrain: ['NUMPAD5', '5']
|
||||
};
|
||||
|
||||
// For now
|
||||
OHIF.viewer.hotkeys = OHIF.viewer.defaultHotkeys;
|
||||
|
||||
OHIF.viewer.defaultWLPresets = {
|
||||
'SoftTissue' : {wc : 40, ww : 400},
|
||||
'Lung' : {wc : -600, ww : 1500},
|
||||
'Liver' : {wc : 90, ww : 150},
|
||||
'Bone' : {wc: 480, ww : 2500},
|
||||
'Brain' : {wc : 40, ww : 80}
|
||||
SoftTissue: {
|
||||
wc: 40,
|
||||
ww: 400
|
||||
},
|
||||
Lung: {
|
||||
wc: -600,
|
||||
ww: 1500
|
||||
},
|
||||
Liver: {
|
||||
wc: 90,
|
||||
ww: 150
|
||||
},
|
||||
Bone: {
|
||||
wc: 480,
|
||||
ww: 2500
|
||||
},
|
||||
Brain: {
|
||||
wc: 40,
|
||||
ww: 80
|
||||
}
|
||||
};
|
||||
|
||||
// For now
|
||||
@ -48,53 +63,53 @@ Meteor.startup(function() {
|
||||
|
||||
OHIF.viewer.hotkeyFunctions = {
|
||||
wwwc: function() {
|
||||
toolManager.setActiveTool("wwwc");
|
||||
toolManager.setActiveTool('wwwc');
|
||||
},
|
||||
zoom: function() {
|
||||
toolManager.setActiveTool("zoom");
|
||||
toolManager.setActiveTool('zoom');
|
||||
},
|
||||
angle: function() {
|
||||
toolManager.setActiveTool("angle");
|
||||
toolManager.setActiveTool('angle');
|
||||
},
|
||||
dragProbe: function() {
|
||||
toolManager.setActiveTool("dragProbe");
|
||||
toolManager.setActiveTool('dragProbe');
|
||||
},
|
||||
ellipticalRoi: function() {
|
||||
toolManager.setActiveTool("ellipticalRoi");
|
||||
toolManager.setActiveTool('ellipticalRoi');
|
||||
},
|
||||
magnify: function() {
|
||||
toolManager.setActiveTool("magnify");
|
||||
toolManager.setActiveTool('magnify');
|
||||
},
|
||||
annotate: function() {
|
||||
toolManager.setActiveTool("annotate");
|
||||
toolManager.setActiveTool('annotate');
|
||||
},
|
||||
stackScroll: function() {
|
||||
toolManager.setActiveTool("stackScroll");
|
||||
toolManager.setActiveTool('stackScroll');
|
||||
},
|
||||
pan: function() {
|
||||
toolManager.setActiveTool("pan");
|
||||
toolManager.setActiveTool('pan');
|
||||
},
|
||||
length: function() {
|
||||
toolManager.setActiveTool("length");
|
||||
toolManager.setActiveTool('length');
|
||||
},
|
||||
spine: function() {
|
||||
toolManager.setActiveTool("spine");
|
||||
toolManager.setActiveTool('spine');
|
||||
},
|
||||
wwwcRegion: function() {
|
||||
toolManager.setActiveTool("wwwcRegion");
|
||||
toolManager.setActiveTool('wwwcRegion');
|
||||
},
|
||||
zoomIn: function () {
|
||||
var button = document.getElementById("zoomIn");
|
||||
zoomIn: function() {
|
||||
var button = document.getElementById('zoomIn');
|
||||
flashButton(button);
|
||||
zoomIn();
|
||||
},
|
||||
zoomOut: function () {
|
||||
var button = document.getElementById("zoomOut");
|
||||
zoomOut: function() {
|
||||
var button = document.getElementById('zoomOut');
|
||||
flashButton(button);
|
||||
zoomOut();
|
||||
},
|
||||
zoomToFit: function () {
|
||||
var button = document.getElementById("zoomToFit");
|
||||
zoomToFit: function() {
|
||||
var button = document.getElementById('zoomToFit');
|
||||
flashButton(button);
|
||||
zoomToFit();
|
||||
},
|
||||
@ -134,27 +149,27 @@ Meteor.startup(function() {
|
||||
previousActivePanel();
|
||||
},
|
||||
invert: function() {
|
||||
var button = document.getElementById("invert");
|
||||
var button = document.getElementById('invert');
|
||||
flashButton(button);
|
||||
invert();
|
||||
},
|
||||
flipV: function() {
|
||||
var button = document.getElementById("flipV");
|
||||
var button = document.getElementById('flipV');
|
||||
flashButton(button);
|
||||
flipV();
|
||||
},
|
||||
flipH: function() {
|
||||
var button = document.getElementById("flipH");
|
||||
var button = document.getElementById('flipH');
|
||||
flashButton(button);
|
||||
flipH();
|
||||
},
|
||||
rotateR: function() {
|
||||
var button = document.getElementById("rotateR");
|
||||
var button = document.getElementById('rotateR');
|
||||
flashButton(button);
|
||||
rotateR();
|
||||
},
|
||||
rotateL: function() {
|
||||
var button = document.getElementById("rotateL");
|
||||
var button = document.getElementById('rotateL');
|
||||
flashButton(button);
|
||||
rotateL();
|
||||
},
|
||||
@ -174,6 +189,8 @@ Meteor.startup(function() {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
OHIF.viewer.loadedSeriesData = {};
|
||||
});
|
||||
|
||||
// Define a jQuery reverse function
|
||||
@ -198,7 +215,6 @@ function previousActivePanel() {
|
||||
setActiveViewport(element);
|
||||
}
|
||||
|
||||
|
||||
function nextActivePanel() {
|
||||
log.info('nextActivePanel');
|
||||
var currentIndex = Session.get('activeViewport');
|
||||
@ -224,7 +240,7 @@ function flashButton(button) {
|
||||
}
|
||||
|
||||
button.classList.add('active');
|
||||
setTimeout(function () {
|
||||
setTimeout(function() {
|
||||
button.classList.remove('active');
|
||||
}, 100);
|
||||
}
|
||||
@ -233,13 +249,13 @@ function bindHotkey(hotkey, task) {
|
||||
var hotkeyFunctions = OHIF.viewer.hotkeyFunctions;
|
||||
|
||||
// Only bind defined, non-empty HotKeys
|
||||
if (!hotkey || hotkey === "") {
|
||||
if (!hotkey || hotkey === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
var fn;
|
||||
if (task.indexOf("WLPreset") > -1) {
|
||||
var presetName = task.replace("WLPreset", "");
|
||||
if (task.indexOf('WLPreset') > -1) {
|
||||
var presetName = task.replace('WLPreset', '');
|
||||
fn = function() {
|
||||
applyWLPresetToActiveElement(presetName);
|
||||
};
|
||||
@ -282,4 +298,4 @@ enableHotkeys = function() {
|
||||
bindHotkey(taskHotkeys, task);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@ -29,8 +29,14 @@ getWADORSImageId = function(instance) {
|
||||
//maxPixelValue : 255,
|
||||
slope: instance.rescaleSlope,
|
||||
intercept: instance.rescaleIntercept,
|
||||
windowCenter : windowCenter,
|
||||
windowWidth : windowWidth,
|
||||
samplesPerPixel: instance.samplesPerPixel,
|
||||
imageOrientationPatient: instance.imageOrientationPatient,
|
||||
imagePositionPatient: instance.imagePositionPatient,
|
||||
sopClassUid: instance.sopClassUid,
|
||||
instanceNumber: instance.instanceNumber,
|
||||
frameOfReferenceUID: instance.frameOfReferenceUID,
|
||||
windowCenter: windowCenter,
|
||||
windowWidth: windowWidth,
|
||||
//render: cornerstone.renderColorImage,
|
||||
//getPixelData: getPixelData,
|
||||
//getImageData: getImageData,
|
||||
@ -47,6 +53,8 @@ getWADORSImageId = function(instance) {
|
||||
instance: instance
|
||||
};
|
||||
|
||||
var imageId = cornerstoneWADORSImageLoader.addImage(image);
|
||||
var imageId = cornerstoneWADOImageLoader.imageManager.add(image);
|
||||
|
||||
console.log('WADO-RS ImageID: ' + imageId);
|
||||
return imageId;
|
||||
};
|
||||
};
|
||||
|
||||
@ -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
|
||||
*/
|
||||
UI.registerHelper('formatPN', function (context) {
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
return context.replace('^', ', ');
|
||||
});
|
||||
UI.registerHelper('formatPN', formatPN);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user