Updates for Timepoint assocation (LT-60) and Conformance Checks (LT-92, LT-93, LT-94)
This commit is contained in:
1 parent
6c4eae3699
commit
beaa56c0b3
144 files changed
+5050
-2416
No files matched your search
@@ -10,8 +10,8 @@
|
||||
{{ >studyAssociationTable }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="saveAssociations" data-dismiss="modal" data-toggle="modal" data-target="#associationModal">Save</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="saveAssociations">Save</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" id='cancelAssociation'>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,116 @@
|
||||
Template.associationModal.events({
|
||||
'click #saveAssociations': function(e) {
|
||||
log.info("Saving associations");
|
||||
log.info('Saving associations');
|
||||
|
||||
// Close the modal
|
||||
var saveButton = $(e.currentTarget);
|
||||
saveButton.attr('disabled', true);
|
||||
saveButton.addClass('btn-success').removeClass('btn-primary');
|
||||
|
||||
// Find the rows of the study association table
|
||||
var tableRows = $('#studyAssociationTable table tbody tr');
|
||||
|
||||
// Create an empty object to group studies into
|
||||
var studies = {};
|
||||
|
||||
// Loop through each row to parse the data
|
||||
tableRows.each(function() {
|
||||
// Get a selector for this row
|
||||
var row = $(this);
|
||||
|
||||
// Check the includeStudy checkbox to see if we should parse this row
|
||||
var includeStudy = row.find('input.includeStudy[type="checkbox"]').eq(0).prop('checked');
|
||||
if (!includeStudy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the selected timepoint option for this study
|
||||
var timepointInput = row.find('input.timepointOption[type="radio"]:checked');
|
||||
|
||||
// Find the related label and trim it down to actual label (TODO: do this another way)
|
||||
var timepointType = timepointInput.val();
|
||||
|
||||
// Get the study metaData by checking the row with the template engine Blaze
|
||||
var data = Blaze.getData(this);
|
||||
|
||||
// Concatenate the study data to an array, depending on whether is was marked as baseline
|
||||
// or follow-up
|
||||
if (!studies.hasOwnProperty(timepointType)) {
|
||||
studies[timepointType] = [];
|
||||
}
|
||||
|
||||
studies[timepointType].push(data);
|
||||
});
|
||||
|
||||
Object.keys(studies).forEach(function(timepointType) {
|
||||
// Get the studies associated with this timepoint
|
||||
var relatedStudies = studies[timepointType];
|
||||
|
||||
// Create an array of all the studyInstanceUids for storage in the Timepoint
|
||||
var studyInstanceUids = relatedStudies.map(function(study) {
|
||||
return study.studyInstanceUid;
|
||||
});
|
||||
|
||||
// Create an array of all the studyDates for storage in the Timepoint
|
||||
var studyDates = relatedStudies.map(function(study) {
|
||||
return moment(study.studyDate, 'YYYYMMDD');
|
||||
});
|
||||
|
||||
// Sort the study dates, so we can get a range for these values
|
||||
studyDates = studyDates.sort();
|
||||
|
||||
// Create a new timepoint to represent the (baseline or follow-up) studies
|
||||
var timepoint = {
|
||||
timepointType: timepointType,
|
||||
timepointId: uuid.new(),
|
||||
studyInstanceUids: studyInstanceUids,
|
||||
patientId: relatedStudies[0].patientId, // TODO: Revisit this (Should timepoints be related to patientId?)
|
||||
earliestDate: studyDates[0].format('YYYYMMDD'),
|
||||
latestDate: studyDates[studyDates.length - 1].format('YYYYMMDD')
|
||||
};
|
||||
|
||||
// Insert this timepoint into the Timepoints Collection
|
||||
Timepoints.insert(timepoint);
|
||||
|
||||
// Loop through these studies to associate them with the newly created timepoint
|
||||
relatedStudies.forEach(function(study) {
|
||||
// Check if a study already exists in the Studies collection
|
||||
var existingStudy = Studies.findOne({
|
||||
studyInstanceUid: study.studyInstanceUid
|
||||
});
|
||||
|
||||
if (existingStudy) {
|
||||
// If a study already exists, update the entry with the new timepointId
|
||||
Studies.update(existingStudy._id, {
|
||||
$set: {
|
||||
timepointId: timepoint.timepointId
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If no such study exists, update the entry with the new timepointId
|
||||
|
||||
// Clear the ID from the document
|
||||
delete study._id;
|
||||
|
||||
// Attach the timepointId and insert it into the Studies Collection
|
||||
study.timepointId = timepoint.timepointId;
|
||||
Studies.insert(study);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Hide the modal
|
||||
$('#associationModal').modal('hide');
|
||||
|
||||
// Reset the save button to its normal state
|
||||
saveButton.removeClass('btn-success').addClass('btn-primary');
|
||||
saveButton.attr('disabled', false);
|
||||
},
|
||||
'click #cancelAssociation': function() {
|
||||
// When the modal is closed, we should reset
|
||||
// the save button to its normal state
|
||||
var saveButton = $('#saveAssociations');
|
||||
saveButton.removeClass('btn-success').addClass('btn-primary');
|
||||
saveButton.attr('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
+7
@@ -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>
|
||||
+11
@@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
+19
@@ -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
|
||||
+13
-4
@@ -11,24 +11,29 @@ function closeHandler(dialog) {
|
||||
|
||||
// This event sets lesion number for new lesion
|
||||
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
|
||||
// Get the current element's timepointID from the study date metadata
|
||||
// Get the current element's timepointId from the study date metadata
|
||||
var element = eventData.element;
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
var imageId = enabledElement.image.imageId;
|
||||
|
||||
var study = cornerstoneTools.metaData.get('study', imageId);
|
||||
|
||||
// Find the relevant timepoint given the current study
|
||||
var timepoint = Timepoints.findOne({
|
||||
timepointName: study.studyDate
|
||||
studyInstanceUids: {
|
||||
$in: [study.studyInstanceUid]
|
||||
}
|
||||
});
|
||||
|
||||
if (!timepoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
measurementData.timepointID = timepoint.timepointID;
|
||||
measurementData.timepointId = timepoint.timepointId;
|
||||
|
||||
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
|
||||
// exists at a different timepoint
|
||||
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = true);
|
||||
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = true);
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
|
||||
// Set lesion number
|
||||
@@ -226,6 +231,9 @@ Template.lesionLocationDialog.events({
|
||||
/// Set the isTarget value to true, since this is the target-lesion dialog callback
|
||||
measurementData.isTarget = true;
|
||||
|
||||
// Set the isNodal value based on the Location's properties
|
||||
measurementData.isNodal = locationObj.isNodal;
|
||||
|
||||
// Adds lesion data to timepoints array
|
||||
LesionManager.updateLesionData(measurementData);
|
||||
} else {
|
||||
@@ -233,6 +241,7 @@ Template.lesionLocationDialog.events({
|
||||
$set: {
|
||||
location: locationObj.location,
|
||||
locationId: locationObj.id,
|
||||
isNodal: locationObj.isNodal,
|
||||
locationUID: id
|
||||
}
|
||||
});
|
||||
|
||||
+7
-7
@@ -5,18 +5,18 @@ Template.lesionTableTimepointCell.helpers({
|
||||
var lesionData = Template.parentData(1);
|
||||
return (lesionData &&
|
||||
lesionData.timepoints &&
|
||||
lesionData.timepoints[this.timepointID]);
|
||||
lesionData.timepoints[this.timepointId]);
|
||||
},
|
||||
displayData: function() {
|
||||
// Search Measurements by lesion and timepoint
|
||||
var lesionData = Template.parentData(1);
|
||||
if (!lesionData ||
|
||||
!lesionData.timepoints ||
|
||||
!lesionData.timepoints[this.timepointID]) {
|
||||
!lesionData.timepoints[this.timepointId]) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = lesionData.timepoints[this.timepointID];
|
||||
var data = lesionData.timepoints[this.timepointId];
|
||||
|
||||
if (lesionData.isTarget === true) {
|
||||
if (data.shortestDiameter) {
|
||||
@@ -40,7 +40,7 @@ function doneCallback(measurementData, deleteTool) {
|
||||
// the specified Timepoint Cell
|
||||
if (deleteTool === true) {
|
||||
log.info('Confirm clicked!');
|
||||
clearMeasurementTimepointData(measurementData.id, measurementData.timepointID);
|
||||
clearMeasurementTimepointData(measurementData.id, measurementData.timepointId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +57,12 @@ Template.lesionTableTimepointCell.events({
|
||||
var currentMeasurement = Template.parentData(1);
|
||||
|
||||
// Create some fake measurement data
|
||||
var currentTimepointID = this.timepointID;
|
||||
var currentTimepointID = this.timepointId;
|
||||
|
||||
var timepointData = currentMeasurement.timepoints[currentTimepointID];
|
||||
var measurementData = {
|
||||
id: currentMeasurement._id,
|
||||
timepointID: currentTimepointID,
|
||||
timepointId: currentTimepointID,
|
||||
response: timepointData.response,
|
||||
imageId: timepointData.imageId,
|
||||
handles: timepointData.handles,
|
||||
@@ -83,7 +83,7 @@ Template.lesionTableTimepointCell.events({
|
||||
if (keyCode === keys.DELETE ||
|
||||
(keyCode === keys.D && e.ctrlKey === true)) {
|
||||
var currentMeasurement = Template.parentData(1);
|
||||
var currentTimepointID = this.timepointID;
|
||||
var currentTimepointID = this.timepointId;
|
||||
|
||||
showConfirmDialog(function() {
|
||||
clearMeasurementTimepointData(currentMeasurement._id, currentTimepointID);
|
||||
|
||||
+3
-6
@@ -1,9 +1,6 @@
|
||||
<template name="lesionTableTimepointHeader">
|
||||
<th class="lesionTableTimepointCell">
|
||||
{{formatDA timepointName "MM/DD/YYYY"}}
|
||||
{{#if timepointTextFound}}
|
||||
{{timepointText}}
|
||||
{{/if}}
|
||||
{{#if timepointLoaded }}<span class="timepointIndicator"><</span> {{/if}}
|
||||
<th class="lesionTableTimepointCell" title="Latest study date: {{formatDA latestDate "MM/DD/YYYY"}}"
|
||||
data-toggle="tooltip" data-placement="top">
|
||||
{{ timepointName }}
|
||||
</th>
|
||||
</template>
|
||||
+4
-103
@@ -1,105 +1,6 @@
|
||||
Template.lesionTableTimepointHeader.events({
|
||||
'click th.lesionTableTimepointCell': function(e, template) {
|
||||
var parentPosition = getPosition(e.currentTarget);
|
||||
var cellText = e.currentTarget.innerText;
|
||||
|
||||
// Remove spaces in string
|
||||
cellText = cellText.replace(/\s/g, ''); // Remove spaces
|
||||
cellText = cellText.replace('<', ''); // Remove <
|
||||
var splitCellText = cellText.split('/');
|
||||
var dateStr = splitCellText[2].replace(/\D/g,'') + '' + splitCellText[0].replace(/\D/g,'') + '' + splitCellText[1].replace(/\D/g,''); // Remove non-digit chars
|
||||
|
||||
// Check patient has a timepointText as Baseline
|
||||
var patientId = template.data.patientId;
|
||||
|
||||
// Open popup
|
||||
var timepointTextDialog = $('#timepointTextDialog');
|
||||
var dialogDisplay = timepointTextDialog.css('display');
|
||||
if (dialogDisplay === 'none') {
|
||||
var isBaselineInCollection = Timepoints.findOne({
|
||||
patientId: patientId,
|
||||
timepointName: dateStr,
|
||||
timepointText: 'Baseline'
|
||||
});
|
||||
|
||||
// If isBaselineInCollection is true, "Baseline" is found in collection for patient and set checkbox as checked
|
||||
if (isBaselineInCollection) {
|
||||
// Set checkbox as checked
|
||||
$('#checkBoxBaseline').prop('checked', true);
|
||||
} else {
|
||||
// Set checkbox as unchecked
|
||||
$('#checkBoxBaseline').prop('checked', false);
|
||||
}
|
||||
|
||||
// Open dialog
|
||||
var dialogProperty = {
|
||||
top: parentPosition.y - 30,
|
||||
left: parentPosition.x,
|
||||
display: 'block'
|
||||
};
|
||||
|
||||
timepointTextDialog.css(dialogProperty);
|
||||
} else {
|
||||
// Get timepoints of patient
|
||||
// Set timepointText as Baseline for selected timepoint
|
||||
var timepoints = Timepoints.find({
|
||||
patientId: patientId
|
||||
}).fetch();
|
||||
|
||||
// Check checkbox is selected
|
||||
var checkboxBaselineChecked = $('#checkBoxBaseline').is(':checked');
|
||||
|
||||
timepoints.forEach(function(timepoint) {
|
||||
// timepointText defines a custom text for timepoint such as Baseline, Nadir, Current
|
||||
if (timepoint.timepointName === dateStr) {
|
||||
// If checkbox is selected, set timepointText as Baseline
|
||||
// Else set timepointText as ""
|
||||
var timepointText = 'Baseline';
|
||||
if (!checkboxBaselineChecked) {
|
||||
timepointText = '';
|
||||
}
|
||||
|
||||
Timepoints.update(timepoint._id,{
|
||||
$set: {
|
||||
timepointText: timepointText
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Set timepointText as empty
|
||||
Timepoints.update(timepoint._id,{
|
||||
$set: {
|
||||
timepointText: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Close dialog
|
||||
timepointTextDialog.css('display', 'none');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.lesionTableTimepointHeader.helpers({
|
||||
timepointTextFound: function() {
|
||||
var timepointText = this.timepointText;
|
||||
return (timepointText && timepointText === 'Baseline');
|
||||
'timepointName': function() {
|
||||
var timepoint = this;
|
||||
return getTimepointName(timepoint);
|
||||
}
|
||||
});
|
||||
|
||||
// Gets parent's position of element which mouse pointer is clicked in
|
||||
function getPosition(element) {
|
||||
var xPosition = 0;
|
||||
var yPosition = 0;
|
||||
|
||||
while (element) {
|
||||
xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft);
|
||||
yPosition += (element.offsetTop - element.scrollTop + element.clientTop);
|
||||
element = element.offsetParent;
|
||||
}
|
||||
|
||||
return {
|
||||
x: xPosition,
|
||||
y: yPosition
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
<template name="lesionTrackerLayout">
|
||||
<div class="logoContainer">
|
||||
<a class="navbar-brand" href="http://ohif.org">
|
||||
<img src="/images/logo.png">
|
||||
<h4 class="name">Open Health Imaging Foundation</h4>
|
||||
</a>
|
||||
</div>
|
||||
{{ >optionsButton }}
|
||||
{{ >yield }}
|
||||
</template>
|
||||
@@ -0,0 +1,38 @@
|
||||
.logoContainer
|
||||
position: absolute;
|
||||
height: 40px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: white;
|
||||
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
|
||||
width: 15%
|
||||
|
||||
.navbar-brand
|
||||
display: inline-block
|
||||
padding: 0
|
||||
margin-right: 5px
|
||||
height: 100%
|
||||
width: 100%
|
||||
|
||||
img
|
||||
height: 30px
|
||||
float: left
|
||||
margin-right: 10px
|
||||
margin-top: 5px;
|
||||
/* Media queries for iPhone6*/
|
||||
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
|
||||
height: 20px
|
||||
margin-top: 10px
|
||||
margin-bottom: 10px
|
||||
margin-right: 5px
|
||||
|
||||
h4.name
|
||||
float: left
|
||||
margin: 10px 0;
|
||||
font-family: "Sanchez"
|
||||
color: #C1C1C1
|
||||
@media only screen and (min-device-width : 375px) and (max-device-width : 667px) and (orientation : portrait)
|
||||
font-size: 0.3em
|
||||
font-size: 0.3em
|
||||
width: 50%;
|
||||
|
||||
+26
@@ -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>
|
||||
+30
@@ -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);
|
||||
}
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
.imageViewerViewportOverlay
|
||||
.timepointName
|
||||
color: #32BFFF
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<template name="lesionTrackerWorklistContextMenu">
|
||||
<div id="studyContextMenu" class="studyContextMenu noselect"
|
||||
oncontextmenu='return false;'
|
||||
unselectable='on'
|
||||
onselectstart='return false;'>
|
||||
<ul>
|
||||
<li>
|
||||
<!-- TODO: Make this dynamically accept options-->
|
||||
<a id="launchStudyAssociation" type="button"
|
||||
data-toggle="modal"
|
||||
data-target="#associationModal"
|
||||
title="Launch Study Association">
|
||||
<i class="fa fa-calendar-plus-o fa-lg"></i>
|
||||
Associate
|
||||
</a>
|
||||
<a id="removeTimepointAssociations" type="button"
|
||||
title="Remove Timepoint Association">
|
||||
<i class="fa fa-eraser fa-lg"></i> Remove Association
|
||||
</a>
|
||||
<a class="disabled"><span class="fa-stack fa-lg">
|
||||
<i class="fa fa-user fa-stack-1x"></i>
|
||||
<i class="fa fa-ban fa-stack-2x text-danger"></i>
|
||||
</span> Anonymize
|
||||
</a>
|
||||
<a class="disabled"><i class="fa fa-trash fa-lg"></i> Delete</a>
|
||||
<a class="disabled"><i class="fa fa-send-o fa-lg"></i> Send</a>
|
||||
<a class="disabled"><i class="fa fa-exchange fa-lg"></i> Export</a>
|
||||
<a class="disabled"><i class="fa fa-download fa-lg"></i> Download</a>
|
||||
<a class="disabled"><i class="fa fa-photo fa-lg"></i> View Series Details</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
+75
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
+31
@@ -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>
|
||||
+30
@@ -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);
|
||||
}
|
||||
});
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
.worklistStudy
|
||||
cursor: pointer
|
||||
+10
-5
@@ -11,24 +11,29 @@ function closeHandler(dialog) {
|
||||
|
||||
// This event sets lesion number for new lesion
|
||||
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
|
||||
// Get the current element's timepointID from the study date metadata
|
||||
// Get the current element's timepointId from the study date metadata
|
||||
var element = eventData.element;
|
||||
var enabledElement = cornerstone.getEnabledElement(element);
|
||||
var imageId = enabledElement.image.imageId;
|
||||
|
||||
var study = cornerstoneTools.metaData.get('study', imageId);
|
||||
|
||||
// Find the relevant timepoint given the current study
|
||||
var timepoint = Timepoints.findOne({
|
||||
timepointName: study.studyDate
|
||||
studyInstanceUids: {
|
||||
$in: [study.studyInstanceUid]
|
||||
}
|
||||
});
|
||||
|
||||
if (!timepoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
measurementData.timepointID = timepoint.timepointID;
|
||||
measurementData.timepointId = timepoint.timepointId;
|
||||
|
||||
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
|
||||
// exists at a different timepoint
|
||||
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = false);
|
||||
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = false);
|
||||
measurementData.lesionNumber = lesionNumber;
|
||||
|
||||
// Set lesion number
|
||||
@@ -211,7 +216,7 @@ changeNonTargetLocationCallback = function(measurementData, eventData, doneCallb
|
||||
multi: true
|
||||
});
|
||||
|
||||
var response = measurement.timepoints[measurementData.timepointID].response;
|
||||
var response = measurement.timepoints[measurementData.timepointId].response;
|
||||
|
||||
// TODO = Standardize this. Searching by code probably isn't the best, we should use
|
||||
// some sort of UID
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ changeNonTargetResponse = function(measurementData, eventData, doneCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
var response = measurement.timepoints[measurementData.timepointID].response;
|
||||
var response = measurement.timepoints[measurementData.timepointId].response;
|
||||
|
||||
// TODO = Standardize this. Searching by code probably isn't the best, we should use
|
||||
// some sort of UID
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<template name="optionsButton">
|
||||
<div class="optionsButton">
|
||||
<a type="button"
|
||||
class="btn btn-default"
|
||||
data-toggle="modal"
|
||||
data-target="#optionsModal"
|
||||
title="Trial Options">
|
||||
<i class="fa fa-cog fa-lg"></i>
|
||||
Trial Options
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,6 @@
|
||||
// These settings are temporary!
|
||||
.optionsButton
|
||||
position: absolute
|
||||
top: 3px
|
||||
right: 350px
|
||||
cursor: pointer
|
||||
@@ -0,0 +1,83 @@
|
||||
<template name="optionsModal">
|
||||
<div class="modal" id="optionsModal" tabindex="-1" role="dialog" aria-labelledby="optionsModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="optionsModalLabel">Clinical Trial Options</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>Select a Trial Criteria Type</h4>
|
||||
<label for="recistCriteria" class="trialCriteriaLabel">
|
||||
<input type="radio" name="trialCriteria" checked="true" id="recistCriteria" value='RECIST' class="trialCriteria"> RECIST 1.1
|
||||
</label>
|
||||
<label for="irRCCriteria" class="trialCriteriaLabel">
|
||||
<input type="radio" name="trialCriteria" id="irRCCriteria" value='irRC' class="trialCriteria"> irRC
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>Trial Criteria Descriptions</h4>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#RECISTDescription" aria-controls="home" role="tab" data-toggle="tab">RECIST 1.1</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#irRCDescription" aria-controls="profile" role="tab" data-toggle="tab">irRC</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade in active" id="RECISTDescription">
|
||||
<h5>Baseline Checks</h5>
|
||||
<ul>
|
||||
<li>Extranodal lesions must be >= 10 mm long axis AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Extranodal lesions must be >= 20 mm on chest x-ray (although x-rays rarely used for clinical trial assessment)</li>
|
||||
<li>Nodal lesions must be >= 15 mm short axis AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Up to a max of 2 target lesions per organ</li>
|
||||
<li>Up to a max of 5 target lesions total</li>
|
||||
<li>Non-targets can only be assessed as 'present'</li>
|
||||
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
|
||||
</ul>
|
||||
<!-- Time Point Measurement Total = Sum of long axis measurements for extranodal target lesion + short axis measurements for nodal lesions</li>-->
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="irRCDescription">
|
||||
<h5>Baseline Checks</h5>
|
||||
<ul>
|
||||
<li>Target lesions must be >= 10 X 10 mm AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Up to a max of 5 target lesions per organ</li>
|
||||
<li>Up to a max of 10 target lesions total</li>
|
||||
<li>Non-targets can only be assessed as 'present'</li>
|
||||
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
|
||||
</ul>
|
||||
<h5>New Lesion Checks for Follow-ups</h5>
|
||||
<ul>
|
||||
<li>New target lesions must be >= 5 X 5 mm AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Up to a max of 5 target lesions per organ</li>
|
||||
<li>Up to a max of 10 target lesions total</li>
|
||||
</ul>
|
||||
<!--Time Point Measurement Total = SPD target lesions + SPD new lesions (SPD = sum of product of long axis and short axis diameters)-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4>Additional Options</h4>
|
||||
<a type="button" class="btn btn-danger clearAllStudyTimepointAssociations">
|
||||
<i class="fa fa-trash"></i> Clear All Study/Timepoint Associations
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
Session.setDefault('TrialResponseAssessmentCriteria', 'RECIST');
|
||||
|
||||
Template.optionsModal.events({
|
||||
/**
|
||||
* When the trial criteria radio buttons are changed, change the
|
||||
* trial assessment criteria for the Lesion Tracker
|
||||
*
|
||||
* @param e The 'change' event on the selected radio button
|
||||
*/
|
||||
'change input.trialCriteria': function(e) {
|
||||
// Get the Trial Criteria type that the selected radio button represents
|
||||
var radioButton = $(e.currentTarget);
|
||||
var criteriaType = radioButton.val();
|
||||
|
||||
// Set this as the current Trial Response Assessment Criteria
|
||||
// TODO: Update when we have more trial-level support
|
||||
// (Currently this information is stored in Session, later this will change)
|
||||
Session.set('TrialResponseAssessmentCriteria', criteriaType);
|
||||
|
||||
log.info('Trial Criteria changed to: ' + criteriaType);
|
||||
},
|
||||
/**
|
||||
* When the Clear Study/Timepoint Associations button is clicked, we
|
||||
* send a call to the server to erase all entries in the Timepoints Collection.
|
||||
*/
|
||||
'click a.clearAllStudyTimepointAssociations': function() {
|
||||
Meteor.call('clearAllTimepoints', function(error) {
|
||||
if (error) {
|
||||
log.warn(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
.trialCriteriaLabel
|
||||
margin: 0 5px
|
||||
+7
-5
@@ -43,13 +43,15 @@
|
||||
<td class="studyDataCell {{ #if autoselected }}disabled{{ /if }}">
|
||||
<p>{{studyDescription}}</p>
|
||||
</td>
|
||||
<td class="timepointOptions center studyDataCell {{ #if autoselected }}disabled{{ /if }}">
|
||||
<td class="timepointOptions center studyDataCell noselect {{ #if autoselected }}disabled{{ /if }}">
|
||||
{{ #each timepointOptions }}
|
||||
<label>
|
||||
<label class="noselect">
|
||||
<input type="radio"
|
||||
name="{{../_id}}"
|
||||
value={{type}}
|
||||
{{ inlineIf autoselected true 'disabled'}}>
|
||||
name="{{../_id}}"
|
||||
value="{{value}}"
|
||||
class="timepointOption"
|
||||
{{ inlineIf checked true 'checked' }}
|
||||
{{ inlineIf autoselected true 'disabled' }}>
|
||||
{{name}}
|
||||
</label>
|
||||
{{ /each }}
|
||||
|
||||
+26
-30
@@ -30,8 +30,13 @@ function getDateRange(selectedStudies, range) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all studies related to the currently input studies,
|
||||
* based on various criteria. Returns the entire array of related studies.
|
||||
*
|
||||
* @returns {Array}
|
||||
* (at the moment, this is only the date range +/- 14 days, with a matching patientId)
|
||||
*
|
||||
* @param selectedStudies A user-selected list of studies
|
||||
* @returns {*} The entire array of related studies
|
||||
*/
|
||||
function autoSelectStudies(selectedStudies) {
|
||||
if (!selectedStudies.length) {
|
||||
@@ -40,6 +45,9 @@ function autoSelectStudies(selectedStudies) {
|
||||
|
||||
var range = getDateRange(selectedStudies);
|
||||
|
||||
// Fetch autoselected studies based on the date range
|
||||
// Note that we used MongoDB's fetch here so we have a mutable array,
|
||||
// rather than a Cursor
|
||||
var autoselected = WorklistStudies.find({
|
||||
studyDate: {
|
||||
$gte: range.earliestDate.format('YYYYMMDD'),
|
||||
@@ -82,8 +90,8 @@ Template.studyAssociationTable.helpers({
|
||||
studyDate: 1
|
||||
}
|
||||
}).fetch() || [];
|
||||
var autoselected = autoSelectStudies(selectedStudies);
|
||||
return autoselected;
|
||||
|
||||
return autoSelectStudies(selectedStudies);
|
||||
},
|
||||
/**
|
||||
* This helper returns the list of Timepoint types the user can set for this study
|
||||
@@ -91,16 +99,15 @@ Template.studyAssociationTable.helpers({
|
||||
* @returns {Array.<T>}
|
||||
*/
|
||||
timepointOptions: function() {
|
||||
return [
|
||||
{
|
||||
return [{
|
||||
value: 'baseline',
|
||||
name: 'Baseline'
|
||||
},
|
||||
{
|
||||
name: 'Baseline',
|
||||
checked: true
|
||||
}, {
|
||||
value: 'followup',
|
||||
name: 'Follow-up'
|
||||
}
|
||||
];
|
||||
name: 'Follow-up',
|
||||
checked: false
|
||||
}];
|
||||
},
|
||||
earliestDate: function() {
|
||||
var selectedStudies = WorklistSelectedStudies.find({}, {
|
||||
@@ -108,12 +115,11 @@ Template.studyAssociationTable.helpers({
|
||||
studyDate: 1
|
||||
}
|
||||
}).fetch();
|
||||
var range = getDateRange(selectedStudies);
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
return range.earliestDate;
|
||||
var range = getDateRange(selectedStudies);
|
||||
if (range) {
|
||||
return range.earliestDate;
|
||||
}
|
||||
},
|
||||
latestDate: function() {
|
||||
var selectedStudies = WorklistSelectedStudies.find({}, {
|
||||
@@ -121,12 +127,11 @@ Template.studyAssociationTable.helpers({
|
||||
studyDate: 1
|
||||
}
|
||||
}).fetch();
|
||||
var range = getDateRange(selectedStudies);
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
return range.latestDate;
|
||||
var range = getDateRange(selectedStudies);
|
||||
if (range) {
|
||||
return range.latestDate;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -143,12 +148,3 @@ Template.studyAssociationTable.events({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//trial criteria!
|
||||
/*There shall be Associate option in right-click dialog
|
||||
If associated, double-click shall go to image view.
|
||||
If not associated, user shall be directed to Associate Time Points Dialog
|
||||
Use shall also be allowed to select multiple studies from study list to associate
|
||||
Associate Time Point dialog shall present selected studies and studies within defined time window of =/- 14 days of selected studies
|
||||
User should only be able to associate one time point at a time (user should not be able to select both BL and F/U for different studies in associate dialog)
|
||||
*/
|
||||
@@ -4,7 +4,7 @@
|
||||
<span class="loading"><i class="fa fa-spinner fa-spin"></i></span>
|
||||
<select id="selectStudyDate">
|
||||
{{#each relatedStudies}}
|
||||
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}}</option>
|
||||
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}} ({{timepointName}})</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -4,21 +4,22 @@ Template.studyDateList.helpers({
|
||||
* The value for 'selected' for the currently loaded study is set to true, so that
|
||||
* this becomes the current option in the combo box.
|
||||
*
|
||||
* @returns {*}
|
||||
* @returns {*} Array of studies that are related to the current study by patient ID
|
||||
*/
|
||||
relatedStudies: function() {
|
||||
// TODO= Fix this! This won't work to retrieve all studies
|
||||
// related to this patient. We will need to do a real search
|
||||
// since the WorklistStudies Collection only contains the studies on-screen
|
||||
|
||||
// Check which study is currently loaded into the study browser
|
||||
var currentStudyInBrowser = ViewerStudies.findOne({
|
||||
selected: true
|
||||
});
|
||||
|
||||
// Find studies which have the same patientId as the currently selected study
|
||||
var relatedStudies = WorklistStudies.find({patientId: currentStudyInBrowser.patientId},
|
||||
{sort: {studyDate: 1}}).fetch();
|
||||
// Find all Timepoint-associated studies which have the same patientId as the currently selected study
|
||||
var relatedStudies = Studies.find({
|
||||
patientId: currentStudyInBrowser.patientId
|
||||
}, {
|
||||
sort: {
|
||||
studyDate: 1
|
||||
}
|
||||
});
|
||||
|
||||
// Modify the array of related studies so the default option is the currently selected study
|
||||
relatedStudies.forEach(function(study) {
|
||||
@@ -31,6 +32,26 @@ Template.studyDateList.helpers({
|
||||
|
||||
// Use this array to populate the combo box
|
||||
return relatedStudies;
|
||||
},
|
||||
timepointName: function() {
|
||||
var data = this;
|
||||
var study = Studies.findOne({
|
||||
studyInstanceUid: data.studyInstanceUid
|
||||
});
|
||||
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
var timepoint = Timepoints.findOne({
|
||||
timepointId: study.timepointId
|
||||
});
|
||||
|
||||
if (!timepoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getTimepointName(timepoint);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -46,7 +67,6 @@ Template.studyDateList.events({
|
||||
*/
|
||||
'change select#selectStudyDate': function(e) {
|
||||
var selectBox = $(e.currentTarget);
|
||||
|
||||
var studyInstanceUid = selectBox.val();
|
||||
|
||||
// Hide the select box
|
||||
@@ -57,6 +77,11 @@ Template.studyDateList.events({
|
||||
loadingIndicator.css('display', 'block');
|
||||
|
||||
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
|
||||
if (error) {
|
||||
log.warn(error);
|
||||
return;
|
||||
}
|
||||
|
||||
sortStudy(study);
|
||||
|
||||
// Hide the loading indicator
|
||||
@@ -66,21 +91,20 @@ Template.studyDateList.events({
|
||||
selectBox.css('display', 'block');
|
||||
|
||||
// Set "Selected" to false for the entire collection
|
||||
ViewerStudies.update({},
|
||||
{
|
||||
$set: {
|
||||
selected: false
|
||||
}
|
||||
},
|
||||
{
|
||||
multi: true
|
||||
});
|
||||
ViewerStudies.update({}, {
|
||||
$set: {
|
||||
selected: false
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// Check if this study already exists in the ViewerStudies collection
|
||||
// of loaded studies. If it does, set it's 'selected' value to true.
|
||||
var existingStudy = ViewerStudies.findOne({
|
||||
studyInstanceUid: studyInstanceUid
|
||||
});
|
||||
|
||||
if (existingStudy) {
|
||||
// Set the current finding in the collection to true
|
||||
ViewerStudies.update(existingStudy._id, {
|
||||
@@ -95,29 +119,6 @@ Template.studyDateList.events({
|
||||
// with the value True, and insert it into the ViewerStudies Collection
|
||||
study.selected = true;
|
||||
ViewerStudies.insert(study);
|
||||
|
||||
var timepointID = uuid.v4();
|
||||
|
||||
var timepoint = Timepoints.findOne({
|
||||
timepointName: study.studyDate
|
||||
});
|
||||
if (timepoint) {
|
||||
log.warn('A timepoint with that study date already exists!');
|
||||
return;
|
||||
}
|
||||
|
||||
var testTimepoint = Timepoints.findOne({});
|
||||
if (testTimepoint && testTimepoint.patientId !== study.patientId) {
|
||||
log.warn('Timepoints collection related to the wrong subject');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('Inserting a new timepoint');
|
||||
Timepoints.insert({
|
||||
patientId: study.patientId,
|
||||
timepointID: timepointID,
|
||||
timepointName: study.studyDate
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user