From 5366e1217d2362817969ba1985d50155025a1980 Mon Sep 17 00:00:00 2001 From: Leonardo Campos Date: Mon, 16 Jan 2017 12:33:59 -0200 Subject: [PATCH] LT-92: Integrating RECIST evaluator and trial configuration --- .../both/configuration/measurements.js | 5 ++ .../client/conformance/ConformanceCriteria.js | 64 ++++++++++++++++--- .../client/conformance/CriteriaEvaluator.js | 6 +- .../client/conformance/criteria/Location.js | 36 +++++++++++ .../client/conformance/criteria/index.js | 1 + .../client/conformance/evaluations/index.js | 3 + .../conformance/evaluations/recist.json | 35 ++++++++++ .../evaluations/recistBaseline.json | 29 --------- 8 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 Packages/ohif-measurements/client/conformance/criteria/Location.js create mode 100644 Packages/ohif-measurements/client/conformance/evaluations/index.js create mode 100644 Packages/ohif-measurements/client/conformance/evaluations/recist.json delete mode 100644 Packages/ohif-measurements/client/conformance/evaluations/recistBaseline.json diff --git a/Packages/ohif-measurements/both/configuration/measurements.js b/Packages/ohif-measurements/both/configuration/measurements.js index 42fc1ba94..33b57e714 100644 --- a/Packages/ohif-measurements/both/configuration/measurements.js +++ b/Packages/ohif-measurements/both/configuration/measurements.js @@ -109,6 +109,10 @@ class MeasurementApi { this.changeObserver.changed(); }; + const changedHandler = () => { + this.changeObserver.changed(); + } + const removedHandler = measurement => { const measurementNumber = measurement.measurementNumber; @@ -167,6 +171,7 @@ class MeasurementApi { collection.find().observe({ added: addedHandler, + changed: changedHandler, removed: removedHandler }); }); diff --git a/Packages/ohif-measurements/client/conformance/ConformanceCriteria.js b/Packages/ohif-measurements/client/conformance/ConformanceCriteria.js index ddf1d9bcb..82d6eaa47 100644 --- a/Packages/ohif-measurements/client/conformance/ConformanceCriteria.js +++ b/Packages/ohif-measurements/client/conformance/ConformanceCriteria.js @@ -1,31 +1,72 @@ import { OHIF } from 'meteor/ohif:core'; import { _ } from 'meteor/underscore'; import { CriteriaEvaluator } from './CriteriaEvaluator'; -import * as recistBaselineEvaluation from './evaluations/recistBaseline.json'; +import * as evaluations from './evaluations'; class ConformanceCriteria { constructor(measurementApi, timepointApi) { this.measurementApi = measurementApi; this.timepointApi = timepointApi; + this.results = []; - this.warnings = {}; + Tracker.autorun(() => { + const trialCriteriaType = TrialCriteriaTypes.findOne({ selected: true }); + this.measurementApi.changeObserver.depend(); + this.validate(trialCriteriaType); + }); } - validate() { - const data = this.getData(); - this.validateRecist(data); + validate(trialCriteriaType) { + const baselineData = this.getData('baseline'); + const followupData = this.getData('followup'); + const mergedData = baselineData; + + mergedData.targets = mergedData.targets.concat(followupData.targets); + mergedData.nonTargets = mergedData.nonTargets.concat(followupData.nonTargets); + + const resultBaseline = this.validateTimepoint('baseline', trialCriteriaType, baselineData); + const resultFollowup = this.validateTimepoint('followup', trialCriteriaType, followupData); + const resultBoth = this.validateTimepoint('both', trialCriteriaType, mergedData); + const results = resultBaseline.concat(resultFollowup).concat(resultBoth); + + console.warn('>>>> validate', results); + + return results; } - validateRecist(data) { - const recistBaselineEvaluator = new CriteriaEvaluator(recistBaselineEvaluation); - console.warn('>>>>check', recistBaselineEvaluator.evaluate(data)); + validateTimepoint(timepointId, trialCriteriaType, data) { + const evaluators = this.getEvaluators(timepointId, trialCriteriaType); + let results = []; + + evaluators.forEach(evaluator => { + const result = evaluator.evaluate(data); + results = results.concat(result); + }); + + return results; + } + + getEvaluators(timepointId, trialCriteriaType) { + const evaluators = []; + const trialCriteriaTypeId = trialCriteriaType.id.toLowerCase(); + const evaluation = evaluations[trialCriteriaTypeId] + + if(evaluation) { + const evaluationTimepoint = evaluation[timepointId]; + + if(evaluationTimepoint) { + evaluators.push(new CriteriaEvaluator(evaluationTimepoint)); + } + } + + return evaluators; } /* * Build the data that will be used to do the conformance criteria checks */ - getData() { + getData(timepointType) { const data = { targets: [], nonTargets: [] @@ -33,12 +74,17 @@ class ConformanceCriteria { const fillData = measurementType => { const measurements = this.measurementApi.fetch(measurementType); + measurements.forEach(measurement => { const { studyInstanceUid, imageId } = measurement; const metadata = this.getImageMetadata(studyInstanceUid, imageId); const timepointId = measurement.timepointId; const timepoint = this.timepointApi.timepoints.findOne({ timepointId }); + if((timepointType !== 'both') && (timepoint.timepointType !== timepointType)) { + return; + } + data[measurementType].push({ measurement, metadata, diff --git a/Packages/ohif-measurements/client/conformance/CriteriaEvaluator.js b/Packages/ohif-measurements/client/conformance/CriteriaEvaluator.js index e2a21eba1..6c921a31f 100644 --- a/Packages/ohif-measurements/client/conformance/CriteriaEvaluator.js +++ b/Packages/ohif-measurements/client/conformance/CriteriaEvaluator.js @@ -25,10 +25,6 @@ export class CriteriaEvaluator { } getCriteriaValidator() { - if (CriteriaEvaluator.validator) { - return CriteriaEvaluator.validator; - } - const schema = { properties: {}, definitions: {} @@ -54,7 +50,7 @@ export class CriteriaEvaluator { } }); - return CriteriaEvaluator.validator = new Ajv().compile(schema); + return new Ajv().compile(schema); } evaluate(data) { diff --git a/Packages/ohif-measurements/client/conformance/criteria/Location.js b/Packages/ohif-measurements/client/conformance/criteria/Location.js new file mode 100644 index 000000000..469392cf0 --- /dev/null +++ b/Packages/ohif-measurements/client/conformance/criteria/Location.js @@ -0,0 +1,36 @@ +import { BaseCriterion } from './BaseCriterion'; + +export const LocationSchema = { + type: 'object' +}; + +/* LocationCriterion + * Check if the there are non-target measurements with response different than "present" on baseline + */ +export class LocationCriterion extends BaseCriterion { + + constructor(options) { + super(options); + } + + evaluate(data) { + const items = data.targets.concat(data.nonTargets); + const measurements = []; + let message; + + items.forEach(item => { + const measurement = item.measurement; + + if (!measurement.location) { + measurements.push(measurement); + } + }); + + if (measurements.length) { + message = 'All measurements should have a location'; + } + + return this.generateResponse(message, measurements); + } + +} diff --git a/Packages/ohif-measurements/client/conformance/criteria/index.js b/Packages/ohif-measurements/client/conformance/criteria/index.js index b95fca63b..5501e2c7a 100644 --- a/Packages/ohif-measurements/client/conformance/criteria/index.js +++ b/Packages/ohif-measurements/client/conformance/criteria/index.js @@ -1,3 +1,4 @@ +export * from './Location'; export * from './MaxTargetsPerOrgan'; export * from './MaxTargets'; export * from './MeasurementsLength'; diff --git a/Packages/ohif-measurements/client/conformance/evaluations/index.js b/Packages/ohif-measurements/client/conformance/evaluations/index.js new file mode 100644 index 000000000..e138e28e0 --- /dev/null +++ b/Packages/ohif-measurements/client/conformance/evaluations/index.js @@ -0,0 +1,3 @@ +import * as recistEvaluation from './recist.json'; + +export const recist = recistEvaluation; \ No newline at end of file diff --git a/Packages/ohif-measurements/client/conformance/evaluations/recist.json b/Packages/ohif-measurements/client/conformance/evaluations/recist.json new file mode 100644 index 000000000..e32a6ad9d --- /dev/null +++ b/Packages/ohif-measurements/client/conformance/evaluations/recist.json @@ -0,0 +1,35 @@ +{ + "both": { + "Location": {}, + "MaxTargetsPerOrgan": { + "limit": 2 + }, + "MaxTargets": { + "limit": 5 + }, + "MeasurementsLength": [{ + "longAxis": 10, + "longAxisSliceThicknessMultiplier": 2, + "modalityIn": ["CT", "MR"], + "locationNotIn": ["Lymph Node"], + "message": "Extranodal lesions must be >= 10mm long axis AND >= double the acquisition slice thickness by CT and MR" + }, { + "shortAxis": 20, + "longAxis": 20, + "modalityIn": ["PX", "XA"], + "locationNotIn": ["Lymph Node"], + "message": "Extranodal lesions must be >= 20mm on chest x-ray (although x-rays rarely used for clinical trial assessment)" + }, { + "shortAxis": 15, + "shortAxisSliceThicknessMultiplier": 2, + "modalityIn": ["CT", "MR"], + "locationIn": ["Lymph Node"], + "message": "Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR" + }] + }, + "baseline": { + "NonTargetResponse": {}, + "TargetType": {} + }, + "followup": {} +} \ No newline at end of file diff --git a/Packages/ohif-measurements/client/conformance/evaluations/recistBaseline.json b/Packages/ohif-measurements/client/conformance/evaluations/recistBaseline.json deleted file mode 100644 index 5cc78e1f7..000000000 --- a/Packages/ohif-measurements/client/conformance/evaluations/recistBaseline.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "MaxTargetsPerOrgan": { - "limit": 2 - }, - "MaxTargets": { - "limit": 5 - }, - "MeasurementsLength": [{ - "longAxis": 10, - "longAxisSliceThicknessMultiplier": 2, - "modalityIn": ["CT", "MR"], - "locationNotIn": ["Lymph Node"], - "message": "Extranodal lesions must be >= 10mm long axis AND >= double the acquisition slice thickness by CT and MR" - }, { - "shortAxis": 20, - "longAxis": 20, - "modalityIn": ["PX", "XA"], - "locationNotIn": ["Lymph Node"], - "message": "Extranodal lesions must be >= 20mm on chest x-ray (although x-rays rarely used for clinical trial assessment)" - }, { - "shortAxis": 15, - "shortAxisSliceThicknessMultiplier": 2, - "modalityIn": ["CT", "MR"], - "locationIn": ["Lymph Node"], - "message": "Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR" - }], - "NonTargetResponse": {}, - "TargetType": {} -}