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

This commit is contained in:
Erik Ziegler committed 2016-02-08 19:59:39 +01:00
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);
}
});
});
@@ -0,0 +1,7 @@
<template name="conformanceCheckFeedback">
<div id="conformanceCheckFeedback">
{{ #each validationErrors }}
<p data-toggle=""><i class="fa fa-exclamation-triangle fa-lg"></i> {{prefix}}{{error}}</p>
{{ /each }}
</div>
</template>
@@ -0,0 +1,11 @@
Template.conformanceCheckFeedback.helpers({
validationErrors: function() {
// Return validation errors sorted by last added Target
return ValidationErrors.find({}, {
sort: {
prefix: -1,
type: 1
}
});
}
});
@@ -0,0 +1,19 @@
#conformanceCheckFeedback
position: absolute
right: 10px
color: darkorange
cursor: pointer
background: black
width: 30%
height: 30px
overflow: hidden
p
margin: 5px
text-align: center
&:hover
overflow: auto
height: 200px
z-index: 200
@@ -11,24 +11,29 @@ function closeHandler(dialog) {
// This event sets lesion number for new lesion
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
// Get the current element's timepointID from the study date metadata
// Get the current element's timepointId from the study date metadata
var element = eventData.element;
var enabledElement = cornerstone.getEnabledElement(element);
var imageId = enabledElement.image.imageId;
var study = cornerstoneTools.metaData.get('study', imageId);
// Find the relevant timepoint given the current study
var timepoint = Timepoints.findOne({
timepointName: study.studyDate
studyInstanceUids: {
$in: [study.studyInstanceUid]
}
});
if (!timepoint) {
return;
}
measurementData.timepointID = timepoint.timepointID;
measurementData.timepointId = timepoint.timepointId;
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
// exists at a different timepoint
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = true);
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = true);
measurementData.lesionNumber = lesionNumber;
// Set lesion number
@@ -226,6 +231,9 @@ Template.lesionLocationDialog.events({
/// Set the isTarget value to true, since this is the target-lesion dialog callback
measurementData.isTarget = true;
// Set the isNodal value based on the Location's properties
measurementData.isNodal = locationObj.isNodal;
// Adds lesion data to timepoints array
LesionManager.updateLesionData(measurementData);
} else {
@@ -233,6 +241,7 @@ Template.lesionLocationDialog.events({
$set: {
location: locationObj.location,
locationId: locationObj.id,
isNodal: locationObj.isNodal,
locationUID: id
}
});
@@ -5,18 +5,18 @@ Template.lesionTableTimepointCell.helpers({
var lesionData = Template.parentData(1);
return (lesionData &&
lesionData.timepoints &&
lesionData.timepoints[this.timepointID]);
lesionData.timepoints[this.timepointId]);
},
displayData: function() {
// Search Measurements by lesion and timepoint
var lesionData = Template.parentData(1);
if (!lesionData ||
!lesionData.timepoints ||
!lesionData.timepoints[this.timepointID]) {
!lesionData.timepoints[this.timepointId]) {
return;
}
var data = lesionData.timepoints[this.timepointID];
var data = lesionData.timepoints[this.timepointId];
if (lesionData.isTarget === true) {
if (data.shortestDiameter) {
@@ -40,7 +40,7 @@ function doneCallback(measurementData, deleteTool) {
// the specified Timepoint Cell
if (deleteTool === true) {
log.info('Confirm clicked!');
clearMeasurementTimepointData(measurementData.id, measurementData.timepointID);
clearMeasurementTimepointData(measurementData.id, measurementData.timepointId);
}
}
@@ -57,12 +57,12 @@ Template.lesionTableTimepointCell.events({
var currentMeasurement = Template.parentData(1);
// Create some fake measurement data
var currentTimepointID = this.timepointID;
var currentTimepointID = this.timepointId;
var timepointData = currentMeasurement.timepoints[currentTimepointID];
var measurementData = {
id: currentMeasurement._id,
timepointID: currentTimepointID,
timepointId: currentTimepointID,
response: timepointData.response,
imageId: timepointData.imageId,
handles: timepointData.handles,
@@ -83,7 +83,7 @@ Template.lesionTableTimepointCell.events({
if (keyCode === keys.DELETE ||
(keyCode === keys.D && e.ctrlKey === true)) {
var currentMeasurement = Template.parentData(1);
var currentTimepointID = this.timepointID;
var currentTimepointID = this.timepointId;
showConfirmDialog(function() {
clearMeasurementTimepointData(currentMeasurement._id, currentTimepointID);
@@ -1,9 +1,6 @@
<template name="lesionTableTimepointHeader">
<th class="lesionTableTimepointCell">
{{formatDA timepointName "MM/DD/YYYY"}}
{{#if timepointTextFound}}
{{timepointText}}
{{/if}}
{{#if timepointLoaded }}<span class="timepointIndicator">&lt;</span> {{/if}}
<th class="lesionTableTimepointCell" title="Latest study date: {{formatDA latestDate "MM/DD/YYYY"}}"
data-toggle="tooltip" data-placement="top">
{{ timepointName }}
</th>
</template>
@@ -1,105 +1,6 @@
Template.lesionTableTimepointHeader.events({
'click th.lesionTableTimepointCell': function(e, template) {
var parentPosition = getPosition(e.currentTarget);
var cellText = e.currentTarget.innerText;
// Remove spaces in string
cellText = cellText.replace(/\s/g, ''); // Remove spaces
cellText = cellText.replace('<', ''); // Remove <
var splitCellText = cellText.split('/');
var dateStr = splitCellText[2].replace(/\D/g,'') + '' + splitCellText[0].replace(/\D/g,'') + '' + splitCellText[1].replace(/\D/g,''); // Remove non-digit chars
// Check patient has a timepointText as Baseline
var patientId = template.data.patientId;
// Open popup
var timepointTextDialog = $('#timepointTextDialog');
var dialogDisplay = timepointTextDialog.css('display');
if (dialogDisplay === 'none') {
var isBaselineInCollection = Timepoints.findOne({
patientId: patientId,
timepointName: dateStr,
timepointText: 'Baseline'
});
// If isBaselineInCollection is true, "Baseline" is found in collection for patient and set checkbox as checked
if (isBaselineInCollection) {
// Set checkbox as checked
$('#checkBoxBaseline').prop('checked', true);
} else {
// Set checkbox as unchecked
$('#checkBoxBaseline').prop('checked', false);
}
// Open dialog
var dialogProperty = {
top: parentPosition.y - 30,
left: parentPosition.x,
display: 'block'
};
timepointTextDialog.css(dialogProperty);
} else {
// Get timepoints of patient
// Set timepointText as Baseline for selected timepoint
var timepoints = Timepoints.find({
patientId: patientId
}).fetch();
// Check checkbox is selected
var checkboxBaselineChecked = $('#checkBoxBaseline').is(':checked');
timepoints.forEach(function(timepoint) {
// timepointText defines a custom text for timepoint such as Baseline, Nadir, Current
if (timepoint.timepointName === dateStr) {
// If checkbox is selected, set timepointText as Baseline
// Else set timepointText as ""
var timepointText = 'Baseline';
if (!checkboxBaselineChecked) {
timepointText = '';
}
Timepoints.update(timepoint._id,{
$set: {
timepointText: timepointText
}
});
} else {
// Set timepointText as empty
Timepoints.update(timepoint._id,{
$set: {
timepointText: ''
}
});
}
});
// Close dialog
timepointTextDialog.css('display', 'none');
}
}
});
Template.lesionTableTimepointHeader.helpers({
timepointTextFound: function() {
var timepointText = this.timepointText;
return (timepointText && timepointText === 'Baseline');
'timepointName': function() {
var timepoint = this;
return getTimepointName(timepoint);
}
});
// Gets parent's position of element which mouse pointer is clicked in
function getPosition(element) {
var xPosition = 0;
var yPosition = 0;
while (element) {
xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft);
yPosition += (element.offsetTop - element.scrollTop + element.clientTop);
element = element.offsetParent;
}
return {
x: xPosition,
y: yPosition
};
}
});
@@ -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%;
@@ -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
@@ -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>
@@ -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);
}
});
@@ -0,0 +1,2 @@
.worklistStudy
cursor: pointer
@@ -11,24 +11,29 @@ function closeHandler(dialog) {
// This event sets lesion number for new lesion
function setLesionNumberCallback(measurementData, eventData, doneCallback) {
// Get the current element's timepointID from the study date metadata
// Get the current element's timepointId from the study date metadata
var element = eventData.element;
var enabledElement = cornerstone.getEnabledElement(element);
var imageId = enabledElement.image.imageId;
var study = cornerstoneTools.metaData.get('study', imageId);
// Find the relevant timepoint given the current study
var timepoint = Timepoints.findOne({
timepointName: study.studyDate
studyInstanceUids: {
$in: [study.studyInstanceUid]
}
});
if (!timepoint) {
return;
}
measurementData.timepointID = timepoint.timepointID;
measurementData.timepointId = timepoint.timepointId;
// Get a lesion number for this lesion, depending on whether or not the same lesion previously
// exists at a different timepoint
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointID, isTarget = false);
var lesionNumber = LesionManager.getNewLesionNumber(measurementData.timepointId, isTarget = false);
measurementData.lesionNumber = lesionNumber;
// Set lesion number
@@ -211,7 +216,7 @@ changeNonTargetLocationCallback = function(measurementData, eventData, doneCallb
multi: true
});
var response = measurement.timepoints[measurementData.timepointID].response;
var response = measurement.timepoints[measurementData.timepointId].response;
// TODO = Standardize this. Searching by code probably isn't the best, we should use
// some sort of UID
@@ -65,7 +65,7 @@ changeNonTargetResponse = function(measurementData, eventData, doneCallback) {
return;
}
var response = measurement.timepoints[measurementData.timepointID].response;
var response = measurement.timepoints[measurementData.timepointId].response;
// TODO = Standardize this. Searching by code probably isn't the best, we should use
// some sort of UID
@@ -0,0 +1,12 @@
<template name="optionsButton">
<div class="optionsButton">
<a type="button"
class="btn btn-default"
data-toggle="modal"
data-target="#optionsModal"
title="Trial Options">
<i class="fa fa-cog fa-lg"></i>
Trial Options
</a>
</div>
</template>
@@ -0,0 +1,6 @@
// These settings are temporary!
.optionsButton
position: absolute
top: 3px
right: 350px
cursor: pointer
@@ -0,0 +1,83 @@
<template name="optionsModal">
<div class="modal" id="optionsModal" tabindex="-1" role="dialog" aria-labelledby="optionsModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="optionsModalLabel">Clinical Trial Options</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<h4>Select a Trial Criteria Type</h4>
<label for="recistCriteria" class="trialCriteriaLabel">
<input type="radio" name="trialCriteria" checked="true" id="recistCriteria" value='RECIST' class="trialCriteria"> RECIST 1.1
</label>
<label for="irRCCriteria" class="trialCriteriaLabel">
<input type="radio" name="trialCriteria" id="irRCCriteria" value='irRC' class="trialCriteria"> irRC
</label>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<h4>Trial Criteria Descriptions</h4>
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active">
<a href="#RECISTDescription" aria-controls="home" role="tab" data-toggle="tab">RECIST 1.1</a>
</li>
<li role="presentation">
<a href="#irRCDescription" aria-controls="profile" role="tab" data-toggle="tab">irRC</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade in active" id="RECISTDescription">
<h5>Baseline Checks</h5>
<ul>
<li>Extranodal lesions must be >= 10 mm long axis AND >= double the acquisition slice thickness by CT and MR</li>
<li>Extranodal lesions must be >= 20 mm on chest x-ray (although x-rays rarely used for clinical trial assessment)</li>
<li>Nodal lesions must be >= 15 mm short axis AND >= double the acquisition slice thickness by CT and MR</li>
<li>Up to a max of 2 target lesions per organ</li>
<li>Up to a max of 5 target lesions total</li>
<li>Non-targets can only be assessed as 'present'</li>
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
</ul>
<!-- Time Point Measurement Total = Sum of long axis measurements for extranodal target lesion + short axis measurements for nodal lesions</li>-->
</div>
<div role="tabpanel" class="tab-pane fade" id="irRCDescription">
<h5>Baseline Checks</h5>
<ul>
<li>Target lesions must be >= 10 X 10 mm AND >= double the acquisition slice thickness by CT and MR</li>
<li>Up to a max of 5 target lesions per organ</li>
<li>Up to a max of 10 target lesions total</li>
<li>Non-targets can only be assessed as 'present'</li>
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
</ul>
<h5>New Lesion Checks for Follow-ups</h5>
<ul>
<li>New target lesions must be >= 5 X 5 mm AND >= double the acquisition slice thickness by CT and MR</li>
<li>Up to a max of 5 target lesions per organ</li>
<li>Up to a max of 10 target lesions total</li>
</ul>
<!--Time Point Measurement Total = SPD target lesions + SPD new lesions (SPD = sum of product of long axis and short axis diameters)-->
</div>
</div>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12">
<h4>Additional Options</h4>
<a type="button" class="btn btn-danger clearAllStudyTimepointAssociations">
<i class="fa fa-trash"></i> Clear All Study/Timepoint Associations
</a>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</template>
@@ -0,0 +1,33 @@
Session.setDefault('TrialResponseAssessmentCriteria', 'RECIST');
Template.optionsModal.events({
/**
* When the trial criteria radio buttons are changed, change the
* trial assessment criteria for the Lesion Tracker
*
* @param e The 'change' event on the selected radio button
*/
'change input.trialCriteria': function(e) {
// Get the Trial Criteria type that the selected radio button represents
var radioButton = $(e.currentTarget);
var criteriaType = radioButton.val();
// Set this as the current Trial Response Assessment Criteria
// TODO: Update when we have more trial-level support
// (Currently this information is stored in Session, later this will change)
Session.set('TrialResponseAssessmentCriteria', criteriaType);
log.info('Trial Criteria changed to: ' + criteriaType);
},
/**
* When the Clear Study/Timepoint Associations button is clicked, we
* send a call to the server to erase all entries in the Timepoints Collection.
*/
'click a.clearAllStudyTimepointAssociations': function() {
Meteor.call('clearAllTimepoints', function(error) {
if (error) {
log.warn(error);
}
});
}
});
@@ -0,0 +1,2 @@
.trialCriteriaLabel
margin: 0 5px
@@ -43,13 +43,15 @@
<td class="studyDataCell {{ #if autoselected }}disabled{{ /if }}">
<p>{{studyDescription}}</p>
</td>
<td class="timepointOptions center studyDataCell {{ #if autoselected }}disabled{{ /if }}">
<td class="timepointOptions center studyDataCell noselect {{ #if autoselected }}disabled{{ /if }}">
{{ #each timepointOptions }}
<label>
<label class="noselect">
<input type="radio"
name="{{../_id}}"
value={{type}}
{{ inlineIf autoselected true 'disabled'}}>
name="{{../_id}}"
value="{{value}}"
class="timepointOption"
{{ inlineIf checked true 'checked' }}
{{ inlineIf autoselected true 'disabled' }}>
{{name}}
</label>
{{ /each }}
@@ -30,8 +30,13 @@ function getDateRange(selectedStudies, range) {
}
/**
* Selects all studies related to the currently input studies,
* based on various criteria. Returns the entire array of related studies.
*
* @returns {Array}
* (at the moment, this is only the date range +/- 14 days, with a matching patientId)
*
* @param selectedStudies A user-selected list of studies
* @returns {*} The entire array of related studies
*/
function autoSelectStudies(selectedStudies) {
if (!selectedStudies.length) {
@@ -40,6 +45,9 @@ function autoSelectStudies(selectedStudies) {
var range = getDateRange(selectedStudies);
// Fetch autoselected studies based on the date range
// Note that we used MongoDB's fetch here so we have a mutable array,
// rather than a Cursor
var autoselected = WorklistStudies.find({
studyDate: {
$gte: range.earliestDate.format('YYYYMMDD'),
@@ -82,8 +90,8 @@ Template.studyAssociationTable.helpers({
studyDate: 1
}
}).fetch() || [];
var autoselected = autoSelectStudies(selectedStudies);
return autoselected;
return autoSelectStudies(selectedStudies);
},
/**
* This helper returns the list of Timepoint types the user can set for this study
@@ -91,16 +99,15 @@ Template.studyAssociationTable.helpers({
* @returns {Array.<T>}
*/
timepointOptions: function() {
return [
{
return [{
value: 'baseline',
name: 'Baseline'
},
{
name: 'Baseline',
checked: true
}, {
value: 'followup',
name: 'Follow-up'
}
];
name: 'Follow-up',
checked: false
}];
},
earliestDate: function() {
var selectedStudies = WorklistSelectedStudies.find({}, {
@@ -108,12 +115,11 @@ Template.studyAssociationTable.helpers({
studyDate: 1
}
}).fetch();
var range = getDateRange(selectedStudies);
if (!range) {
return;
}
return range.earliestDate;
var range = getDateRange(selectedStudies);
if (range) {
return range.earliestDate;
}
},
latestDate: function() {
var selectedStudies = WorklistSelectedStudies.find({}, {
@@ -121,12 +127,11 @@ Template.studyAssociationTable.helpers({
studyDate: 1
}
}).fetch();
var range = getDateRange(selectedStudies);
if (!range) {
return;
}
return range.latestDate;
var range = getDateRange(selectedStudies);
if (range) {
return range.latestDate;
}
}
});
@@ -143,12 +148,3 @@ Template.studyAssociationTable.events({
}
}
});
//trial criteria!
/*There shall be Associate option in right-click dialog
If associated, double-click shall go to image view.
If not associated, user shall be directed to Associate Time Points Dialog
Use shall also be allowed to select multiple studies from study list to associate
Associate Time Point dialog shall present selected studies and studies within defined time window of =/- 14 days of selected studies
User should only be able to associate one time point at a time (user should not be able to select both BL and F/U for different studies in associate dialog)
*/
@@ -4,7 +4,7 @@
<span class="loading"><i class="fa fa-spinner fa-spin"></i></span>
<select id="selectStudyDate">
{{#each relatedStudies}}
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}}</option>
<option value="{{studyInstanceUid}}" selected={{selected}}>{{formatDA studyDate}} ({{timepointName}})</option>
{{/each}}
</select>
</div>
@@ -4,21 +4,22 @@ Template.studyDateList.helpers({
* The value for 'selected' for the currently loaded study is set to true, so that
* this becomes the current option in the combo box.
*
* @returns {*}
* @returns {*} Array of studies that are related to the current study by patient ID
*/
relatedStudies: function() {
// TODO= Fix this! This won't work to retrieve all studies
// related to this patient. We will need to do a real search
// since the WorklistStudies Collection only contains the studies on-screen
// Check which study is currently loaded into the study browser
var currentStudyInBrowser = ViewerStudies.findOne({
selected: true
});
// Find studies which have the same patientId as the currently selected study
var relatedStudies = WorklistStudies.find({patientId: currentStudyInBrowser.patientId},
{sort: {studyDate: 1}}).fetch();
// Find all Timepoint-associated studies which have the same patientId as the currently selected study
var relatedStudies = Studies.find({
patientId: currentStudyInBrowser.patientId
}, {
sort: {
studyDate: 1
}
});
// Modify the array of related studies so the default option is the currently selected study
relatedStudies.forEach(function(study) {
@@ -31,6 +32,26 @@ Template.studyDateList.helpers({
// Use this array to populate the combo box
return relatedStudies;
},
timepointName: function() {
var data = this;
var study = Studies.findOne({
studyInstanceUid: data.studyInstanceUid
});
if (!study) {
return;
}
var timepoint = Timepoints.findOne({
timepointId: study.timepointId
});
if (!timepoint) {
return;
}
return getTimepointName(timepoint);
}
});
@@ -46,7 +67,6 @@ Template.studyDateList.events({
*/
'change select#selectStudyDate': function(e) {
var selectBox = $(e.currentTarget);
var studyInstanceUid = selectBox.val();
// Hide the select box
@@ -57,6 +77,11 @@ Template.studyDateList.events({
loadingIndicator.css('display', 'block');
Meteor.call('GetStudyMetadata', studyInstanceUid, function(error, study) {
if (error) {
log.warn(error);
return;
}
sortStudy(study);
// Hide the loading indicator
@@ -66,21 +91,20 @@ Template.studyDateList.events({
selectBox.css('display', 'block');
// Set "Selected" to false for the entire collection
ViewerStudies.update({},
{
$set: {
selected: false
}
},
{
multi: true
});
ViewerStudies.update({}, {
$set: {
selected: false
}
}, {
multi: true
});
// Check if this study already exists in the ViewerStudies collection
// of loaded studies. If it does, set it's 'selected' value to true.
var existingStudy = ViewerStudies.findOne({
studyInstanceUid: studyInstanceUid
});
if (existingStudy) {
// Set the current finding in the collection to true
ViewerStudies.update(existingStudy._id, {
@@ -95,29 +119,6 @@ Template.studyDateList.events({
// with the value True, and insert it into the ViewerStudies Collection
study.selected = true;
ViewerStudies.insert(study);
var timepointID = uuid.v4();
var timepoint = Timepoints.findOne({
timepointName: study.studyDate
});
if (timepoint) {
log.warn('A timepoint with that study date already exists!');
return;
}
var testTimepoint = Timepoints.findOne({});
if (testTimepoint && testTimepoint.patientId !== study.patientId) {
log.warn('Timepoints collection related to the wrong subject');
return;
}
log.info('Inserting a new timepoint');
Timepoints.insert({
patientId: study.patientId,
timepointID: timepointID,
timepointName: study.studyDate
});
});
}
});