Moving updates on Hanging Protocols from Nucleus to private OHIF branch

This commit is contained in:
Emanuel F. Oliveira 2017-02-27 19:03:43 -03:00 committed by Eloízio Salgado
parent 2a6cb42228
commit 744a0bd76e
7 changed files with 387 additions and 222 deletions

View File

@ -50,6 +50,10 @@ HP.Protocol = class Protocol {
// default protocols. // default protocols.
this.locked = false; this.locked = false;
// Boolean value to indicate if the protocol has updated priors information
// it's set in "updateNumberOfPriorsReferenced" function
this.hasUpdatedPriorsInformation = false;
// Apply the desired name // Apply the desired name
this.name = name; this.name = name;
@ -73,28 +77,42 @@ HP.Protocol = class Protocol {
// and Stages // and Stages
this.protocolMatchingRules = []; this.protocolMatchingRules = [];
this.stages = []; this.stages = [];
// Define auxiliary values for priors
this.numberOfPriorsReferenced = 0; this.numberOfPriorsReferenced = 0;
this.numberOfPriorsReferencedRequired = 0;
} }
updateNumberOfPriorsReferenced() { updateNumberOfPriorsReferenced() {
var numPriorsReferenced = 0; let numPriorsReferenced = 0;
let numberOfPriorsReferencedRequired = 0;
this.stages.forEach(function(stage) { this.stages.forEach(stage => {
if (!stage.viewports) { if (!stage.viewports) {
return; return;
} }
stage.viewports.forEach(function(viewport) { stage.viewports.forEach(viewport => {
if (!viewport.studyMatchingRules) { if (!viewport.studyMatchingRules) {
return; return;
} }
viewport.studyMatchingRules.forEach(function(rule) { viewport.studyMatchingRules.forEach(rule => {
if (rule.attribute === 'abstractPriorValue') { if (rule.attribute === 'abstractPriorValue') {
// If the rule is required
if (rule.required) {
numberOfPriorsReferencedRequired++;
}
// TODO: Double check here that the abstractPriorValue is not // TODO: Double check here that the abstractPriorValue is not
// set as zero // set as zero
numPriorsReferenced++; numPriorsReferenced++;
} else if (rule.attribute === 'relativeTime') { } else if (rule.attribute === 'relativeTime') {
// If the rule is required
if (rule.required) {
numberOfPriorsReferencedRequired++;
}
numPriorsReferenced++; numPriorsReferenced++;
} }
}); });
@ -102,6 +120,10 @@ HP.Protocol = class Protocol {
}); });
this.numberOfPriorsReferenced = numPriorsReferenced; this.numberOfPriorsReferenced = numPriorsReferenced;
this.numberOfPriorsReferencedRequired = numberOfPriorsReferencedRequired;
// To indicate that the priors informations were updated
this.hasUpdatedPriorsInformation = true;
} }
/** /**
@ -115,6 +137,11 @@ HP.Protocol = class Protocol {
this.modifiedBy = Meteor.userId; this.modifiedBy = Meteor.userId;
} }
// Protocol has been modified, so mark priors information
// as "outdated"
this.hasUpdatedPriorsInformation = false;
// Update number of priors referenced info
this.updateNumberOfPriorsReferenced(); this.updateNumberOfPriorsReferenced();
// Update the modifiedDate with the current Date/Time // Update the modifiedDate with the current Date/Time

View File

@ -0,0 +1,28 @@
// 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 = (attributeId, attributeName, callback) => {
HP.CustomAttributeRetrievalCallbacks[attributeId] = {
name: attributeName,
callback: callback
};
};

View File

@ -0,0 +1,20 @@
// 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 = (settingId, settingName, options, callback) => {
HP.CustomViewportSettings[settingId] = {
id: settingId,
text: settingName,
options: options,
callback: callback
};
};

View File

@ -0,0 +1,8 @@
// Sorts an array by score
const sortByScore = arr => {
arr.sort((a, b) => {
return b.score - a.score;
});
};
export { sortByScore };

View File

@ -0,0 +1,113 @@
// OHIF Modules
import { Viewerbase } from 'meteor/ohif:viewerbase';
// Local imports
import '../customAttributes';
/**
* Import Constants
*/
const { OHIFError, metadata: { StudySummary, InstanceMetadata } } = Viewerbase;
/**
* Match a Metadata instance against rules using Validate.js for validation.
* @param {StudySummary|InstanceMetadata} metadataInstance Metadata instance object
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
* @return {Object} Matching Object with score and details (which rule passed or failed)
*/
const match = (metadataInstance, rules) => {
// Make sure the supplied data is valid.
if (!(metadataInstance instanceof StudySummary || metadataInstance instanceof InstanceMetadata)) {
throw new OHIFError('HPMatcher::match metadataInstance must be an instance of StudySummary or InstanceMetadata');
}
const options = {
format: 'grouped'
};
const details = {
passed: [],
failed: []
};
let requiredFailed = false;
let score = 0;
rules.forEach(rule => {
const attribute = rule.attribute;
let customAttributeExists = metadataInstance.customAttributeExists(attribute);
// If the metadataInstance we are testing (e.g. study, series, or instance MetadataInstance) do
// not contain the attribute specified in the rule, check whether or not they have been
// defined in the CustomAttributeRetrievalCallbacks Object.
if (!customAttributeExists && HP.CustomAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
const customAttribute = HP.CustomAttributeRetrievalCallbacks[attribute];
metadataInstance.setCustomAttribute(attribute, customAttribute.callback(metadataInstance));
customAttributeExists = true;
}
// Format the constraint as required by Validate.js
const testConstraint = {
[attribute]: rule.constraint
};
// Create a single attribute object to be validated, since metadataInstance is an
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
const attributeValue = customAttributeExists ? metadataInstance.getCustomAttribute(attribute) : metadataInstance.getTagValue(attribute);
const attributeMap = {
[attribute]: attributeValue
};
// Use Validate.js to evaluate the constraints on the specified metadataInstance
let errorMessages;
try {
errorMessages = validate(attributeMap, testConstraint, [options]);
} catch (e) {
errorMessages = [ 'Something went wrong during validation.', e ];
}
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
});
} 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,
errorMessages
});
}
});
// If a required Rule has failed Validation, set the matching score to zero
if (requiredFailed) {
score = 0;
}
return {
score,
details
};
};
const HPMatcher = {
match
};
export { HPMatcher };

