Removing unused code from LesionTracker
This commit is contained in:
parent
bacfa24216
commit
67a525167b
@ -53,8 +53,6 @@ Template.viewer.onCreated(() => {
|
|||||||
Object.assign(OHIF.viewer, apis);
|
Object.assign(OHIF.viewer, apis);
|
||||||
Object.assign(instance.data, apis);
|
Object.assign(instance.data, apis);
|
||||||
|
|
||||||
ValidationErrors.remove({});
|
|
||||||
|
|
||||||
instance.state = new ReactiveDict();
|
instance.state = new ReactiveDict();
|
||||||
instance.state.set('leftSidebar', Session.get('leftSidebar'));
|
instance.state.set('leftSidebar', Session.get('leftSidebar'));
|
||||||
instance.state.set('rightSidebar', Session.get('rightSidebar'));
|
instance.state.set('rightSidebar', Session.get('rightSidebar'));
|
||||||
|
|||||||
@ -64,22 +64,8 @@ const BidirectionalSchema = new SimpleSchema([MeasurementSchemaTypes.Cornerstone
|
|||||||
}]);
|
}]);
|
||||||
|
|
||||||
const displayFunction = data => {
|
const displayFunction = data => {
|
||||||
// Check whether this is a Nodal or Extranodal Measurement
|
|
||||||
// const targetType = 'target';
|
|
||||||
// const nodalType = data.isNodal ? 'nodal' : 'extraNodal';
|
|
||||||
|
|
||||||
// Get criteria types
|
|
||||||
// const criteriaTypes = OHIF.lesiontracker.TrialCriteriaTypes.find({
|
|
||||||
// selected: true
|
|
||||||
// }).map(criteria => {
|
|
||||||
// return criteria.id;
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const currentConstraints = OHIF.lesiontracker.getTrialCriteriaConstraints(criteriaTypes, data.imageId);
|
|
||||||
|
|
||||||
if (data.shortestDiameter) {
|
if (data.shortestDiameter) {
|
||||||
// TODO: Make this check criteria again to see if we should display
|
// TODO: Make this check criteria again to see if we should display shortest x longest
|
||||||
// shortest x longest
|
|
||||||
return data.longestDiameter + ' x ' + data.shortestDiameter;
|
return data.longestDiameter + ' x ' + data.shortestDiameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,378 +0,0 @@
|
|||||||
import { OHIF } from 'meteor/ohif:core';
|
|
||||||
|
|
||||||
// Create a client-only Collection to store our Validation Errors
|
|
||||||
ValidationErrors = new Meteor.Collection(null);
|
|
||||||
ValidationErrors._debugName = 'ValidationErrors';
|
|
||||||
|
|
||||||
// Set Validate.js Library's default options
|
|
||||||
validate.options = {
|
|
||||||
format: 'detailed'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an array of validation error messages given an Object of validation errors
|
|
||||||
* and an optional prefix for the messages. An example of a useful prefix would be
|
|
||||||
* the location of the measurement or something like 'Target 1 '.
|
|
||||||
*
|
|
||||||
* @param validationErrors
|
|
||||||
* @param prefix
|
|
||||||
*/
|
|
||||||
function addValidationErrorsToCollection(validationErrors, prefix, type) {
|
|
||||||
// If no input was given, stop here
|
|
||||||
if (!validationErrors || !validationErrors.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through each of the entries in the validationErrors Array
|
|
||||||
validationErrors.forEach(function(validationError) {
|
|
||||||
var existingError = ValidationErrors.findOne({
|
|
||||||
attribute: validationError.attribute,
|
|
||||||
validator: validationError.validator,
|
|
||||||
error: validationError.error,
|
|
||||||
prefix: prefix
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingError) {
|
|
||||||
ValidationErrors.update(existingError._id, {
|
|
||||||
$set: {
|
|
||||||
value: validationError.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
validationError.type = type;
|
|
||||||
validationError.prefix = prefix;
|
|
||||||
ValidationErrors.insert(validationError);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs conformance checks related to a group of measurements. This function
|
|
||||||
* searches the input object of Constraints and looks for the 'group' attribute.
|
|
||||||
*
|
|
||||||
* It calculates some general group-level values for the current set of Measurements
|
|
||||||
* and validates these using the input constraints.
|
|
||||||
*
|
|
||||||
* @param constraints
|
|
||||||
* @returns {Array} Array of error messages related to the input conformance checks
|
|
||||||
*/
|
|
||||||
function assessGroupOfMeasurements(constraints) {
|
|
||||||
OHIF.log.info('assessGroupOfMeasurements');
|
|
||||||
|
|
||||||
// Retrieve the group-level constraints
|
|
||||||
var groupConstraints = constraints.group;
|
|
||||||
|
|
||||||
// If no group-level constraints exist, stop here
|
|
||||||
if (!groupConstraints) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var type = 'group';
|
|
||||||
ValidationErrors.remove({
|
|
||||||
type: type
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate some simple group-level Measurement statistics for validation
|
|
||||||
var testStructure = {
|
|
||||||
totalNumberOfLesions: Measurements.find().count()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run the conformance checks with the validate.js library
|
|
||||||
var validationErrors = validate(testStructure, groupConstraints);
|
|
||||||
|
|
||||||
// Return any error messages as a flattened array of errors
|
|
||||||
addValidationErrorsToCollection(validationErrors, '', type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs conformance checks related to per-organ sets of measurements.
|
|
||||||
*
|
|
||||||
* This function searches the input object of Constraints and looks for the
|
|
||||||
* 'perOrgan' attribute.
|
|
||||||
*
|
|
||||||
* It calculates some general per-organ statistics for the current set of Measurements
|
|
||||||
* and validates these using the input constraints.
|
|
||||||
*
|
|
||||||
* @param constraints
|
|
||||||
* @returns {Array} Array of error messages related to the input conformance checks
|
|
||||||
*/
|
|
||||||
function assessMeasurementPerOrgan(constraints) {
|
|
||||||
OHIF.log.info('assessMeasurementPerOrgan');
|
|
||||||
|
|
||||||
// Retrieve the per-organ constraints
|
|
||||||
var perOrganConstraints = constraints.perOrgan;
|
|
||||||
|
|
||||||
// If no per-organ constraints exist, stop here
|
|
||||||
if (!perOrganConstraints) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a list of all unique locations that contain measurements
|
|
||||||
// by looping through the Measurements Collection
|
|
||||||
var organLocations = [];
|
|
||||||
Measurements.find().forEach(function(measurement) {
|
|
||||||
if (organLocations.indexOf(measurement.location) > -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
organLocations.push(measurement.location);
|
|
||||||
});
|
|
||||||
|
|
||||||
var type = 'perOrgan';
|
|
||||||
ValidationErrors.remove({
|
|
||||||
type: type
|
|
||||||
});
|
|
||||||
|
|
||||||
// Loop through each unique organ location in order to validate
|
|
||||||
// the per-organ constraints for each organ
|
|
||||||
organLocations.forEach(function(location) {
|
|
||||||
// Calculate the number of Lesions per Organ
|
|
||||||
var numberOfLesionsPerOrgan = Measurements.find({
|
|
||||||
location: location
|
|
||||||
}).count();
|
|
||||||
|
|
||||||
// Store per-organ Measurement statistics for validation
|
|
||||||
// Right now this is only the numberOfLesionsPerOrgan, but later
|
|
||||||
// this may include other checks
|
|
||||||
var testStructure = {
|
|
||||||
numberOfLesionsPerOrgan: numberOfLesionsPerOrgan
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run the conformance checks with the validate.js library
|
|
||||||
var validationErrors = validate(testStructure, perOrganConstraints);
|
|
||||||
|
|
||||||
// Obtain any error messages as a flattened array of errors, prefixed
|
|
||||||
// with the Organ name, in the form 'Liver Left: '
|
|
||||||
addValidationErrorsToCollection(validationErrors, location + ': ', type);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs conformance checks on a single Measurement given the
|
|
||||||
* cornerstone toolData related to it.
|
|
||||||
*
|
|
||||||
* @param constraints
|
|
||||||
* @param measurementData CornerstoneTools toolData Object for this specific Measurement
|
|
||||||
* @returns {Array} Array of error messages related to the input conformance checks
|
|
||||||
*/
|
|
||||||
function assessSingleMeasurement(constraints, measurementData) {
|
|
||||||
OHIF.log.info('assessSingleMeasurement');
|
|
||||||
|
|
||||||
// Check whether this is a Target or Non-Target Measurement
|
|
||||||
var targetType = measurementData.isTarget ? 'target' : 'nonTarget';
|
|
||||||
|
|
||||||
// Retrieve any target/non-target-specific single-measurement constraints
|
|
||||||
// from the input constraint structure
|
|
||||||
var measurementConstraints = constraints[targetType];
|
|
||||||
|
|
||||||
// If no relevant constraints exist, stop here
|
|
||||||
if (!measurementConstraints) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether this is a Nodal or Extranodal Measurement
|
|
||||||
var nodalType = measurementData.isNodal ? 'nodal' : 'extraNodal';
|
|
||||||
|
|
||||||
// Retrieve any nodal/extra-nodal-specific constraints to see if we can apply them
|
|
||||||
var constraintsToApply;
|
|
||||||
if (measurementData.isNodal !== undefined && measurementConstraints[nodalType]) {
|
|
||||||
// Check if we have enough information (about nodality of this Measurement,
|
|
||||||
// and nodality-specific constraints) to apply nodality-specific constraints
|
|
||||||
constraintsToApply = measurementConstraints[nodalType];
|
|
||||||
} else if (measurementConstraints.all) {
|
|
||||||
// If we have no data about the nodality of this Measurement, or no relevant
|
|
||||||
// specific constraints, we should apply the constraints valid for 'all' nodality
|
|
||||||
// types
|
|
||||||
constraintsToApply = measurementConstraints.all;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate a lesion name based on whether or not we have a Target or Non-target
|
|
||||||
// Measurement, and the lesion number of this Measurement.
|
|
||||||
var lesionName = measurementData.isTarget ? 'Target' : 'Non-target';
|
|
||||||
lesionName = lesionName + ' ' + measurementData.lesionNumber + ': ';
|
|
||||||
|
|
||||||
ValidationErrors.remove({
|
|
||||||
prefix: lesionName
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use validate.js to check the criteria
|
|
||||||
var validationErrors = validate(measurementData, constraintsToApply);
|
|
||||||
|
|
||||||
if (validationErrors) {
|
|
||||||
validationErrors.forEach(function(error) {
|
|
||||||
error.measurementId = measurementData._id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the Lesion Name as a prefix to concatenate any validation error messages into
|
|
||||||
// an array to return
|
|
||||||
addValidationErrorsToCollection(validationErrors, lesionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate from a single Measurement up the chain to include group and perOrgan
|
|
||||||
* conformance checks
|
|
||||||
*
|
|
||||||
* @param measurementData The CornerstoneTools toolData for a single Measurement
|
|
||||||
*/
|
|
||||||
function validateSingleMeasurement(measurementData) {
|
|
||||||
// Obtain the name of the current TrialResponseAssessmentCriteria that
|
|
||||||
// we are using.
|
|
||||||
var criteriaTypes = OHIF.lesiontracker.TrialCriteriaTypes.find({
|
|
||||||
selected: true
|
|
||||||
}).map(function(criteria) {
|
|
||||||
return criteria.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
const imageId = OHIF.viewerbase.getImageIdForImagePath(measurementData.imagePath);
|
|
||||||
var currentConstraints = OHIF.lesiontracker.getTrialCriteriaConstraints(criteriaTypes, imageId);
|
|
||||||
|
|
||||||
// If we have no relevant constraints, stop here
|
|
||||||
if (!currentConstraints) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the relevant Measurement in the Measurements Collection
|
|
||||||
var measurement = Measurements.findOne(measurementData.id);
|
|
||||||
|
|
||||||
// If no such Measurement exists, stop here
|
|
||||||
if (!measurement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the current timepointId that the user was editing the Measurement on
|
|
||||||
var timepointId = measurementData.timepointId;
|
|
||||||
|
|
||||||
// Find the specific measurement data for this Measurement at this Timepoint
|
|
||||||
var currentMeasurement = measurement.timepoints[timepointId];
|
|
||||||
|
|
||||||
// Return here if the measurement was removed during validation
|
|
||||||
if (!currentMeasurement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include target and nodal flags on the timepoint-specific data so it is easier to validate
|
|
||||||
// TODO: Rethink what to pass to assessSingleMeasurement?
|
|
||||||
currentMeasurement.isTarget = measurement.isTarget;
|
|
||||||
currentMeasurement.isNodal = measurement.isNodal;
|
|
||||||
currentMeasurement.lesionNumber = measurement.lesionNumber;
|
|
||||||
currentMeasurement._id = measurement._id;
|
|
||||||
|
|
||||||
// Run the single-measurement-specific conformance checks
|
|
||||||
// If any messages exist, add them to the array of messages
|
|
||||||
assessSingleMeasurement(currentConstraints, currentMeasurement);
|
|
||||||
|
|
||||||
validateGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateGroups() {
|
|
||||||
OHIF.log.info('validateGroups');
|
|
||||||
|
|
||||||
// Obtain the names of the current TrialResponseAssessmentCriteria that
|
|
||||||
// we are using.
|
|
||||||
var criteriaTypes = OHIF.lesiontracker.TrialCriteriaTypes.find({
|
|
||||||
selected: true
|
|
||||||
}).map(function(criteria) {
|
|
||||||
return criteria.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Criteria for the specific image are retrieved from the general set of criteria.
|
|
||||||
var currentConstraints = OHIF.lesiontracker.getTrialCriteriaConstraints(criteriaTypes);
|
|
||||||
if (!currentConstraints) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Revisit this! We can't use the Timepoints collection inside ANY
|
|
||||||
// of these functions, since it causes an infinite loop, since Measurement
|
|
||||||
// validation is performed inside the observe:added hook for the Measurements
|
|
||||||
// Collection.
|
|
||||||
|
|
||||||
var timepointTypes = ['baseline', 'followup'];
|
|
||||||
timepointTypes.forEach(function(timepointType) {
|
|
||||||
// Retrieve the current constraints which apply to the specific Timepoint type
|
|
||||||
// (e.g. baseline, followup) that this Measurement is being edited on.
|
|
||||||
var timepointConstraints = currentConstraints[timepointType];
|
|
||||||
if (!timepointConstraints) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the group-level conformance checks
|
|
||||||
assessGroupOfMeasurements(timepointConstraints);
|
|
||||||
|
|
||||||
// Run the per-organ conformance checks
|
|
||||||
assessMeasurementPerOrgan(timepointConstraints);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateAll() {
|
|
||||||
// Obtain the names of the current TrialResponseAssessmentCriteria that
|
|
||||||
// we are using.
|
|
||||||
var criteriaTypes = OHIF.lesiontracker.TrialCriteriaTypes.find({
|
|
||||||
selected: true
|
|
||||||
}).map(function(criteria) {
|
|
||||||
return criteria.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
Measurements.find().forEach(function(measurement) {
|
|
||||||
Object.keys(measurement.timepoints).forEach(function(timepointId) {
|
|
||||||
var currentMeasurement = measurement.timepoints[timepointId];
|
|
||||||
currentMeasurement.isTarget = measurement.isTarget;
|
|
||||||
currentMeasurement.isNodal = measurement.isNodal;
|
|
||||||
currentMeasurement.lesionNumber = measurement.lesionNumber;
|
|
||||||
currentMeasurement._id = measurement._id;
|
|
||||||
|
|
||||||
// Criteria for the specific image are retrieved from the general set of criteria.
|
|
||||||
const imageId = OHIF.viewerbase.getImageIdForImagePath(currentMeasurement.imagePath);
|
|
||||||
var currentConstraints = OHIF.lesiontracker.getTrialCriteriaConstraints(criteriaTypes, imageId);
|
|
||||||
if (!currentConstraints) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the single-measurement-specific conformance checks
|
|
||||||
// If any messages exist, add them to the array of messages
|
|
||||||
assessSingleMeasurement(currentConstraints, currentMeasurement);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
validateGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
var validationTimeout = 400;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate the measurements after a set delay period
|
|
||||||
*
|
|
||||||
* @param measurementData Input measurement data from CornerstoneTools
|
|
||||||
*/
|
|
||||||
function validateDelayed(measurementData) {
|
|
||||||
// Erase any currently-waiting validation call
|
|
||||||
clearTimeout(validationTimeout);
|
|
||||||
|
|
||||||
// Set a timeout to run validation after a delay
|
|
||||||
// Currently this is 400 milliseconds
|
|
||||||
setTimeout(function() {
|
|
||||||
validateSingleMeasurement(measurementData);
|
|
||||||
}, validationTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate all measurements after a set delay period
|
|
||||||
*/
|
|
||||||
function validateAllDelayed() {
|
|
||||||
// Erase any currently-waiting validation call
|
|
||||||
clearTimeout(validationTimeout);
|
|
||||||
|
|
||||||
// Set a timeout to run validation after a delay
|
|
||||||
// Currently this is 400 milliseconds
|
|
||||||
setTimeout(function() {
|
|
||||||
validateAll();
|
|
||||||
}, validationTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
TrialResponseCriteria = {
|
|
||||||
validateAll: validateAll,
|
|
||||||
validateAllDelayed: validateAllDelayed,
|
|
||||||
validateSingleMeasurement: validateSingleMeasurement,
|
|
||||||
validateDelayed: validateDelayed,
|
|
||||||
validateGroups: validateGroups
|
|
||||||
};
|
|
||||||
@ -1,396 +0,0 @@
|
|||||||
import { OHIF } from 'meteor/ohif:core';
|
|
||||||
|
|
||||||
// Define the Trial Criteria Structure
|
|
||||||
OHIF.lesiontracker.TrialCriteriaConstraints = {
|
|
||||||
RECIST: RECIST,
|
|
||||||
irRC: irRC
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RECIST 1.1 Trial Criteria Definition
|
|
||||||
*
|
|
||||||
* Baseline Checks:
|
|
||||||
* - Extranodal lesions must be >/= 10 mm long axis AND >/= double the acquisition slice thickness by CT and MR
|
|
||||||
* - Extranodal lesions must be >/= 20 mm on chest x-ray (although x-rays rarely used for clinical trial assessment)
|
|
||||||
* - Nodal lesions must be >/= 15 mm short axis AND >/= double the acquisition slice thickness by CT and MR
|
|
||||||
* - Up to a max of 2 target lesions per organ
|
|
||||||
* - Up to a max of 5 target lesions total
|
|
||||||
* - Non-targets can only be assessed as 'present'
|
|
||||||
* - Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)
|
|
||||||
* - Time Point Measurement Total = Sum of long axis measurements for extranodal target lesion + short axis measurements for nodal lesions
|
|
||||||
*/
|
|
||||||
function RECIST(image) {
|
|
||||||
let acquisitionSliceThickness;
|
|
||||||
|
|
||||||
let isChestXray;
|
|
||||||
if (image) {
|
|
||||||
acquisitionSliceThickness = image.acquisitionSliceThickness;
|
|
||||||
|
|
||||||
// TODO: Use metaData to determine if this is a chest X-ray
|
|
||||||
isChestXray = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the RECIST 1.1 structure
|
|
||||||
const criteria = {
|
|
||||||
baseline: {
|
|
||||||
target: {},
|
|
||||||
nonTarget: {},
|
|
||||||
group: {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (acquisitionSliceThickness) {
|
|
||||||
criteria.baseline.target.nodal = {
|
|
||||||
shortestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: Math.max(15, 2 * acquisitionSliceThickness),
|
|
||||||
message: '^Nodal lesions must be >= 15 mm short axis AND >= double the acquisition slice thickness (' +
|
|
||||||
acquisitionSliceThickness + ' mm) for CT and MR.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
criteria.baseline.target.nodal = {
|
|
||||||
shortestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: 15,
|
|
||||||
//message: '^Nodal target lesions must be >= %{count} mm short axis'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
criteria.baseline.target.all = {
|
|
||||||
// - Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)
|
|
||||||
response: {
|
|
||||||
exclusion: {
|
|
||||||
within: {
|
|
||||||
CR: 'Complete Response (CR)',
|
|
||||||
UN: 'Unknown (UN)',
|
|
||||||
NE: 'Non-evaluable (NE)',
|
|
||||||
EX: 'Excluded (EX)'
|
|
||||||
},
|
|
||||||
message: '^Target lesions must have a length and cannot be marked as %{value} at baseline.'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
totalLesionBurden: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: 2, // TODO: Check this, the value wasn't specified!
|
|
||||||
//message: '^Total lesion burden (SPD target lesions + SPD new lesions) should be greater than %{count}.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
criteria.baseline.nonTarget.all = {
|
|
||||||
// - Non-targets can only be assessed as 'present'
|
|
||||||
response: {
|
|
||||||
// This is a workaround since Validating equality to something is not implemented yet
|
|
||||||
// https://github.com/ansman/validate.js/issues/79
|
|
||||||
presence: {
|
|
||||||
message: "^Non-target lesions can only be assessed as 'Present' at Baseline"
|
|
||||||
},
|
|
||||||
inclusion: {
|
|
||||||
within: ['Present'],
|
|
||||||
message: "^Non-target lesions can only be assessed as 'Present' at Baseline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
criteria.baseline.perOrgan = {
|
|
||||||
numberOfLesionsPerOrgan: {
|
|
||||||
numericality: {
|
|
||||||
lessThanOrEqualTo: 2,
|
|
||||||
//message: '^A maximum of %{count} target lesions per organ are allowed at Baseline.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
criteria.baseline.group = {
|
|
||||||
totalNumberOfLesions: {
|
|
||||||
numericality: {
|
|
||||||
lessThanOrEqualTo: 5,
|
|
||||||
//message: '^A maximum of %{count} target lesions total are allowed at Baseline.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (acquisitionSliceThickness) {
|
|
||||||
criteria.baseline.target.extraNodal = {
|
|
||||||
longestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: Math.max(10, 2 * acquisitionSliceThickness),
|
|
||||||
message: '^Extranodal lesions must be >= 10 mm long axis AND >= double the acquisition slice thickness (' +
|
|
||||||
acquisitionSliceThickness + ' mm) for CT and MR.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else if (isChestXray) {
|
|
||||||
criteria.baseline.target.extraNodal = {
|
|
||||||
// -
|
|
||||||
longestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: 20,
|
|
||||||
//message: '^Extranodal lesions must be >= %{count} mm on chest X-ray'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
criteria.baseline.target.extraNodal = {
|
|
||||||
longestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: 10,
|
|
||||||
//message: '^Extranodal target lesions must be >= %{count} mm long axis'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return criteria;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* irRC Trial Criteria Definition
|
|
||||||
*
|
|
||||||
* Baseline Checks:
|
|
||||||
* - Target lesions must be >/= 10 X 10 mm
|
|
||||||
* - Up to a max of 5 target lesions per organ
|
|
||||||
* - Up to a max of 10 target lesions total
|
|
||||||
* - Non-targets can only be assessed as 'present'
|
|
||||||
* - Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)
|
|
||||||
*/
|
|
||||||
function irRC(image) {
|
|
||||||
let acquisitionSliceThickness;
|
|
||||||
if (image) {
|
|
||||||
acquisitionSliceThickness = image.acquisitionSliceThickness;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the irRC structure
|
|
||||||
const criteria = {
|
|
||||||
baseline: {
|
|
||||||
target: {},
|
|
||||||
nonTarget: {}
|
|
||||||
},
|
|
||||||
followup: {
|
|
||||||
newLesions: {
|
|
||||||
target: {}
|
|
||||||
},
|
|
||||||
target: {}
|
|
||||||
},
|
|
||||||
all: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (acquisitionSliceThickness) {
|
|
||||||
criteria.baseline.target.all = {
|
|
||||||
longestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: Math.max(10, acquisitionSliceThickness),
|
|
||||||
message: '^Target lesions must be >= 10 mm long axis AND >= double the acquisition slice thickness (' +
|
|
||||||
acquisitionSliceThickness + ' mm) for CT and MR.'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shortestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: Math.max(10, acquisitionSliceThickness),
|
|
||||||
message: '^Target lesions must be >= 10 mm short axis AND >= double the acquisition slice thickness (' +
|
|
||||||
acquisitionSliceThickness + ' mm) for CT and MR.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
criteria.baseline.target.all = {
|
|
||||||
longestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: 10,
|
|
||||||
//message: '^Target lesions must be >= %{count} mm long axis.'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shortestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: 10,
|
|
||||||
//message: '^Target lesions must be >= %{count} mm short axis.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
criteria.baseline.target.all.response = {
|
|
||||||
exclusion: {
|
|
||||||
within: {
|
|
||||||
CR: 'Complete Response (CR)',
|
|
||||||
UN: 'Unknown (UN)',
|
|
||||||
NE: 'Non-evaluable (NE)',
|
|
||||||
EX: 'Excluded (EX)'
|
|
||||||
},
|
|
||||||
message: '^^Target lesions must have a length and cannot be marked as %{value} at baseline.'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
criteria.baseline.nonTarget.all = {
|
|
||||||
response: {
|
|
||||||
// This is a workaround since Validating equality to something is not implemented yet
|
|
||||||
// https://github.com/ansman/validate.js/issues/79
|
|
||||||
presence: {
|
|
||||||
message: "^Non-target lesions can only be assessed as 'Present' at Baseline"
|
|
||||||
},
|
|
||||||
inclusion: {
|
|
||||||
within: ['Present'],
|
|
||||||
message: "^Non-target lesions can only be assessed as 'Present' at Baseline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
criteria.baseline.perOrgan = {
|
|
||||||
numberOfLesionsPerOrgan: {
|
|
||||||
numericality: {
|
|
||||||
lessThanOrEqualTo: 5,
|
|
||||||
//message: '^A maximum of %{count} target lesions per organ are allowed at Baseline.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
criteria.baseline.group = {
|
|
||||||
totalNumberOfLesions: {
|
|
||||||
numericality: {
|
|
||||||
lessThanOrEqualTo: 10,
|
|
||||||
//message: '^A maximum of %{count} target lesions total are allowed at Baseline.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (acquisitionSliceThickness) {
|
|
||||||
criteria.followup.newLesions.target.all = {
|
|
||||||
// - New target lesions must be >/= 5 X 5 mm AND >/= double the acquisition slice thickness by CT and MR
|
|
||||||
longestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: Math.max(5, 2 * acquisitionSliceThickness),
|
|
||||||
message: '^New target lesions must be >= 5 mm long axis AND >= double the acquisition slice thickness (' +
|
|
||||||
acquisitionSliceThickness + ' mm) for CT and MR.'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shortestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: Math.max(5, 2 * acquisitionSliceThickness),
|
|
||||||
message: '^New target lesions must be >= 5 mm short axis AND >= double the acquisition slice thickness (' +
|
|
||||||
acquisitionSliceThickness + ' mm) for CT and MR.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
criteria.followup.newLesions.target.all = {
|
|
||||||
// - New target lesions must be >/= 5 X 5 mm
|
|
||||||
longestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: 5,
|
|
||||||
//message: '^New target lesions must be >= %{count} mm long axis.'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shortestDiameter: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: 5,
|
|
||||||
//message: '^New target lesions must be >= %{count} mm short axis.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
criteria.followup.group = {
|
|
||||||
numberOfLesionsPerOrgan: {
|
|
||||||
numericality: {
|
|
||||||
lessThanOrEqualTo: 5,
|
|
||||||
//message: '^A maximum of %{count} target lesions per organ are allowed at Followup.'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
totalNumberOfLesions: {
|
|
||||||
numericality: {
|
|
||||||
lessThanOrEqualTo: 10,
|
|
||||||
//message: '^A maximum of %{count} target lesions total are allowed at Followup.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Check the actual requirement for total burden!
|
|
||||||
criteria.all.group = {
|
|
||||||
totalLesionBurden: {
|
|
||||||
numericality: {
|
|
||||||
greaterThanOrEqualTo: 100,
|
|
||||||
//message: '^Total lesion burden (SPD target lesions + SPD new lesions) should be greater than %{count}.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return criteria;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve trial criteria constraints based on the image that measurements appear upon
|
|
||||||
* If no image is specified, it is assumed that group or per Organ level criteria are desired.
|
|
||||||
*
|
|
||||||
* @param criteriaTypes An array of valid Trial Criteria set names (e.g. ['RECIST', 'irRC'])
|
|
||||||
* NOTE: Multiple criteria are not yet supported
|
|
||||||
*
|
|
||||||
* @param imageId A Cornerstone Image ID
|
|
||||||
* @returns {*} An Object of Trial Criteria that can be used to validate measurements' conformance
|
|
||||||
*/
|
|
||||||
OHIF.lesiontracker.getTrialCriteriaConstraints = (criteriaTypes, imageId) => {
|
|
||||||
// TODO: update this when we allow multiple criteria
|
|
||||||
const allCriteria = [];
|
|
||||||
criteriaTypes.forEach(function(criteriaType) {
|
|
||||||
if (!OHIF.lesiontracker.TrialCriteriaConstraints[criteriaType]) {
|
|
||||||
throw 'No such Trial Criteria defined: ' + criteriaType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no imageId was specified, skip customization of the criteria
|
|
||||||
// and return the requested criteria right away
|
|
||||||
let criteria;
|
|
||||||
if (!imageId) {
|
|
||||||
criteria = OHIF.lesiontracker.TrialCriteriaConstraints[criteriaType]();
|
|
||||||
allCriteria.push(criteria);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, retrieve the series metaData to identify the modality of the image
|
|
||||||
const seriesMetaData = cornerstoneTools.metaData.get('series', imageId);
|
|
||||||
if (!seriesMetaData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Get the rest of the metaData that has already been loaded by Cornerstone
|
|
||||||
const image = {};
|
|
||||||
|
|
||||||
// If we are looking at an MR or CT image, we should pass the slice thickness
|
|
||||||
// to the Trial Criteria functions so that they can customize the validation rules
|
|
||||||
if (seriesMetaData.modality === 'MR' || seriesMetaData.modality === 'CT') {
|
|
||||||
const instanceMetaData = cornerstoneTools.metaData.get('instance', imageId);
|
|
||||||
image.acquisitionSliceThickness = instanceMetaData.sliceThickness;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the study metaData in order to find the timepoint type
|
|
||||||
const studyMetaData = cornerstoneTools.metaData.get('study', imageId);
|
|
||||||
if (!studyMetaData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the related Timepoint document
|
|
||||||
const { timepointApi } = OHIF.viewer;
|
|
||||||
if (!timepointApi) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timepoint = timepointApi.study(studyMetaData.studyInstanceUid)[0];
|
|
||||||
if (!timepoint) {
|
|
||||||
OHIF.log.warn('Timepoint related to study is missing.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the Timepoint's type (e.g. 'baseline' or 'followup')
|
|
||||||
const timepointType = timepoint.timepointType;
|
|
||||||
|
|
||||||
// Obtain the customized trial criteria given the image metaData
|
|
||||||
criteria = OHIF.lesiontracker.TrialCriteriaConstraints[criteriaType](image);
|
|
||||||
|
|
||||||
// Return the relevant criteria given the current timepoint type
|
|
||||||
allCriteria.push(criteria[timepointType]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return allCriteria[0];
|
|
||||||
};
|
|
||||||
@ -5,8 +5,6 @@ import './studylist/studylistModification.js';
|
|||||||
import './bidirectional';
|
import './bidirectional';
|
||||||
|
|
||||||
// Library functions
|
// Library functions
|
||||||
import './TrialCriteriaConstraints.js';
|
|
||||||
import './MeasurementValidation.js';
|
|
||||||
import './pixelSpacingAutorunCheck.js';
|
import './pixelSpacingAutorunCheck.js';
|
||||||
import './removeMeasurementIfInvalid.js';
|
import './removeMeasurementIfInvalid.js';
|
||||||
import './toggleLesionTrackerTools.js';
|
import './toggleLesionTrackerTools.js';
|
||||||
|
|||||||
@ -31,14 +31,7 @@ Package.onUse(function(api) {
|
|||||||
|
|
||||||
api.addFiles('client/index.js', 'client');
|
api.addFiles('client/index.js', 'client');
|
||||||
|
|
||||||
// Export global functions
|
|
||||||
api.export('convertNonTarget', 'client');
|
|
||||||
|
|
||||||
// Export client-side collections
|
// Export client-side collections
|
||||||
api.export('ValidationErrors', 'client');
|
|
||||||
api.export('LesionLocations', 'client');
|
api.export('LesionLocations', 'client');
|
||||||
api.export('LocationResponses', 'client');
|
api.export('LocationResponses', 'client');
|
||||||
|
|
||||||
// Export collections spanning both client and server
|
|
||||||
api.export('Configuration', ['client', 'server']);
|
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user