First pass at a Hanging Protocol implementation and editor (LT-131)
This commit is contained in:
1 parent
dfcb33153f
commit
2e52d9e3c1
157 files changed
+8869
-23585
No files matched your search
@@ -0,0 +1,685 @@
|
||||
// Define a global variable that will be used to refer to the Protocol Engine
|
||||
// It must be populated by HP.setEngine when the Viewer is initialized and a ProtocolEngine
|
||||
// is instantiated on top of the LayoutManager. If the global ProtocolEngine variable remains
|
||||
// undefined, none of the HangingProtocol functions will operate.
|
||||
ProtocolEngine = undefined;
|
||||
|
||||
/**
|
||||
* Sets the ProtocolEngine global given an instantiated ProtocolEngine. This is done so that
|
||||
* The functions in the package can depend on a ProtocolEngine variable, but this variable does
|
||||
* not have to be exported from the application level.
|
||||
*
|
||||
* (There may be a better way to do this, but for now this works with no real downside)
|
||||
*
|
||||
* @param protocolEngine An instantiated ProtocolEngine linked to a LayoutManager from the
|
||||
* Viewerbase package
|
||||
*/
|
||||
HP.setEngine = function(protocolEngine) {
|
||||
ProtocolEngine = protocolEngine;
|
||||
};
|
||||
|
||||
// Define an empty object to store callbacks that are used to retrieve custom attributes
|
||||
// The simplest example for a custom attribute is the Timepoint type (i.e. baseline or follow-up)
|
||||
// used in the LesionTracker application.
|
||||
//
|
||||
// Timepoint type can be obtained given a studyId, and this is done through a custom callback.
|
||||
// Developers can define attributes (i.e. attributeId = timepointType) with a name ('Timepoint Type')
|
||||
// and a callback function that is used to calculate them.
|
||||
//
|
||||
// The input to the callback, which is called during viewport-image matching rule evaluation
|
||||
// is the set of attributes that contains the specified attribute. In our example, timepointType is
|
||||
// linked to the study attributes, and so the inputs to the callback is an object containing
|
||||
// the study attributes.
|
||||
HP.CustomAttributeRetrievalCallbacks = {};
|
||||
|
||||
/**
|
||||
* Adds a custom attribute to be used in the HangingProtocol UI and matching rules, including a
|
||||
* callback that will be used to calculate the attribute value.
|
||||
*
|
||||
* @param attributeId The ID used to refer to the attribute (e.g. 'timepointType')
|
||||
* @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type')
|
||||
* @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image)
|
||||
*/
|
||||
HP.addCustomAttribute = function(attributeId, attributeName, callback) {
|
||||
HP.CustomAttributeRetrievalCallbacks[attributeId] = {
|
||||
name: attributeName,
|
||||
callback: callback
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// Define an empty object to store callbacks that are used to apply custom viewport settings
|
||||
// after a viewport is rendered.
|
||||
HP.CustomViewportSettings = {};
|
||||
|
||||
/**
|
||||
* Adds a custom setting that can be chosen in the HangingProtocol UI and applied to a Viewport
|
||||
*
|
||||
* @param settingId The ID used to refer to the setting (e.g. 'displayCADMarkers')
|
||||
* @param settingName The name of the setting to be displayed (e.g. 'Display CAD Markers')
|
||||
* @param options
|
||||
* @param callback A function to be run after a viewport is rendered with a series
|
||||
*/
|
||||
HP.addCustomViewportSetting = function(settingId, settingName, options, callback) {
|
||||
HP.CustomViewportSettings[settingId] = {
|
||||
id: settingId,
|
||||
text: settingName,
|
||||
options: options,
|
||||
callback: callback
|
||||
};
|
||||
};
|
||||
|
||||
Meteor.startup(function() {
|
||||
HP.addCustomViewportSetting('wlPreset', 'Window/Level Preset', Object.keys(OHIF.viewer.wlPresets), function(element, optionValue) {
|
||||
if (OHIF.viewer.wlPresets.hasOwnProperty(optionValue)) {
|
||||
applyWLPreset(optionValue, element);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Log decisions regarding matching
|
||||
HP.match = function(attributes, rules) {
|
||||
var options = {
|
||||
format: 'grouped'
|
||||
};
|
||||
|
||||
var score = 0;
|
||||
var details = {
|
||||
passed: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
var requiredFailed = false;
|
||||
|
||||
rules.forEach(rule => {
|
||||
var attribute = rule.attribute;
|
||||
|
||||
// If the attributes we are testing (e.g. study, series, or instance attributes) do
|
||||
// not contain the attribute specified in the rule, check whether or not they have been
|
||||
// defined in the CustomAttributeRetrievalCallbacks Object.
|
||||
|
||||
// TODO: Investigate why attributes.hasOwnProperty(attribute) doesn't work?
|
||||
if (attributes[attribute] === undefined &&
|
||||
HP.CustomAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
||||
var customAttribute = HP.CustomAttributeRetrievalCallbacks[attribute];
|
||||
attributes[attribute] = customAttribute.callback(attributes);
|
||||
}
|
||||
|
||||
// Format the constraint as required by Validate.js
|
||||
var testConstraint = {};
|
||||
testConstraint[attribute] = rule.constraint;
|
||||
|
||||
// Use Validate.js to evaluate the constraints on the specified attributes
|
||||
var errorMessages = validate(attributes, testConstraint, [options]);
|
||||
|
||||
if (!errorMessages) {
|
||||
// If no errorMessages were returned, then validation passed.
|
||||
|
||||
// Add the rule's weight to the total score
|
||||
score += rule.weight;
|
||||
|
||||
// Log that this rule passed in the matching details object
|
||||
details.passed.push({
|
||||
rule: rule
|
||||
});
|
||||
} else {
|
||||
// If errorMessages were present, then validation failed
|
||||
|
||||
// If the rule that failed validation was Required, then
|
||||
// mark that a required Rule has failed
|
||||
if (rule.required) {
|
||||
requiredFailed = true;
|
||||
}
|
||||
|
||||
// Log that this rule failed in the matching details object
|
||||
// and include any error messages
|
||||
details.failed.push({
|
||||
rule: rule,
|
||||
errorMessages: errorMessages
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If a required Rule has failed Validation, set the matching score to zero
|
||||
if (requiredFailed) {
|
||||
score = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
score: score,
|
||||
details: details
|
||||
};
|
||||
};
|
||||
|
||||
var sortByScore = function(arr) {
|
||||
arr.sort(function(a, b) {
|
||||
return b.score - a.score;
|
||||
});
|
||||
};
|
||||
|
||||
HP.ProtocolEngine = class ProtocolEngine {
|
||||
constructor(LayoutManager, studies) {
|
||||
this.LayoutManager = LayoutManager;
|
||||
this.studies = studies;
|
||||
|
||||
this.reset();
|
||||
|
||||
// Create an array for new stage ids to be stored
|
||||
// while editing a stage
|
||||
this.newStageIds = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the ProtocolEngine to the best match
|
||||
*/
|
||||
reset() {
|
||||
var protocol = this.getBestMatch();
|
||||
this.setHangingProtocol(protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current Stage from the current Protocol and stage index
|
||||
*
|
||||
* @returns {*} The Stage model for the currently displayed Stage
|
||||
*/
|
||||
getCurrentStageModel() {
|
||||
return this.protocol.stages[this.stage];
|
||||
}
|
||||
|
||||
findMatchByStudy(study) {
|
||||
var matched = [];
|
||||
|
||||
HangingProtocols.find().forEach(protocol => {
|
||||
// Clone the protocol's protocolMatchingRules array
|
||||
// We clone it so that we don't accidentally add the
|
||||
// numberOfPriorsReferenced rule to the Protocol itself.
|
||||
var rules = protocol.protocolMatchingRules.slice(0);
|
||||
if (!rules) {
|
||||
return;
|
||||
}
|
||||
|
||||
study.numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study);
|
||||
var rule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', {
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: protocol.numberOfPriorsReferenced
|
||||
}
|
||||
});
|
||||
|
||||
rules.push(rule);
|
||||
|
||||
var matchedDetails = HP.match(study, rules);
|
||||
|
||||
if (matchedDetails.score > 0) {
|
||||
matched.push({
|
||||
score: matchedDetails.score,
|
||||
protocol: protocol
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!matched.length) {
|
||||
var defaultProtocol = HangingProtocols.findOne({
|
||||
id: 'defaultProtocol'
|
||||
});
|
||||
|
||||
return [{
|
||||
score: 1,
|
||||
protocol: defaultProtocol
|
||||
}];
|
||||
}
|
||||
|
||||
sortByScore(matched);
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the MatchedProtocols Collection by running the matching procedure
|
||||
*/
|
||||
updateMatches() {
|
||||
// Clear all data from the MatchedProtocols Collection
|
||||
MatchedProtocols.remove({});
|
||||
|
||||
// For each study, find the matching protocols
|
||||
this.studies.forEach(study => {
|
||||
var matched = this.findMatchByStudy(study);
|
||||
|
||||
// For each matched protocol, check if it is already in MatchedProtocols
|
||||
matched.forEach(function(matchedDetail) {
|
||||
var protocol = matchedDetail.protocol;
|
||||
var protocolInCollection = MatchedProtocols.findOne({
|
||||
id: protocol.id
|
||||
});
|
||||
|
||||
// If it is not already in the MatchedProtocols Collection, insert it
|
||||
if (!protocolInCollection) {
|
||||
MatchedProtocols.insert(protocol);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the best matched Protocol to the current study or set of studies
|
||||
* @returns {*}
|
||||
*/
|
||||
getBestMatch() {
|
||||
// Run the matching to populate the MatchedProtocols Collection
|
||||
this.updateMatches();
|
||||
|
||||
// Retrieve the highest scoring Protocol
|
||||
var sorted = MatchedProtocols.find({}, {
|
||||
sort: {
|
||||
score: -1
|
||||
},
|
||||
limit: 1
|
||||
}).fetch();
|
||||
|
||||
// Return the highest scoring Protocol
|
||||
return sorted[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of previous studies in the cached Worklist that
|
||||
* have the same patientId and an earlier study date
|
||||
*
|
||||
* @param study The input study
|
||||
* @returns {any|*} The number of available prior studies with the same patientId
|
||||
*/
|
||||
getNumberOfAvailablePriors(study) {
|
||||
var studies = WorklistStudies.find({
|
||||
patientId: study.patientId,
|
||||
studyDate: {
|
||||
$lt: study.studyDate
|
||||
}
|
||||
});
|
||||
|
||||
return studies.count();
|
||||
}
|
||||
|
||||
findRelatedStudies(protocol, study) {
|
||||
if (!protocol.protocolMatchingRules) {
|
||||
return;
|
||||
}
|
||||
|
||||
var studies = WorklistStudies.find({
|
||||
patientId: study.patientId,
|
||||
studyDate: {
|
||||
$lt: study.studyDate
|
||||
}
|
||||
}, {
|
||||
sort: {
|
||||
studyDate: -1
|
||||
}
|
||||
});
|
||||
|
||||
var related = [];
|
||||
var currentDate = moment(study.studyDate, 'YYYYMMDD');
|
||||
|
||||
studies.forEach(function(priorStudy, priorIndex) {
|
||||
// Calculate an abstract prior value for the study in question
|
||||
if (priorIndex === (studies.length - 1)) {
|
||||
priorStudy.abstractPriorValue = -1;
|
||||
} else {
|
||||
// Abstract prior index starts from 1 in the DICOM standard
|
||||
priorStudy.abstractPriorValue = priorIndex + 1;
|
||||
}
|
||||
|
||||
// Calculate the relative time using Moment.js
|
||||
var priorDate = moment(priorStudy.studyDate, 'YYYYMMDD');
|
||||
priorStudy.relativeTime = currentDate.diff(priorDate);
|
||||
|
||||
var details = HP.match(priorStudy, protocol.protocolMatchingRules);
|
||||
if (details.score) {
|
||||
related.push({
|
||||
score: details.score,
|
||||
study: priorStudy
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
sortByScore(related);
|
||||
return related.map(function(v) {
|
||||
return v.study;
|
||||
});
|
||||
}
|
||||
|
||||
// Match images given a list of Studies and a Viewport's image matching reqs
|
||||
matchImages(viewport) {
|
||||
var studyMatchingRules = viewport.studyMatchingRules;
|
||||
var seriesMatchingRules = viewport.seriesMatchingRules;
|
||||
var instanceMatchingRules = viewport.imageMatchingRules;
|
||||
|
||||
var highestStudyMatchingScore = 0;
|
||||
var highestSeriesMatchingScore = 0;
|
||||
var highestImageMatchingScore = 0;
|
||||
var matchingScores = [];
|
||||
var bestMatch;
|
||||
|
||||
var currentStudy = this.studies[0];
|
||||
currentStudy.abstractPriorValue = 0;
|
||||
|
||||
studyMatchingRules.forEach(rule => {
|
||||
if (rule.attribute === 'abstractPriorValue') {
|
||||
var validatorType = Object.keys(rule.constraint)[0];
|
||||
var validator = Object.keys(rule.constraint[validatorType])[0];
|
||||
var abstractPriorValue = rule.constraint[validatorType][validator];
|
||||
abstractPriorValue = parseInt(abstractPriorValue, 10);
|
||||
// TODO: Restrict or clarify validators for abstractPriorValue?
|
||||
|
||||
var studies = WorklistStudies.find({
|
||||
patientId: currentStudy.patientId,
|
||||
studyDate: {
|
||||
$lt: currentStudy.studyDate
|
||||
}
|
||||
}, {
|
||||
sort: {
|
||||
studyDate: -1
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
// TODO: Revisit this later: What about two studies with the same
|
||||
// study date?
|
||||
|
||||
var priorStudy;
|
||||
if (abstractPriorValue === -1) {
|
||||
priorStudy = studies[studies.length - 1];
|
||||
} else {
|
||||
var studyIndex = Math.max(abstractPriorValue - 1, 0);
|
||||
priorStudy = studies[studyIndex];
|
||||
}
|
||||
|
||||
if (!priorStudy) {
|
||||
return;
|
||||
}
|
||||
|
||||
var alreadyLoaded = ViewerStudies.findOne({
|
||||
studyInstanceUid: priorStudy.studyInstanceUid
|
||||
});
|
||||
|
||||
if (!alreadyLoaded) {
|
||||
getStudyMetadata(priorStudy.studyInstanceUid, study => {
|
||||
study.abstractPriorValue = abstractPriorValue;
|
||||
|
||||
ViewerStudies.insert(study);
|
||||
this.studies.push(study);
|
||||
this.matchImages(viewport);
|
||||
this.updateViewports();
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: Add relative Date / time
|
||||
});
|
||||
|
||||
var lastStudyIndex = this.studies.length - 1;
|
||||
this.studies.forEach(function(study) {
|
||||
var studyMatchDetails = HP.match(study, studyMatchingRules);
|
||||
if ((studyMatchingRules.length && !studyMatchDetails.score) ||
|
||||
studyMatchDetails.score < highestStudyMatchingScore) {
|
||||
return;
|
||||
}
|
||||
|
||||
highestStudyMatchingScore = studyMatchDetails.score;
|
||||
|
||||
study.seriesList.forEach(function(series) {
|
||||
var seriesMatchDetails = HP.match(series, seriesMatchingRules);
|
||||
if ((seriesMatchingRules.length && !seriesMatchDetails.score) ||
|
||||
seriesMatchDetails.score < highestSeriesMatchingScore) {
|
||||
return;
|
||||
}
|
||||
|
||||
highestSeriesMatchingScore = seriesMatchDetails.score;
|
||||
|
||||
series.instances.forEach(function(instance, index) {
|
||||
// This tests to make sure there is actually image data in this instance
|
||||
// TODO: Change this when we add PDF and MPEG support
|
||||
// See https://ohiforg.atlassian.net/browse/LT-227
|
||||
if (!instance.rows || !instance.columns) {
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceMatchDetails = HP.match(instance, instanceMatchingRules);
|
||||
|
||||
var matchDetails = {
|
||||
passed: [],
|
||||
failed: []
|
||||
};
|
||||
|
||||
matchDetails.passed = matchDetails.passed.concat(instanceMatchDetails.details.passed);
|
||||
matchDetails.passed = matchDetails.passed.concat(seriesMatchDetails.details.passed);
|
||||
matchDetails.passed = matchDetails.passed.concat(studyMatchDetails.details.passed);
|
||||
|
||||
matchDetails.failed = matchDetails.failed.concat(instanceMatchDetails.details.failed);
|
||||
matchDetails.failed = matchDetails.failed.concat(seriesMatchDetails.details.failed);
|
||||
matchDetails.failed = matchDetails.failed.concat(studyMatchDetails.details.failed);
|
||||
|
||||
var totalMatchScore = instanceMatchDetails.score + seriesMatchDetails.score + studyMatchDetails.score;
|
||||
|
||||
var imageDetails = {
|
||||
studyInstanceUid: study.studyInstanceUid,
|
||||
seriesInstanceUid: series.seriesInstanceUid,
|
||||
sopInstanceUid: instance.sopInstanceUid,
|
||||
currentImageIdIndex: index,
|
||||
matchingScore: totalMatchScore,
|
||||
matchDetails: matchDetails
|
||||
};
|
||||
|
||||
if ((totalMatchScore > highestImageMatchingScore) || !bestMatch) {
|
||||
highestImageMatchingScore = totalMatchScore;
|
||||
bestMatch = imageDetails;
|
||||
}
|
||||
|
||||
matchingScores.push(imageDetails);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
bestMatch: bestMatch,
|
||||
matchingScores: matchingScores
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rerenders viewports that are part of the current ProtocolEngine's LayoutManager
|
||||
* using the matching rules internal to each viewport.
|
||||
*
|
||||
* If this function is provided the index of a viewport, only the specified viewport
|
||||
* is rerendered.
|
||||
*
|
||||
* @param viewportIndex
|
||||
*/
|
||||
updateViewports(viewportIndex) {
|
||||
// Make sure we have an active protocol with a non-empty array of display sets
|
||||
if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the current display set in the display set sequence
|
||||
var stageModel = this.getCurrentStageModel();
|
||||
|
||||
// If the current display set does not fulfill the requirements to be displayed,
|
||||
// stop here.
|
||||
if (!stageModel ||
|
||||
!stageModel.viewportStructure ||
|
||||
!stageModel.viewports ||
|
||||
!stageModel.viewports.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the layoutTemplate associated with the current display set's viewport structure
|
||||
// If no such template name exists, stop here.
|
||||
var layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
|
||||
if (!layoutTemplateName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the properties associated with the current display set's viewport structure template
|
||||
// If no such layout properties exist, stop here.
|
||||
var layoutProps = stageModel.viewportStructure.properties;
|
||||
if (!layoutProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an empty array to store the output viewportData
|
||||
var viewportData = [];
|
||||
|
||||
// Empty the matchDetails associated with the ProtocolEngine.
|
||||
// This will be used to store the pass/fail details and score
|
||||
// for each of the viewport matching procedures
|
||||
this.matchDetails = [];
|
||||
|
||||
// Loop through each viewport
|
||||
stageModel.viewports.forEach((viewport, viewportIndex) => {
|
||||
var details = this.matchImages(viewport);
|
||||
this.matchDetails[viewportIndex] = details;
|
||||
|
||||
// Convert any YES/NO values into true/false for Cornerstone
|
||||
var cornerstoneViewportParams = {};
|
||||
Object.keys(viewport.viewportSettings).forEach(function(key) {
|
||||
var value = viewport.viewportSettings[key];
|
||||
if (value === 'YES') {
|
||||
value = true;
|
||||
} else if (value === 'NO') {
|
||||
value = false;
|
||||
}
|
||||
|
||||
cornerstoneViewportParams[key] = value;
|
||||
});
|
||||
|
||||
// imageViewerViewports occasionally needs relevant layout data in order to set
|
||||
// the element style of the viewport in question
|
||||
var currentViewportData = $.extend({
|
||||
viewportIndex: viewportIndex,
|
||||
viewport: cornerstoneViewportParams
|
||||
}, layoutProps);
|
||||
|
||||
var customSettings = [];
|
||||
Object.keys(viewport.viewportSettings).forEach(id => {
|
||||
var setting = HP.CustomViewportSettings[id];
|
||||
if (!setting) {
|
||||
return;
|
||||
}
|
||||
|
||||
customSettings.push({
|
||||
id: id,
|
||||
value: viewport.viewportSettings[id]
|
||||
});
|
||||
});
|
||||
|
||||
currentViewportData.renderedCallback = function(element) {
|
||||
console.log('renderedCallback for ' + element.id);
|
||||
customSettings.forEach(function(customSetting) {
|
||||
console.log('Applying custom setting: ' + customSetting.id);
|
||||
console.log('with value: ' + customSetting.value);
|
||||
|
||||
var setting = HP.CustomViewportSettings[customSetting.id];
|
||||
setting.callback(element, customSetting.value);
|
||||
});
|
||||
};
|
||||
|
||||
if (details.bestMatch) {
|
||||
currentViewportData.studyInstanceUid = details.bestMatch.studyInstanceUid;
|
||||
currentViewportData.seriesInstanceUid = details.bestMatch.seriesInstanceUid;
|
||||
currentViewportData.sopInstanceUid = details.bestMatch.sopInstanceUid;
|
||||
currentViewportData.currentImageIdIndex = details.bestMatch.currentImageIdIndex;
|
||||
}
|
||||
|
||||
viewportData.push(currentViewportData);
|
||||
});
|
||||
|
||||
this.LayoutManager.layoutTempleName = layoutTemplateName;
|
||||
this.LayoutManager.layoutProps = layoutProps;
|
||||
this.LayoutManager.viewportData = viewportData;
|
||||
|
||||
if (viewportIndex !== undefined && viewportData[viewportIndex]) {
|
||||
this.LayoutManager.rerenderViewportWithNewSeries(viewportIndex, viewportData[viewportIndex]);
|
||||
} else {
|
||||
this.LayoutManager.updateViewports();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current Hanging Protocol to the specified Protocol
|
||||
* An optional argument can also be used to prevent the updating of the Viewports
|
||||
*
|
||||
* @param newProtocol
|
||||
* @param updateViewports
|
||||
*/
|
||||
setHangingProtocol(newProtocol, updateViewports=true) {
|
||||
// Reset the array of newStageIds
|
||||
this.newStageIds = [];
|
||||
|
||||
if (HP.Protocol.prototype.isPrototypeOf(newProtocol)) {
|
||||
this.protocol = newProtocol;
|
||||
} else {
|
||||
this.protocol = new HP.Protocol();
|
||||
this.protocol.fromObject(newProtocol);
|
||||
}
|
||||
|
||||
this.stage = 0;
|
||||
|
||||
// Update viewports by default
|
||||
if (updateViewports) {
|
||||
this.updateViewports();
|
||||
}
|
||||
|
||||
MatchedProtocols.update({}, {
|
||||
$set: {
|
||||
selected: false
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
MatchedProtocols.update({
|
||||
id: this.protocol.id
|
||||
}, {
|
||||
$set: {
|
||||
selected: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the current stage to a new stage index in the display set sequence
|
||||
*
|
||||
* @param stage An integer value specifying the index of the desired Stage
|
||||
*/
|
||||
setCurrentProtocolStage(stage) {
|
||||
if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stage >= this.protocol.stages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stage = stage;
|
||||
this.updateViewports();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of Stages in the current Protocol
|
||||
*/
|
||||
getNumProtocolStages() {
|
||||
if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.protocol.stages.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the next protocol stage in the display set sequence
|
||||
*/
|
||||
nextProtocolStage() {
|
||||
this.setCurrentProtocolStage(++this.stage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the previous protocol stage in the display set sequence
|
||||
*/
|
||||
previousProtocolStage() {
|
||||
this.setCurrentProtocolStage(--this.stage);
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user