View File

@ -1,17 +1,24 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { _ } from 'meteor/underscore'; import { _ } from 'meteor/underscore';
import { Session } from 'meteor/session';
// OHIF Modules // OHIF Modules
import { OHIF } from 'meteor/ohif:core'; import { OHIF } from 'meteor/ohif:core';
import 'meteor/ohif:viewerbase'; import 'meteor/ohif:viewerbase';
// Hanging Protocol local imports
import { HPMatcher } from './matcher/HPMatcher';
import { sortByScore } from './lib/sortByScore';
import './customViewportSettings';
/** /**
* Import Constants * Import Constants
*/ */
const { OHIFError } = OHIF.viewerbase; const { OHIFError, metadata: { StudyMetadata, SeriesMetadata, InstanceMetadata, StudySummary } } = OHIF.viewerbase;
const { StudyMetadata, SeriesMetadata, InstanceMetadata, StudySummary } = OHIF.viewerbase.metadata;
// Useful constants
const ABSTRACT_PRIOR_VALUE = 'abstractPriorValue';
// Define a global variable that will be used to refer to the Protocol Engine // 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 // It must be populated by HP.setEngine when the Viewer is initialized and a ProtocolEngine
@ -29,172 +36,18 @@ ProtocolEngine = undefined;
* @param protocolEngine An instantiated ProtocolEngine linked to a LayoutManager from the * @param protocolEngine An instantiated ProtocolEngine linked to a LayoutManager from the
* Viewerbase package * Viewerbase package
*/ */
HP.setEngine = function(protocolEngine) { HP.setEngine = protocolEngine => {
ProtocolEngine = protocolEngine; ProtocolEngine = protocolEngine;
}; };
// Define an empty object to store callbacks that are used to retrieve custom attributes Meteor.startup(() => {
// The simplest example for a custom attribute is the Timepoint type (i.e. baseline or follow-up) HP.addCustomViewportSetting('wlPreset', 'Window/Level Preset', Object.create(null), (element, optionValue) => {
// 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.create(null), function(element, optionValue) {
if (optionValue in OHIF.viewer.wlPresets) { if (optionValue in OHIF.viewer.wlPresets) {
OHIF.viewerbase.wlPresets.applyWLPreset(optionValue, element); OHIF.viewerbase.wlPresets.applyWLPreset(optionValue, element);
} }
}); });
}); });
/**
* Match a Metadata instance against rules using Validate.js for validation.
* @param {StudySummary|InstanceMetadata} metadataInstance Metadata instance object
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
* @return {Object} Matching Object with score and details (which rule passed or failed)
*/
HP.match = function(metadataInstance, rules) {
// Make sure the supplied data is valid.
if (!(metadataInstance instanceof StudySummary || metadataInstance instanceof InstanceMetadata)) {
throw new OHIFError('HP::match metadataInstance must be an instance of StudySummary or InstanceMetadata');
}
const options = {
format: 'grouped'
};
const details = {
passed: [],
failed: []
};
let requiredFailed = false;
let score = 0;
rules.forEach(rule => {
const attribute = rule.attribute;
let customAttributeExists = metadataInstance.customAttributeExists(attribute);
// If the metadataInstance we are testing (e.g. study, series, or instance MetadataInstance) do
// not contain the attribute specified in the rule, check whether or not they have been
// defined in the CustomAttributeRetrievalCallbacks Object.
if (!customAttributeExists && HP.CustomAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
const customAttribute = HP.CustomAttributeRetrievalCallbacks[attribute];
metadataInstance.setCustomAttribute(attribute, customAttribute.callback(metadataInstance));
customAttributeExists = true;
}
// Format the constraint as required by Validate.js
const testConstraint = {
[attribute]: rule.constraint
};
// Create a single attribute object to be validated, since metadataInstance is an
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
const attributeValue = customAttributeExists ? metadataInstance.getCustomAttribute(attribute) : metadataInstance.getTagValue(attribute);
const attributeMap = {
[attribute]: attributeValue
};
// Use Validate.js to evaluate the constraints on the specified metadataInstance
let errorMessages;
try {
errorMessages = validate(attributeMap, testConstraint, [options]);
} catch (e) {
errorMessages = [ 'Something went wrong during validation.', e ];
}
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
});
} 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,
errorMessages
});
}
});
// If a required Rule has failed Validation, set the matching score to zero
if (requiredFailed) {
score = 0;
}
return {
score,
details
};
};
const sortByScore = arr => {
arr.sort((a, b) => {
return b.score - a.score;
});
};
HP.ProtocolEngine = class ProtocolEngine { HP.ProtocolEngine = class ProtocolEngine {
/** /**
* Constructor * Constructor
@ -241,7 +94,8 @@ HP.ProtocolEngine = class ProtocolEngine {
* Resets the ProtocolEngine to the best match * Resets the ProtocolEngine to the best match
*/ */
reset() { reset() {
const protocol = this.getBestMatch(); const protocol = this.getBestProtocolMatch();
this.setHangingProtocol(protocol); this.setHangingProtocol(protocol);
} }
@ -254,51 +108,112 @@ HP.ProtocolEngine = class ProtocolEngine {
return this.protocol.stages[this.stage]; return this.protocol.stages[this.stage];
} }
/**
* Get number of priors rules for a given protocol. This rules are for raising up the score when
* matching a protocol against study's number of priors. Protocols with same study/series/instance
* rules that references the exactly number of priors that a study will have highest score.
* @param {Integer} numberOfPriorsReferenced Number of priors required by the protocol
* @return {Array} Returs an array of HP.ProtocolMatchingRule objects
*/
getNumberOfPriorsRules(numberOfPriorsReferenced) {
const rules = [];
// If study at least the number of priors required by the protocol
const requiredPriorsRule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', {
numericality: {
greaterThanOrEqualTo: numberOfPriorsReferenced
}
});
// If study has the same number of priors
const equalPriorsRule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', {
numericality: {
equalTo: numberOfPriorsReferenced
}
});
rules.push(requiredPriorsRule, equalPriorsRule);
return rules;
}
/**
* Check if a given protocol requires a minimum number of priors.
* @param {HP.Protocol} protocol HP.Protocol object that contains rules
* @return {Boolean} Returns true if the protocol requires priors or false otherwise
*/
protocolRequiresPriors(protocol) {
return protocol.numberOfPriorsReferencedRequired > 0;
}
/**
* Finds the best protocols from Protocol Store, matching each protocol matching rules
* with the given study. The best protocol are orded by score and returned in an array
* @param {Object} study StudyMetadata instance object
* @return {Array} Array of match objects or an empty array if no match was found
* Each match object has the score of the matching and the matched
* protocol
*/
findMatchByStudy(study) { findMatchByStudy(study) {
OHIF.log.info('ProtocolEngine::findMatchByStudy'); OHIF.log.info('ProtocolEngine::findMatchByStudy');
const matched = []; const matched = [];
const studyInstance = study.getFirstInstance(); const studyInstance = study.getFirstInstance();
// Set custom attribute for study metadata
const numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study.getObjectID());
studyInstance.setCustomAttribute('numberOfPriorsReferenced', numberOfPriorsReferenced);
HP.ProtocolStore.getProtocol().forEach(protocol => { HP.ProtocolStore.getProtocol().forEach(protocol => {
// Clone the protocol's protocolMatchingRules array // Clone the protocol's protocolMatchingRules array
// We clone it so that we don't accidentally add the // We clone it so that we don't accidentally add the
// numberOfPriorsReferenced rule to the Protocol itself. // numberOfPriorsReferenced rule to the Protocol itself.
const rules = protocol.protocolMatchingRules.slice(0); let rules = protocol.protocolMatchingRules.slice(0);
if (!rules) { if (!rules) {
return; return;
} }
// Set custom attribute for study metadata // To make sure the protocol has updated priors information.
const numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study.getObjectID()); // This is reasonable for Viewers that use different APIs to
studyInstance.setCustomAttribute('numberOfPriorsReferenced', numberOfPriorsReferenced); // manage ProtocolStore or protocols.
if (!protocol.hasUpdatedPriorsInformation) {
const requiredPriorsRule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', { // If protocol is not an instance of HP.Protocol
numericality: { // make it be.
greaterThanOrEqualTo: protocol.numberOfPriorsReferenced if (!(protocol instanceof HP.Protocol)) {
const protocolObject = new HP.Protocol();
protocolObject.fromObject(protocol);
protocol = protocolObject;
} }
});
rules.push(requiredPriorsRule); protocol.updateNumberOfPriorsReferenced();
}
const exactPriorsDesiredRule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', { // Skip protocols that require more priors than the study has
numericality: { const protocolRequiresPriors = this.protocolRequiresPriors(protocol);
equalTo: protocol.numberOfPriorsReferenced if (protocolRequiresPriors && numberOfPriorsReferenced < protocol.numberOfPriorsReferenced) {
}, return;
}, false, 1); }
rules.push(exactPriorsDesiredRule); // Get additional rules for number of priors referenced
const priorsRules = this.getNumberOfPriorsRules(protocol.numberOfPriorsReferenced);
// Concatenate rules
rules = rules.concat(priorsRules);
const matchedDetails = HP.match(studyInstance, rules); // Run the matcher and get matching details
const matchedDetails = HPMatcher.match(studyInstance, rules);
const score = matchedDetails.score;
if (matchedDetails.score > 0) { // The protocol matched some rule, add it to the matched list
if (score > 0) {
matched.push({ matched.push({
score: matchedDetails.score, score,
protocol: protocol protocol
}); });
} }
}); });
// If no matches were found, select the default protocol
if (!matched.length) { if (!matched.length) {
const defaultProtocol = HP.ProtocolStore.getProtocol('defaultProtocol'); const defaultProtocol = HP.ProtocolStore.getProtocol('defaultProtocol');
@ -308,6 +223,7 @@ HP.ProtocolEngine = class ProtocolEngine {
}]; }];
} }
// Sort the matched list by score
sortByScore(matched); sortByScore(matched);
OHIF.log.info('ProtocolEngine::findMatchByStudy matched', matched); OHIF.log.info('ProtocolEngine::findMatchByStudy matched', matched);
@ -318,8 +234,8 @@ HP.ProtocolEngine = class ProtocolEngine {
/** /**
* Populates the MatchedProtocols Collection by running the matching procedure * Populates the MatchedProtocols Collection by running the matching procedure
*/ */
updateMatches() { updateProtocolMatches() {
OHIF.log.info('ProtocolEngine::updateMatches'); OHIF.log.info('ProtocolEngine::updateProtocolMatches');
// Clear all data from the MatchedProtocols Collection // Clear all data from the MatchedProtocols Collection
MatchedProtocols.remove({}); MatchedProtocols.remove({});
@ -341,6 +257,7 @@ HP.ProtocolEngine = class ProtocolEngine {
// If it is not already in the MatchedProtocols Collection, insert it with its score // If it is not already in the MatchedProtocols Collection, insert it with its score
if (!protocolInCollection) { if (!protocolInCollection) {
OHIF.log.info('ProtocolEngine::updateProtocolMatches inserting protocol match', matchedDetail);
MatchedProtocols.insert(matchedDetail); MatchedProtocols.insert(matchedDetail);
} }
}); });
@ -351,9 +268,9 @@ HP.ProtocolEngine = class ProtocolEngine {
* Return the best matched Protocol to the current study or set of studies * Return the best matched Protocol to the current study or set of studies
* @returns {*} * @returns {*}
*/ */
getBestMatch() { getBestProtocolMatch() {
// Run the matching to populate the MatchedProtocols Collection // Run the matching to populate the MatchedProtocols Collection
this.updateMatches(); this.updateProtocolMatches();
// Retrieve the highest scoring Protocol // Retrieve the highest scoring Protocol
const sorted = MatchedProtocols.find({}, { const sorted = MatchedProtocols.find({}, {
@ -366,7 +283,8 @@ HP.ProtocolEngine = class ProtocolEngine {
// Highest scoring Protocol // Highest scoring Protocol
const bestMatch = sorted[0].protocol; const bestMatch = sorted[0].protocol;
OHIF.log.info('ProtocolEngine::getBestMatch bestMatch', bestMatch); OHIF.log.info('ProtocolEngine::getBestProtocolMatch bestMatch', bestMatch);
return bestMatch; return bestMatch;
} }
@ -378,6 +296,7 @@ HP.ProtocolEngine = class ProtocolEngine {
*/ */
getNumberOfAvailablePriors(studyObjectID) { getNumberOfAvailablePriors(studyObjectID) {
const priors = this.getAvailableStudyPriors(studyObjectID); const priors = this.getAvailableStudyPriors(studyObjectID);
return priors.length; return priors.length;
} }
@ -389,6 +308,7 @@ HP.ProtocolEngine = class ProtocolEngine {
*/ */
getAvailableStudyPriors(studyObjectID) { getAvailableStudyPriors(studyObjectID) {
const priors = this.priorStudies.get(studyObjectID); const priors = this.priorStudies.get(studyObjectID);
return priors instanceof Array ? priors : []; return priors instanceof Array ? priors : [];
} }
@ -407,7 +327,7 @@ HP.ProtocolEngine = class ProtocolEngine {
if (prior instanceof StudyMetadata) { if (prior instanceof StudyMetadata) {
prior = prior.getFirstInstance(); prior = prior.getFirstInstance();
} }
const matchDetails = HP.match(prior, protocolMatchingRules); const matchDetails = HPMatcher.match(prior, protocolMatchingRules);
return matchDetails.score > 0; return matchDetails.score > 0;
}); });
} }
@ -416,7 +336,7 @@ HP.ProtocolEngine = class ProtocolEngine {
} }
// Match images given a list of Studies and a Viewport's image matching reqs // Match images given a list of Studies and a Viewport's image matching reqs
matchImages(viewport) { matchImages(viewport, viewportIndex) {
OHIF.log.info('ProtocolEngine::matchImages'); OHIF.log.info('ProtocolEngine::matchImages');
const { studyMatchingRules, seriesMatchingRules, imageMatchingRules: instanceMatchingRules } = viewport; const { studyMatchingRules, seriesMatchingRules, imageMatchingRules: instanceMatchingRules } = viewport;
@ -431,13 +351,13 @@ HP.ProtocolEngine = class ProtocolEngine {
let bestMatch; let bestMatch;
// Set custom attribute for study metadata and it's first instance // Set custom attribute for study metadata and it's first instance
currentStudy.setCustomAttribute('abstractPriorValue', 0); currentStudy.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
if (firstInstance instanceof InstanceMetadata) { if (firstInstance instanceof InstanceMetadata) {
firstInstance.setCustomAttribute('abstractPriorValue', 0); firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
} }
studyMatchingRules.forEach(rule => { studyMatchingRules.forEach(rule => {
if (rule.attribute === 'abstractPriorValue') { if (rule.attribute === ABSTRACT_PRIOR_VALUE) {
const validatorType = Object.keys(rule.constraint)[0]; const validatorType = Object.keys(rule.constraint)[0];
const validator = Object.keys(rule.constraint[validatorType])[0]; const validator = Object.keys(rule.constraint[validatorType])[0];
@ -473,19 +393,19 @@ HP.ProtocolEngine = class ProtocolEngine {
// Get study metadata if necessary and load study in the viewer (each viewer should provide it's own load study method) // Get study metadata if necessary and load study in the viewer (each viewer should provide it's own load study method)
this.studyMetadataSource.loadStudy(priorStudy).then(studyMetadata => { this.studyMetadataSource.loadStudy(priorStudy).then(studyMetadata => {
// Set the custom attribute abstractPriorValue for the study metadata // Set the custom attribute abstractPriorValue for the study metadata
studyMetadata.setCustomAttribute('abstractPriorValue', abstractPriorValue); studyMetadata.setCustomAttribute(ABSTRACT_PRIOR_VALUE, abstractPriorValue);
// Also add custom attribute // Also add custom attribute
const firstInstance = studyMetadata.getFirstInstance(); const firstInstance = studyMetadata.getFirstInstance();
if (firstInstance instanceof InstanceMetadata) { if (firstInstance instanceof InstanceMetadata) {
firstInstance.setCustomAttribute('abstractPriorValue', abstractPriorValue); firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, abstractPriorValue);
} }
// Insert the new study metadata // Insert the new study metadata
this.studies.push(studyMetadata); this.studies.push(studyMetadata);
// Update the viewport to refresh layout manager with new study // Update the viewport to refresh layout manager with new study
this.updateViewports(); this.updateViewports(viewportIndex);
}, error => { }, error => {
OHIF.log.warn(error); OHIF.log.warn(error);
throw new OHIFError(`ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`); throw new OHIFError(`ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`);
@ -495,7 +415,7 @@ HP.ProtocolEngine = class ProtocolEngine {
}); });
this.studies.forEach(study => { this.studies.forEach(study => {
const studyMatchDetails = HP.match(study.getFirstInstance(), studyMatchingRules); const studyMatchDetails = HPMatcher.match(study.getFirstInstance(), studyMatchingRules);
if ((studyMatchingRules.length && !studyMatchDetails.score) || if ((studyMatchingRules.length && !studyMatchDetails.score) ||
studyMatchDetails.score < highestStudyMatchingScore) { studyMatchDetails.score < highestStudyMatchingScore) {
return; return;
@ -504,7 +424,7 @@ HP.ProtocolEngine = class ProtocolEngine {
highestStudyMatchingScore = studyMatchDetails.score; highestStudyMatchingScore = studyMatchDetails.score;
study.forEachSeries(series => { study.forEachSeries(series => {
const seriesMatchDetails = HP.match(series.getFirstInstance(), seriesMatchingRules); const seriesMatchDetails = HPMatcher.match(series.getFirstInstance(), seriesMatchingRules);
if ((seriesMatchingRules.length && !seriesMatchDetails.score) || if ((seriesMatchingRules.length && !seriesMatchDetails.score) ||
seriesMatchDetails.score < highestSeriesMatchingScore) { seriesMatchDetails.score < highestSeriesMatchingScore) {
return; return;
@ -522,7 +442,7 @@ HP.ProtocolEngine = class ProtocolEngine {
return; return;
} }
const instanceMatchDetails = HP.match(instance, instanceMatchingRules); const instanceMatchDetails = HPMatcher.match(instance, instanceMatchingRules);
const matchDetails = { const matchDetails = {
passed: [], passed: [],
@ -609,7 +529,7 @@ HP.ProtocolEngine = class ProtocolEngine {
OHIF.log.info(`ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}`); OHIF.log.info(`ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}`);
// Make sure we have an active protocol with a non-empty array of display sets // 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) { if (!this.getNumProtocolStages()) {
return; return;
} }
@ -649,7 +569,7 @@ HP.ProtocolEngine = class ProtocolEngine {
// Loop through each viewport // Loop through each viewport
stageModel.viewports.forEach((viewport, viewportIndex) => { stageModel.viewports.forEach((viewport, viewportIndex) => {
const details = this.matchImages(viewport); const details = this.matchImages(viewport, viewportIndex);
this.matchDetails[viewportIndex] = details; this.matchDetails[viewportIndex] = details;
@ -747,7 +667,7 @@ HP.ProtocolEngine = class ProtocolEngine {
* @param newProtocol * @param newProtocol
* @param updateViewports * @param updateViewports
*/ */
setHangingProtocol(newProtocol, updateViewports=true) { setHangingProtocol(newProtocol, updateViewports = true) {
OHIF.log.info('ProtocolEngine::setHangingProtocol newProtocol', newProtocol); OHIF.log.info('ProtocolEngine::setHangingProtocol newProtocol', newProtocol);
OHIF.log.info(`ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`); OHIF.log.info(`ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`);
@ -783,30 +703,64 @@ HP.ProtocolEngine = class ProtocolEngine {
selected: true selected: true
} }
}); });
Session.set('HangingProtocolName', this.protocol.name);
Session.set('HangingProtocolStage', this.stage);
} }
/** /**
* Changes the current stage to a new stage index in the display set sequence * Check if the next stage is available
* * @return {Boolean} True if next stage is available or false otherwise
* @param stage An integer value specifying the index of the desired Stage
*/ */
setCurrentProtocolStage(stage) { isNextStageAvailable() {
OHIF.log.info(`ProtocolEngine::setCurrentProtocolStage stage = ${stage}`); const numberOfStages = this.getNumProtocolStages();
if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) { return this.stage + 1 < numberOfStages;
return; }
}
if (stage >= this.protocol.stages.length) { /**
return; * Check if the previous stage is available
} * @return {Boolean} True if previous stage is available or false otherwise
*/
this.stage = stage; isPreviousStageAvailable() {
this.updateViewports(); return this.stage - 1 >= 0;
} }
/** /**
* Retrieves the number of Stages in the current Protocol * Changes the current stage to a new stage index in the display set sequence.
* It checks if the next stage exists.
*
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
* @return {Boolean} True if new stage has set or false, otherwise
*/
setCurrentProtocolStage(stageAction) {
// Check if previous or next stage is available
if (stageAction === -1 && !this.isPreviousStageAvailable()) {
return false;
} else if (stageAction === 1 && !this.isNextStageAvailable()) {
return false;
}
// Sets the new stage
this.stage += stageAction;
// Log the new stage
OHIF.log.info(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
// Set stage Session variable for reactivity
Session.set('HangingProtocolStage', this.stage);
// Since stage has changed, we need to update the viewports
// and redo matchings
this.updateViewports();
// Everything went well
return true;
}
/**
* Retrieves the number of Stages in the current Protocol or
* undefined if no protocol or stages are set
*/ */
getNumProtocolStages() { getNumProtocolStages() {
if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) { if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) {
@ -820,13 +774,23 @@ HP.ProtocolEngine = class ProtocolEngine {
* Switches to the next protocol stage in the display set sequence * Switches to the next protocol stage in the display set sequence
*/ */
nextProtocolStage() { nextProtocolStage() {
this.setCurrentProtocolStage(++this.stage); OHIF.log.info('ProtocolEngine::nextProtocolStage');
if (!this.setCurrentProtocolStage(1)) {
// Just for logging purpose
OHIF.log.info('ProtocolEngine::nextProtocolStage failed');
}
} }
/** /**
* Switches to the previous protocol stage in the display set sequence * Switches to the previous protocol stage in the display set sequence
*/ */
previousProtocolStage() { previousProtocolStage() {
this.setCurrentProtocolStage(--this.stage); OHIF.log.info('ProtocolEngine::previousProtocolStage');
if (!this.setCurrentProtocolStage(-1)) {
// Just for logging purpose
OHIF.log.info('ProtocolEngine::previousProtocolStage failed');
}
} }
}; };

View File

@ -288,14 +288,19 @@ const stopAllClips = () => {
const isStackScrollLinkingDisabled = () => { const isStackScrollLinkingDisabled = () => {
let linkableViewportsCount = 0;
// Its called everytime active viewport and/or layout change // Its called everytime active viewport and/or layout change
Session.get('viewportActivated'); Session.get('viewportActivated');
Session.get('LayoutManagerUpdated'); Session.get('LayoutManagerUpdated');
const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer; const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;
const linkableViewports = synchronizer.getLinkableViewports(); if (synchronizer) {
const linkableViewports = synchronizer.getLinkableViewports();
linkableViewportsCount = linkableViewports.length;
}
return linkableViewports.length <= 1; return linkableViewportsCount <= 1;
}; };
// Create an event listener to update playing state when a clip stops playing // Create an event listener to update playing state when a clip stops playing