Fixes for current case status and Lesion Tracker measurement table behaviour (LT-248)

This commit is contained in:
Erik Ziegler 2016-08-15 11:29:49 +02:00
parent d70439c12d
commit 62f102617f
17 changed files with 244 additions and 92 deletions

View File

@ -0,0 +1,102 @@
const hasValueAtTimepoint = timepointId => {
return measurement => {
if (measurement.timepoints[timepointId]) {
return true;
}
};
};
const hasNoValueAtTimepoint = timepointId => {
return measurement => {
if (measurement.timepoints[timepointId] === undefined) {
return true;
}
};
};
export const MeasurementApi = {
sortOptions: {
sort: {
lesionNumberAbsolute: 1
}
},
// Return all Measurements
all(withPriors=false) {
let data = Measurements.find({}, this.sortOptions).fetch();
// If we don't have a prior for this Timepoint,
// this filter, we should just return all of the
// available Non-Targets measurements
if (this.priorTimepointId && withPriors === true) {
return data.filter(hasValueAtTimepoint(this.priorTimepointId))
}
return data;
},
unmarked() {
const withPriors = true;
return this.all(withPriors).filter(hasNoValueAtTimepoint(this.currentTimepointId));;
},
unmarkedTargets() {
const withPriors = true;
return this.targets(withPriors).filter(hasNoValueAtTimepoint(this.currentTimepointId));;
},
unmarkedNonTargets() {
const withPriors = true;
return this.nonTargets(withPriors).filter(hasNoValueAtTimepoint(this.currentTimepointId));;
},
// Return only Target Measurements
targets(withPriors=false) {
let data = Measurements.find({
isTarget: true
}, this.sortOptions).fetch();
// If we don't have a prior for this Timepoint,
// this filter, we should just return all of the
// available Non-Targets measurements
if (this.priorTimepointId && withPriors === true) {
return data.filter(hasValueAtTimepoint(this.priorTimepointId))
}
return data;
},
// Return only Non-Target Measurements
nonTargets(withPriors=false) {
let data = Measurements.find({
isTarget: false
}, this.sortOptions).fetch();
// If we don't have a prior for this Timepoint,
// this filter, we should just return all of the
// available Non-Targets measurements
if (this.priorTimepointId && withPriors === true) {
return data.filter(hasValueAtTimepoint(this.priorTimepointId))
}
return data;
},
// Return only New Lesions
newLesions() {
// If we are current editing a Baseline we won't have any priors, so newLesions
// should return an empty array.
if (!this.priorTimepointId) {
return [];
}
// Find only lesions that have no value at the previous timepoint
return this.all().filter(hasNoValueAtTimepoint(this.priorTimepointId));
},
firstLesion() {
return Measurements.findOne({
target: true
}, this.sortOptions);
}
};

View File

@ -19,12 +19,42 @@ class TimepointApi {
return this.timepoints.find().fetch(); return this.timepoints.find().fetch();
} }
// Return only the current and prior timepoints // Return only the current timepoint
latest() { current() {
const options = { return this.timepoints.findOne({
limit: 2 timepointId: this.currentTimepointId
}; });
return this.timepoints.find({}, options).fetch(); }
prior() {
const latestDate = this.current().latestDate;
return this.timepoints.findOne({
latestDate: {
$lt: latestDate
}
}, {
sort: {
latestDate: -1
},
});
}
// Return only the current and prior Timepoints
currentAndPrior() {
let timepoints = [this.current()];
const prior = this.prior();
if (prior) {
timepoints.push(prior);
}
return timepoints;
}
// Return only the baseline timepoint
baseline() {
return this.timepoints.findOne({
timepointType: 'baseline'
});
} }
// Return only the key timepoints (current, prior, nadir and baseline) // Return only the key timepoints (current, prior, nadir and baseline)

View File

