LT-275: Migrating ohif:lesiontracker to the new import structure
This commit is contained in:
1 parent
a49fca71d2
commit
826bf9bf16
26 files changed
+142
-151
No files matched your search
@@ -0,0 +1,2 @@
|
||||
import './LesionLocations.js';
|
||||
import './LocationResponses.js';
|
||||
@@ -0,0 +1,5 @@
|
||||
import './bidirectionalTool.js';
|
||||
import './nonTargetTool.js';
|
||||
import './scaleOverlayTool.js';
|
||||
import './deleteLesionKeyboardTool.js';
|
||||
import './qualitativeTargetTools.js';
|
||||
@@ -0,0 +1,22 @@
|
||||
import './optionsModal/optionsModal.html';
|
||||
import './optionsModal/optionsModal.styl';
|
||||
import './optionsModal/optionsModal.js';
|
||||
|
||||
import './optionsModal/recistDescription/recistDescription.html';
|
||||
import './optionsModal/irRCDescription/irRCDescription.html';
|
||||
|
||||
import './measurementLocationDialog/measurementLocationDialog.html';
|
||||
import './measurementLocationDialog/measurementLocationDialog.js';
|
||||
import './measurementLocationDialog/measurementLocationDialog.styl';
|
||||
|
||||
import './nonTargetLesionDialog/nonTargetLesionDialog.html';
|
||||
import './nonTargetLesionDialog/nonTargetLesionDialog.styl';
|
||||
import './nonTargetLesionDialog/nonTargetLesionDialog.js';
|
||||
|
||||
import './nonTargetResponseDialog/nonTargetResponseDialog.html';
|
||||
import './nonTargetResponseDialog/nonTargetResponseDialog.styl';
|
||||
import './nonTargetResponseDialog/nonTargetResponseDialog.js';
|
||||
|
||||
import './measureFlow/measureFlow.html';
|
||||
import './measureFlow/measureFlow.styl';
|
||||
import './measureFlow/measureFlow.js';
|
||||
+1
-1
@@ -268,7 +268,7 @@ Template.measurementLocationDialog.events({
|
||||
|
||||
const instance = Template.instance();
|
||||
const measurementApi = instance.data.measurementApi;
|
||||
convertToNonTarget(measurementApi, measurementData);
|
||||
OHIF.lesiontracker.convertToNonTarget(measurementApi, measurementData);
|
||||
|
||||
closeHandler(dialog);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Client-side collections
|
||||
import './collections';
|
||||
|
||||
// Additional Custom Cornerstone Tools for Lesion Tracker
|
||||
import './compatibility';
|
||||
|
||||
import './tools.js';
|
||||
|
||||
// UI Components
|
||||
import './components';
|
||||
|
||||
import './lib';
|
||||
@@ -0,0 +1,375 @@
|
||||
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 = TrialCriteriaTypes.find({
|
||||
selected: true
|
||||
}).map(function(criteria) {
|
||||
return criteria.id;
|
||||
});
|
||||
var currentConstraints = OHIF.lesiontracker.getTrialCriteriaConstraints(criteriaTypes, measurementData.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 = 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 = 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.
|
||||
var currentConstraints = OHIF.lesiontracker.getTrialCriteriaConstraints(criteriaTypes, currentMeasurement.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
|
||||
};
|
||||
@@ -0,0 +1,397 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
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) {
|
||||
var acquisitionSliceThickness;
|
||||
|
||||
var 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
|
||||
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 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 = Template.instance().timepointApi;
|
||||
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];
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
const removeToolDataWithMeasurementId = (imageId, toolType, measurementId) => {
|
||||
OHIF.log.info('removeToolDataWithMeasurementId');
|
||||
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.toolState;
|
||||
|
||||
// Find any related toolData
|
||||
if (!toolState[imageId] || !toolState[imageId][toolType]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolData = toolState[imageId][toolType].data;
|
||||
if (!toolData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search toolData for entries linked to the specified Measurement
|
||||
const toRemove = [];
|
||||
toolData.forEach(function(measurement, index) {
|
||||
if (measurement.id === measurementId ||
|
||||
measurement._id === measurementId) {
|
||||
toRemove.push(index);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
OHIF.log.info('Removing Indices: ');
|
||||
OHIF.log.info(toRemove);
|
||||
|
||||
// If any toolData entries need to be removed, splice them from
|
||||
// the toolData array
|
||||
toRemove.forEach(function(index) {
|
||||
toolData.splice(index, 1);
|
||||
});
|
||||
};
|
||||
|
||||
OHIF.lesiontracker.clearMeasurementTimepointData = (measurementId, timepointId) => {
|
||||
const data = Measurements.findOne(measurementId);
|
||||
|
||||
// Clear the Measurement data for this timepoint
|
||||
const imageId = data.timepoints[timepointId].imageId;
|
||||
const toolType = data.toolType;
|
||||
removeToolDataWithMeasurementId(imageId, toolType, measurementId);
|
||||
|
||||
// Update any viewports that are currently displaying this imageId
|
||||
const enabledElements = cornerstone.getEnabledElementsByImageId(imageId);
|
||||
enabledElements.forEach(enabledElement => cornerstone.updateImage(enabledElement.element));
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
var responseTypes = {
|
||||
crTool: 'CR',
|
||||
exTool: 'EX',
|
||||
unTool: 'UN'
|
||||
};
|
||||
|
||||
convertNonTarget = function(measurementApi, measurementData, newTooltype) {
|
||||
if (measurementData.toolType !== 'nonTarget') {
|
||||
return;
|
||||
}
|
||||
|
||||
var measurement = Measurements.findOne(measurementData.id);
|
||||
|
||||
var timepoint = Timepoints.findOne({
|
||||
timepointId: measurementData.timepointId
|
||||
});
|
||||
|
||||
if (timepoint && timepoint.timepointType !== 'followup') {
|
||||
OHIF.log.warn('Not a followup');
|
||||
return;
|
||||
}
|
||||
|
||||
// Next, update the measurementData and add it to the new tool type
|
||||
var newMeasurement = {
|
||||
id: 'notready',
|
||||
lesionNumber: measurementApi.getNewLesionNumber(measurementData.timepointId, false),
|
||||
visible: true,
|
||||
active: true,
|
||||
imageId: measurementData.imageId,
|
||||
seriesInstanceUid: measurementData.seriesInstanceUid,
|
||||
studyInstanceUid: measurementData.studyInstanceUid,
|
||||
patientId: measurementData.patientId,
|
||||
isTarget: false,
|
||||
toolType: newTooltype
|
||||
};
|
||||
|
||||
newMeasurement.handles = measurementData.handles;
|
||||
|
||||
if (responseTypes[newTooltype]) {
|
||||
newMeasurement.response = responseTypes[newTooltype];
|
||||
}
|
||||
|
||||
// Adds lesion data to timepoints array
|
||||
measurementApi.updateLesionData(newMeasurement);
|
||||
|
||||
// Set the new Measurement to have the same location as the old one
|
||||
if (measurement.location && measurement.locationId) {
|
||||
var existingMeasurement = Measurements.findOne({
|
||||
lesionNumber: newMeasurement.lesionNumber,
|
||||
isTarget: newMeasurement.isTarget
|
||||
});
|
||||
|
||||
if (!existingMeasurement) {
|
||||
return;
|
||||
}
|
||||
|
||||
Measurements.update(existingMeasurement._id, {
|
||||
$set: {
|
||||
location: measurement.location,
|
||||
locationId: measurement.locationId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (measurement) {
|
||||
// Remove the timepointData from this Measurement and update it
|
||||
// in the database, if it is already in the database
|
||||
OHIF.lesiontracker.clearMeasurementTimepointData(measurement._id, measurementData.timepointId);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
OHIF.lesiontracker.convertToNonTarget = (measurementApi, measurementData) => {
|
||||
if (measurementData.isTarget === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const measurement = Measurements.findOne(measurementData.id);
|
||||
|
||||
const timepoint = Timepoints.findOne({
|
||||
timepointId: measurementData.timepointId
|
||||
});
|
||||
|
||||
// Next, update the measurementData and add it to the new tool type
|
||||
const toolType = 'nonTarget';
|
||||
const newMeasurement = {
|
||||
id: 'notready',
|
||||
lesionNumber: LesionManager.getNewLesionNumber(measurementData.timepointId, false),
|
||||
visible: true,
|
||||
active: true,
|
||||
imageId: measurementData.imageId,
|
||||
seriesInstanceUid: measurementData.seriesInstanceUid,
|
||||
studyInstanceUid: measurementData.studyInstanceUid,
|
||||
patientId: measurementData.patientId,
|
||||
isTarget: false,
|
||||
toolType: toolType
|
||||
};
|
||||
|
||||
if (timepoint && timepoint.timepointType === 'baseline') {
|
||||
newMeasurement.response = 'Present';
|
||||
} else {
|
||||
newMeasurement.response = '';
|
||||
}
|
||||
|
||||
newMeasurement.handles = {
|
||||
start: {
|
||||
x: (measurementData.handles.start.x + measurementData.handles.end.x) / 2,
|
||||
y: (measurementData.handles.start.y + measurementData.handles.end.y) / 2,
|
||||
highlight: true,
|
||||
active: false
|
||||
},
|
||||
end: {
|
||||
x: Math.min(measurementData.handles.start.x, measurementData.handles.end.x),
|
||||
y: Math.min(measurementData.handles.start.y, measurementData.handles.end.y),
|
||||
highlight: true,
|
||||
active: false
|
||||
},
|
||||
textBox: {
|
||||
x: measurementData.handles.textBox.x,
|
||||
y: measurementData.handles.textBox.y,
|
||||
active: false,
|
||||
movesIndependently: false,
|
||||
drawnIndependently: true,
|
||||
allowedOutsideImage: true,
|
||||
hasBoundingBox: true
|
||||
}
|
||||
};
|
||||
|
||||
// Adds lesion data to timepoints array
|
||||
LesionManager.updateLesionData(newMeasurement);
|
||||
|
||||
// Set the new Measurement to have the same location as the old one
|
||||
if (measurement.location && measurement.locationId) {
|
||||
var existingMeasurement = Measurements.findOne({
|
||||
lesionNumber: newMeasurement.lesionNumber,
|
||||
isTarget: newMeasurement.isTarget
|
||||
});
|
||||
|
||||
if (!existingMeasurement) {
|
||||
return;
|
||||
}
|
||||
|
||||
Measurements.update(existingMeasurement._id, {
|
||||
$set: {
|
||||
location: measurement.location,
|
||||
locationId: measurement.locationId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (measurement) {
|
||||
// Remove the timepointData from this Measurement and update it
|
||||
// in the database, if it is already in the database
|
||||
OHIF.lesiontracker.clearMeasurementTimepointData(measurement._id, measurementData.timepointId);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// StudyList-related functions
|
||||
import './studylist/openNewTabWithTimepoint.js';
|
||||
import './studylist/studylistModification.js';
|
||||
|
||||
// Library functions
|
||||
import './TrialCriteriaConstraints.js';
|
||||
import './MeasurementValidation.js';
|
||||
import './pixelSpacingAutorunCheck.js';
|
||||
import './toggleLesionTrackerTools.js';
|
||||
import './clearMeasurementTimepointData.js';
|
||||
import './convertToNonTarget.js';
|
||||
import './convertNonTarget.js';
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Session } from 'meteor/session';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
OHIF.lesiontracker.pixelSpacingAutorunCheck = () => {
|
||||
OHIF.log.info('lesionTool button change autorun');
|
||||
|
||||
// Get oncology tools
|
||||
const $oncologyTools = $('button#lesion, button#nonTarget');
|
||||
|
||||
// TODO: Set activeViewport for empty viewport element
|
||||
const activeViewportIndex = Session.get('activeViewport');
|
||||
if (activeViewportIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = $('.imageViewerViewport').get(activeViewportIndex);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
let enabledElement;
|
||||
try {
|
||||
enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check value of rowPixelSpacing & columnPixelSpacing to define as unavailable
|
||||
if (!enabledElement ||
|
||||
!enabledElement.image ||
|
||||
!enabledElement.image.rowPixelSpacing ||
|
||||
!enabledElement.image.columnPixelSpacing) {
|
||||
// Disable Lesion Buttons
|
||||
$oncologyTools.prop('disabled', true);
|
||||
} else {
|
||||
// Enable Lesion Buttons
|
||||
$oncologyTools.prop('disabled', false);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* Opens a new tab in the tabbed studylist environment using
|
||||
* a given timepoint and new tab title.
|
||||
*
|
||||
* @param timepointId The UID of the Timepoint to be opened
|
||||
* @param title The title to be used for the tab heading
|
||||
*/
|
||||
OHIF.lesiontracker.openNewTabWithTimepoint = (timepointId, title) => {
|
||||
const contentId = 'viewerTab';
|
||||
|
||||
const Timepoints = StudyList.timepointApi.timepoints;
|
||||
const timepoint = Timepoints.findOne({
|
||||
timepointId: timepointId
|
||||
});
|
||||
|
||||
if (!timepoint) {
|
||||
throw 'No such timepoint exists';
|
||||
}
|
||||
|
||||
// Get the relevant studyInstanceUids given the timepoints
|
||||
const data = getDataFromTimepoint(timepoint);
|
||||
if (!data.studyInstanceUids) {
|
||||
throw 'No studies found that are related to this timepoint';
|
||||
}
|
||||
|
||||
ViewerData = window.ViewerData || ViewerData;
|
||||
|
||||
// Update the ViewerData global object
|
||||
ViewerData[contentId] = {
|
||||
title: title,
|
||||
contentid: contentId,
|
||||
studyInstanceUids: data.studyInstanceUids,
|
||||
timepointIds: data.timepointIds,
|
||||
currentTimepointId: timepointId
|
||||
};
|
||||
|
||||
// Switch to the new tab
|
||||
switchToTab(contentId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves related studies given a Baseline or Follow-up Timepoint
|
||||
*
|
||||
* @param timepoint
|
||||
* @returns {Array}
|
||||
*/
|
||||
function getDataFromTimepoint(timepoint) {
|
||||
let relatedStudies = [timepoint.studyInstanceUids[0]];
|
||||
|
||||
// If this is the baseline, we should stop here and return the relevant studies
|
||||
if (isBaseline(timepoint)) {
|
||||
return {
|
||||
studyInstanceUids: relatedStudies,
|
||||
timepointIds: [timepoint.timepointId]
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, this is a follow-up exam, so we should also find the baseline timepoint,
|
||||
// and all studies related to it. We also enforce that the Baseline should have a studyDate
|
||||
// prior to the latest studyDate in the current (Follow-up) Timepoint.
|
||||
const Timepoints = StudyList.timepointApi.timepoints;
|
||||
const baseline = Timepoints.findOne({
|
||||
timepointType: 'baseline',
|
||||
patientId: timepoint.patientId,
|
||||
latestDate: {
|
||||
$lte: timepoint.latestDate
|
||||
}
|
||||
});
|
||||
|
||||
let timepointIds = [];
|
||||
if (baseline) {
|
||||
// NOTE: Temporarily added [0], since we only need one study per timepoint to load immediately?
|
||||
relatedStudies = relatedStudies.concat(baseline.studyInstanceUids[0]);
|
||||
timepointIds.push(baseline.timepointId);
|
||||
} else {
|
||||
OHIF.log.warn('No Baseline found while opening a Follow-up Timepoint');
|
||||
}
|
||||
|
||||
timepointIds.push(timepoint.timepointId);
|
||||
|
||||
return {
|
||||
studyInstanceUids: relatedStudies,
|
||||
timepointIds: timepointIds
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a Timepoints is a baseline or not
|
||||
* (abstracting this for later use, since I expect it to get more complex)
|
||||
*
|
||||
* @param timepoint a document from the Timepoints Collection
|
||||
* @returns {boolean} Whether or not the timepoint is stored as a Baseline
|
||||
*/
|
||||
function isBaseline(timepoint) {
|
||||
return (timepoint.timepointType === 'baseline');
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Meteor.startup(function() {
|
||||
StudyList.callbacks['dblClickOnStudy'] = dblClickOnStudy;
|
||||
StudyList.callbacks['middleClickOnStudy'] = dblClickOnStudy;
|
||||
|
||||
StudyList.timepointApi = new OHIF.measurements.TimepointApi();
|
||||
StudyList.timepointApi.retrieveTimepoints();
|
||||
});
|
||||
|
||||
/**
|
||||
* Lesion Tracker method including Timepoints / other studies
|
||||
*/
|
||||
function dblClickOnStudy(data) {
|
||||
// Use the formatPN template helper to clean up the patient name
|
||||
let title = formatPN(data.patientName);
|
||||
|
||||
const instance = Template.instance();
|
||||
|
||||
// Find the relevant timepoint given the clicked-on study
|
||||
const timepointApi = StudyList.timepointApi;
|
||||
if (!timepointApi) {
|
||||
console.log('No timepoint api on dbl-clicked study?');
|
||||
return;
|
||||
}
|
||||
|
||||
const timepoint = timepointApi.study(data.studyInstanceUid)[0];
|
||||
if (timepoint) {
|
||||
// Add the Timepoint name to the Patient name to create the tab title
|
||||
title += ' ' + timepointApi.name(timepoint);
|
||||
OHIF.lesiontracker.openNewTabWithTimepoint(timepoint.timepointId, title);
|
||||
} else {
|
||||
open(data.studyInstanceUid, title);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a study
|
||||
*
|
||||
* @param studyInstanceUid The UID of the Study to be opened
|
||||
* @param title The title to be used for the tab heading
|
||||
*/
|
||||
function open(studyInstanceUid, title) {
|
||||
const contentid = 'viewerTab';
|
||||
|
||||
ViewerData = window.ViewerData || ViewerData;
|
||||
|
||||
// Update the ViewerData global object
|
||||
ViewerData[contentid] = {
|
||||
title: title,
|
||||
contentid: contentid,
|
||||
studyInstanceUids: [studyInstanceUid]
|
||||
};
|
||||
|
||||
// Switch to the new tab
|
||||
switchToTab(contentid);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* Show / hide lesion tracker tools
|
||||
*/
|
||||
|
||||
let previousStates;
|
||||
let previousActiveTool;
|
||||
|
||||
let toolsShown = true;
|
||||
|
||||
OHIF.lesiontracker.toggleLesionTrackerTools = () => {
|
||||
if (toolsShown === true) {
|
||||
// Save the current settings for later
|
||||
previousStates = toolManager.getToolDefaultStates();
|
||||
previousActiveTool = toolManager.getActiveTool();
|
||||
|
||||
// Hide the tools (set them all to disabled)
|
||||
const toolDefaultStates = {
|
||||
activate: [ 'deleteLesionKeyboardTool' ],
|
||||
deactivate: [],
|
||||
enable: [],
|
||||
disable: [ 'bidirectional', 'nonTarget', 'length', 'crTool', 'unTool', 'exTool' ]
|
||||
};
|
||||
|
||||
toolManager.setToolDefaultStates(toolDefaultStates);
|
||||
|
||||
// Using setActiveTool with no arguments activates the
|
||||
// default tool on all available viewports
|
||||
toolManager.setActiveTool();
|
||||
|
||||
toolsShown = false;
|
||||
} else {
|
||||
// Show the tools (reload previous states)
|
||||
toolManager.setToolDefaultStates(previousStates);
|
||||
|
||||
// Using setActiveTool with no elements specified activates
|
||||
// the specified tool on all available viewports
|
||||
toolManager.setActiveTool(previousActiveTool);
|
||||
|
||||
toolsShown = true;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user