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