@ -1,45 +1,37 @@
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
Template.caseProgress.onCreated(() => { Template.caseProgress.onCreated(() => {
const instance = Template.instance(); const instance = Template.instance();
instance.progressPercent = new ReactiveVar(); instance.progressPercent = new ReactiveVar();
instance.progressText = new ReactiveVar(); instance.progressText = new ReactiveVar();
instance.isLocked = new ReactiveVar(); instance.isLocked = new ReactiveVar();
if (!instance.data.currentTimepointId) { const current = instance.data.timepointApi.current();
if (!current.timepointId) {
console.warn('Case has no timepointId'); console.warn('Case has no timepointId');
return; return;
} }
const currentTimepointId = instance.data.currentTimepointId; instance.isLocked.set(current.isLocked);
const timepoint = Timepoints.findOne({
timepointId: currentTimepointId
});
const timepointType = timepoint.timepointType; // Retrieve the initial number of targets left to measure at this
// follow-up. Note that this is done outside of the reactive function
// below so that new lesions don't change the initial target count.
const withPriors = true;
const totalTargets = MeasurementApi.targets(withPriors).length;
instance.isLocked.set(timepoint.isLocked); // If we're currently reviewing a Baseline timepoint, don't do any
// progress measurement.
if (timepointType === 'baseline') { if (current.timepointType === 'baseline') {
instance.progressPercent.set(100); instance.progressPercent.set(100);
} else { } else {
// Retrieve the initial number of targets left to measure at this
// follow-up. Note that this is done outside of the reactive function
// below so that new lesions don't change the initial target count.
const totalTargets = Measurements.find({
isTarget: true
}).count();
// Setup a reactive function to update the progress whenever // Setup a reactive function to update the progress whenever
// a measurement is made // a measurement is made
instance.autorun(() => { instance.autorun(() => {
// Obtain the number of Measurements for which the current Timepoint has // Obtain the number of Measurements for which the current Timepoint has
// no Measurement data // no Measurement data
let numRemainingMeasurements = 0; const numRemainingMeasurements = MeasurementApi.unmarked().length;
Measurements.find().forEach(measurement => {
if (!measurement.timepoints[currentTimepointId]) {
numRemainingMeasurements++;
}
});
// Update the Case Progress text with the remaining measurement count // Update the Case Progress text with the remaining measurement count
instance.progressText.set(numRemainingMeasurements); instance.progressText.set(numRemainingMeasurements);
@ -67,7 +59,7 @@ Template.caseProgress.helpers({
}, },
progressComplete() { progressComplete() {
let progressPercent = Template.instance().progressPercent.get(); const progressPercent = Template.instance().progressPercent.get();
return progressPercent === 100; return progressPercent === 100;
} }
}); });

View File

@ -1,3 +1,5 @@
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
Template.lesionTable.onCreated(() => { Template.lesionTable.onCreated(() => {
const instance = Template.instance(); const instance = Template.instance();
@ -13,7 +15,7 @@ Template.lesionTable.onCreated(() => {
if (tableLayout === 'key') { if (tableLayout === 'key') {
timepoints = instance.data.timepointApi.key(); timepoints = instance.data.timepointApi.key();
} else { } else {
timepoints = instance.data.timepointApi.latest(); timepoints = instance.data.timepointApi.currentAndPrior();
} }
// Return key timepoints // Return key timepoints
@ -47,14 +49,10 @@ Session.setDefault('NewSeriesLoaded', false);
Template.lesionTable.onRendered(() => { Template.lesionTable.onRendered(() => {
// Find the first measurement by Lesion Number // Find the first measurement by Lesion Number
var firstLesion = Measurements.findOne({}, { const firstLesion = MeasurementApi.firstLesion();
sort: {
lesionNumber: 1
}
});
// Create an object to store the ContentId inside // Create an object to store the ContentId inside
var templateData = { const templateData = {
contentId: Session.get('activeContentId') contentId: Session.get('activeContentId')
}; };

View File

@ -1,7 +1,7 @@
Template.lesionTableHUD.onCreated(() => { Template.lesionTableHUD.onCreated(() => {
const instance = Template.instance(); const instance = Template.instance();
instance.data.timepoints = new ReactiveVar(instance.data.timepointApi.latest()); instance.data.timepoints = new ReactiveVar(instance.data.timepointApi.currentAndPrior());
}); });
Template.lesionTableHUD.onRendered(() => { Template.lesionTableHUD.onRendered(() => {

View File

@ -1,14 +1,16 @@
<template name="lesionTableHeaderRow"> <template name="lesionTableHeaderRow">
<div class="lesionTableHeaderRow"> <div class="lesionTableHeaderRow">
{{ #if anyUnmarkedLesionsLeft}}
<div class="add js-setTool"> <div class="add js-setTool">
<svg> <svg>
<use xlink:href=/packages/viewerbase/assets/icons.svg#icon-ui-add></use> <use xlink:href=/packages/viewerbase/assets/icons.svg#icon-ui-add></use>
</svg> </svg>
</div> </div>
{{ /if }}
<div class="type"> <div class="type">
{{type}} {{type}}
</div> </div>
<div class="max"> <div class="max {{ # if gt numberOfLesions maxNumLesions }}warning{{/if}}">
{{ #if maxNumLesions }} {{ #if maxNumLesions }}
<p class="maxNumLesions"> <p class="maxNumLesions">
Max {{maxNumLesions}} Max {{maxNumLesions}}

View File

@ -1,3 +1,5 @@
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
const toolTypesById = { const toolTypesById = {
target: 'bidirectional', target: 'bidirectional',
nonTarget: 'nonTarget', nonTarget: 'nonTarget',
@ -14,21 +16,14 @@ Template.lesionTableHeaderRow.onCreated(() => {
const instance = Template.instance(); const instance = Template.instance();
instance.maxNumLesions = new ReactiveVar(); instance.maxNumLesions = new ReactiveVar();
const current = instance.data.timepointApi.current();
const timepointType = current.timepointType;
if (!instance.data.currentTimepointId) { if (!instance.data.currentTimepointId) {
console.warn('Case has no timepointId'); console.warn('Case has no timepointId');
return; return;
} }
// Give the header a proper name
instance.data.type = namesById[instance.data.id];
const currentTimepointId = instance.data.currentTimepointId;
const timepoint = Timepoints.findOne({
timepointId: currentTimepointId
});
const timepointType = timepoint.timepointType;
// TODO: Check if we have criteria where maximum limits are applied to // TODO: Check if we have criteria where maximum limits are applied to
// Non-Targets and/or New Lesions // Non-Targets and/or New Lesions
if (timepointType === 'baseline' && instance.data.id === 'target') { if (timepointType === 'baseline' && instance.data.id === 'target') {
@ -57,17 +52,35 @@ Template.lesionTableHeaderRow.onCreated(() => {
}); });
Template.lesionTableHeaderRow.helpers({ Template.lesionTableHeaderRow.helpers({
numberOfLesions: function() { type() {
const instance = Template.instance(); // Give the header a proper name
return instance.data.measurements.count(); const id = Template.instance().data.id;
return namesById[id];
}, },
maxNumLesions: function() {
numberOfLesions() {
return Template.instance().data.measurements.length;
},
maxNumLesions() {
return Template.instance().maxNumLesions.get(); return Template.instance().maxNumLesions.get();
},
anyUnmarkedLesionsLeft() {
const id = Template.instance().data.id;
if (id === 'target') {
return MeasurementApi.unmarkedTargets().length;
} else if (id === 'nonTarget') {
return MeasurementApi.unmarkedNonTargets().length;
}
// Keep the 'Add' button for the New Lesions header row
return true;
} }
}); });
Template.lesionTableHeaderRow.events({ Template.lesionTableHeaderRow.events({
'click .js-setTool': function(event, instance) { 'click .js-setTool'(event, instance) {
const id = instance.data.id; const id = instance.data.id;
const toolType = toolTypesById[id]; const toolType = toolTypesById[id];
toolManager.setActiveTool(toolType); toolManager.setActiveTool(toolType);

View File

@ -63,7 +63,7 @@ $headerRowHeight = 63px
text-align: right text-align: right
.maxNumLesions .maxNumLesions
background-color: $textSecondaryColor; background-color: $textSecondaryColor
border-radius: 3px border-radius: 3px
color: black color: black
display: table display: table
@ -75,3 +75,9 @@ $headerRowHeight = 63px
margin-top: 22px margin-top: 22px
padding: 2px 6px 0 padding: 2px 6px 0
text-transform: uppercase text-transform: uppercase
transition($sidebarTransition)
&.warning
.maxNumLesions
color: $textPrimaryColor
background-color: $uiYellow

View File

@ -1,17 +1,26 @@
<template name="lesionTableView"> <template name="lesionTableView">
<div class="lesionTableView scrollArea"> <div class="lesionTableView scrollArea">
{{>lesionTableHeaderRow id="target" measurements=targets currentTimepointId=currentTimepointId}} {{>lesionTableHeaderRow id="target"
measurements=targets
currentTimepointId=currentTimepointId
timepointApi=timepointApi}}
{{#each target in targets}} {{#each target in targets}}
{{>lesionTableRow (clone this rowItem=target)}} {{>lesionTableRow (clone this rowItem=target)}}
{{/each}} {{/each}}
{{>lesionTableHeaderRow id="nonTarget" measurements=nonTargets currentTimepointId=currentTimepointId}} {{>lesionTableHeaderRow id="nonTarget"
measurements=nonTargets
currentTimepointId=currentTimepointId
timepointApi=timepointApi}}
{{#each nonTarget in nonTargets}} {{#each nonTarget in nonTargets}}
{{>lesionTableRow (clone this rowItem=nonTarget)}} {{>lesionTableRow (clone this rowItem=nonTarget)}}
{{/each}} {{/each}}
{{#if gt this.timepoints.get.length 1}} {{#if gt this.timepoints.get.length 1}}
{{>lesionTableHeaderRow id="newLesion" measurements=newLesions currentTimepointId=currentTimepointId}} {{>lesionTableHeaderRow id="newLesion"
measurements=newLesions
currentTimepointId=currentTimepointId
timepointApi=timepointApi}}
{{#each newLesion in newLesions}} {{#each newLesion in newLesions}}
{{>lesionTableRow (clone this rowItem=newLesion)}} {{>lesionTableRow (clone this rowItem=newLesion)}}
{{/each}} {{/each}}

View File

@ -1,32 +1,23 @@
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
Template.lesionTableView.helpers({ Template.lesionTableView.helpers({
targets() { targets() {
// All Targets shall be listed first followed by Non-Targets const withPriors = true;
return Measurements.find({ return MeasurementApi.targets(withPriors);
isTarget: true
}, {
sort: {
lesionNumberAbsolute: 1
}
});
}, },
nonTargets() { nonTargets() {
return Measurements.find({ const withPriors = true;
isTarget: false return MeasurementApi.nonTargets(withPriors);
}, {
sort: {
lesionNumberAbsolute: 1
}
});
}, },
newLesions() { newLesions() {
return Measurements.find({ return MeasurementApi.newLesions();
saved: false },
}, {
sort: { isFollowup() {
lesionNumberAbsolute: 1 const instance = Template.instance();
} const current = instance.data.timepointApi.current();
}); return (current && current.timepointType === 'followup');
} }
}); });

View File

@ -28,7 +28,7 @@ $circleSize = 46px
font-weight: 700 font-weight: 700
font-size: 17px font-size: 17px
text-align: center text-align: center
color: $textColorActive color: $activeColor
top: 0 top: 0
svg svg

View File

@ -9,7 +9,7 @@
</div> </div>
</div> </div>
<table class="table table-striped"> <table class="table table-responsive">
<thead> <thead>
<tr> <tr>
<th class="center">Include Study?</th> <th class="center">Include Study?</th>

View File

@ -133,19 +133,16 @@ Template.toolbarSection.helpers({
return buttonData; return buttonData;
} }
}); });
Template.toolbarSection.events({ Template.toolbarSection.events({
'click #toggleHUD'(event, instance) { 'click #toggleHUD'() {
const state = Session.get('lesionTableHudOpen'); const state = Session.get('lesionTableHudOpen');
Session.set('lesionTableHudOpen', !state); Session.set('lesionTableHudOpen', !state);
} }
}); });
Template.toolbarSection.onRendered(function() { Template.toolbarSection.onRendered(function() {
const instance = Template.instance();
// Set disabled/enabled tool buttons that are set in toolManager // Set disabled/enabled tool buttons that are set in toolManager
const states = toolManager.getToolDefaultStates(); const states = toolManager.getToolDefaultStates();
const disabledToolButtons = states.disabledToolButtons; const disabledToolButtons = states.disabledToolButtons;

View File

@ -1,5 +1,6 @@
import { OHIF } from 'meteor/ohif:core'; import { OHIF } from 'meteor/ohif:core';
import { TimepointApi } from 'meteor/lesiontracker/lib/api/timepoint'; import { TimepointApi } from 'meteor/lesiontracker/client/api/timepoint';
import { MeasurementApi } from 'meteor/lesiontracker/client/api/measurement';
OHIF.viewer = OHIF.viewer || {}; OHIF.viewer = OHIF.viewer || {};
OHIF.viewer.loadIndicatorDelay = 3000; OHIF.viewer.loadIndicatorDelay = 3000;
@ -18,8 +19,6 @@ Session.setDefault('rightSidebar', null);
Template.viewer.onCreated(() => { Template.viewer.onCreated(() => {
const instance = Template.instance(); const instance = Template.instance();
instance.data.timepointApi = new TimepointApi();
ValidationErrors.remove({}); ValidationErrors.remove({});
instance.data.state = new ReactiveDict(); instance.data.state = new ReactiveDict();
@ -91,6 +90,17 @@ Template.viewer.onCreated(() => {
// Set buttons as enabled/disabled when Timepoints collection is ready // Set buttons as enabled/disabled when Timepoints collection is ready
timepointAutoCheck(dataContext); timepointAutoCheck(dataContext);
// Wait until the Timepoint subscription is ready to initialize the TimepointApi
instance.data.timepointApi = new TimepointApi();
instance.data.timepointApi.currentTimepointId = instance.data.currentTimepointId;
// Provide the necessary data to the Measurement API
MeasurementApi.currentTimepointId = instance.data.currentTimepointId;
const prior = instance.data.timepointApi.prior();
if (prior) {
MeasurementApi.priorTimepointId = prior.timepointId;
}
TrialResponseCriteria.validateAllDelayed(); TrialResponseCriteria.validateAllDelayed();
ViewerStudies.find().observe({ ViewerStudies.find().observe({

View File

@ -11,8 +11,9 @@ handleMeasurementAdded = function(e, eventData) {
TrialResponseCriteria.validateDelayed(measurementData); TrialResponseCriteria.validateDelayed(measurementData);
// Set reviewer for this timepoint // Set reviewer for this timepoint
if (measurementData.studyInstanceUid) { if (measurementData.studyInstanceUid) {
Meteor.call('setReviewer',measurementData.studyInstanceUid); Meteor.call('setReviewer', measurementData.studyInstanceUid);
} }
break; break;
case 'ellipticalRoi': case 'ellipticalRoi':
case 'length': case 'length':

View File

@ -18,7 +18,7 @@ handleMeasurementRemoved = function(e, eventData) {
} }
// Set reviewer for this timepoint // Set reviewer for this timepoint
if (measurementData.studyInstanceUid) { if (measurementData.studyInstanceUid) {
Meteor.call('setReviewer',measurementData.studyInstanceUid); Meteor.call('setReviewer', measurementData.studyInstanceUid);
} }
clearMeasurementTimepointData(measurement._id, measurementData.timepointId); clearMeasurementTimepointData(measurement._id, measurementData.timepointId);

View File

@ -265,7 +265,8 @@ Package.onUse(function(api) {
api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.js', 'client'); api.addFiles('client/components/serverInformation/serverInformationModal/serverInformationModal.js', 'client');
// API classes // API classes
api.addFiles('lib/api/timepoint.js'); api.addFiles('client/api/timepoint.js');
api.addFiles('client/api/measurement.js');
// Export global functions // Export global functions
api.export('pixelSpacingAutorunCheck', 'client'); api.export('pixelSpacingAutorunCheck', 'client');