391 lines
14 KiB
JavaScript
391 lines
14 KiB
JavaScript
// Define the Trial Criteria Structure
|
|
TrialCriteriaConstraints = {
|
|
RECIST: RECIST,
|
|
irRC: irRC
|
|
};
|
|
|
|
/**
|
|
* RECIST 1.1 Trial Criteria Definition
|
|
*
|
|
* Baseline Checks:
|
|
* - Extranodal lesions must be >/= 10 mm long axis AND >/= double the acquisition slice thickness by CT and MR
|
|
* - Extranodal lesions must be >/= 20 mm on chest x-ray (although x-rays rarely used for clinical trial assessment)
|
|
* - Nodal lesions must be >/= 15 mm short axis AND >/= double the acquisition slice thickness by CT and MR
|
|
* - Up to a max of 2 target lesions per organ
|
|
* - Up to a max of 5 target lesions total
|
|
* - Non-targets can only be assessed as 'present'
|
|
* - Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)
|
|
* - Time Point Measurement Total = Sum of long axis measurements for extranodal target lesion + short axis measurements for nodal lesions
|
|
*/
|
|
function RECIST(image) {
|
|
var acquisitionSliceThickness;
|
|
if (image) {
|
|
acquisitionSliceThickness = image.acquisitionSliceThickness;
|
|
|
|
// TODO: Use metaData to determine if this is a chest X-ray
|
|
var isChestXray = false;
|
|
}
|
|
|
|
// Define the RECIST 1.1 structure
|
|
var criteria = {
|
|
baseline: {
|
|
target: {},
|
|
nonTarget: {},
|
|
group: {}
|
|
}
|
|
};
|
|
|
|
if (acquisitionSliceThickness) {
|
|
criteria.baseline.target.nodal = {
|
|
shortestDiameter: {
|
|
numericality: {
|
|
greaterThanOrEqualTo: Math.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) {
|
|
var acquisitionSliceThickness;
|
|
if (image) {
|
|
acquisitionSliceThickness = image.acquisitionSliceThickness;
|
|
}
|
|
|
|
// Define the irRC structure
|
|
var criteria = {
|
|
baseline: {
|
|
target: {},
|
|
nonTarget: {}
|
|
},
|
|
followup: {
|
|
newLesions: {
|
|
target: {}
|
|
},
|
|
target: {}
|
|
},
|
|
all: {}
|
|
};
|
|
|
|
if (acquisitionSliceThickness) {
|
|
criteria.baseline.target.all = {
|
|
longestDiameter: {
|
|
numericality: {
|
|
greaterThanOrEqualTo: Math.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 criteriaType A valid Trial Criteria set name (e.g. 'RECIST' or 'irRC')
|
|
* @param imageId A Cornerstone Image ID
|
|
* @returns {*} An Object of Trial Criteria that can be used to validate measurements' conformance
|
|
*/
|
|
getTrialCriteriaConstraints = function(criteriaType, imageId) {
|
|
if (!TrialCriteriaConstraints[criteriaType]) {
|
|
throw 'No such Trial Criteria defined: ' + criteriaType;
|
|
}
|
|
|
|
// If no imageId was specified, skip customization of the criteria
|
|
// and return the requested criteria right away
|
|
var criteria;
|
|
if (!imageId) {
|
|
criteria = TrialCriteriaConstraints[criteriaType]();
|
|
return criteria;
|
|
}
|
|
|
|
// Otherwise, retrieve the series metaData to identify the modality of the image
|
|
var seriesMetaData = cornerstoneTools.metaData.get('series', imageId);
|
|
if (!seriesMetaData) {
|
|
return;
|
|
}
|
|
|
|
// TODO: Get the rest of the metaData that has already been loaded by Cornerstone
|
|
var image = {};
|
|
|
|
// If we are looking at an MR or CT image, we should pass the slice thickness
|
|
// to the Trial Criteria functions so that they can customize the validation rules
|
|
if (seriesMetaData.modality === 'MR' || seriesMetaData.modality === 'CT') {
|
|
var instanceMetaData = cornerstoneTools.metaData.get('instance', imageId);
|
|
image.acquisitionSliceThickness = instanceMetaData.sliceThickness;
|
|
}
|
|
|
|
// Retrieve the study metaData in order to find the timepoint type
|
|
var studyMetaData = cornerstoneTools.metaData.get('study', imageId);
|
|
if (!studyMetaData) {
|
|
return;
|
|
}
|
|
|
|
// Retrieve the Study document from the Collection of associated Studies
|
|
var study = Studies.findOne({
|
|
studyInstanceUid: studyMetaData.studyInstanceUid
|
|
});
|
|
|
|
if (!study) {
|
|
return;
|
|
}
|
|
|
|
// Find the related Timepoint document
|
|
var timepoint = Timepoints.findOne({
|
|
timepointId: study.timepointId
|
|
});
|
|
|
|
if (!timepoint) {
|
|
log.warn('Timepoint related to study is missing.');
|
|
return;
|
|
}
|
|
|
|
// Retrieve the Timepoint's type (e.g. 'baseline' or 'followup')
|
|
var timepointType = timepoint.timepointType;
|
|
|
|
// Obtain the customized trial criteria given the image metaData
|
|
criteria = TrialCriteriaConstraints[criteriaType](image);
|
|
|
|
// Return the relevant criteria given the current timepoint type
|
|
return criteria[timepointType];
|
|
};
|