From 744a0bd76e50b08952331ead155b8c5ad7b899e6 Mon Sep 17 00:00:00 2001 From: "Emanuel F. Oliveira" Date: Mon, 27 Feb 2017 19:03:43 -0300 Subject: [PATCH] Moving updates on Hanging Protocols from Nucleus to private OHIF branch --- .../ohif-hanging-protocols/both/schema.js | 35 +- .../client/customAttributes/index.js | 28 ++ .../client/customViewportSettings/index.js | 20 + .../client/lib/sortByScore.js | 8 + .../client/matcher/HPMatcher.js | 113 +++++ .../client/protocolEngine.js | 396 ++++++++---------- .../client/lib/viewportUtils.js | 9 +- 7 files changed, 387 insertions(+), 222 deletions(-) create mode 100644 Packages/ohif-hanging-protocols/client/customAttributes/index.js create mode 100644 Packages/ohif-hanging-protocols/client/customViewportSettings/index.js create mode 100644 Packages/ohif-hanging-protocols/client/lib/sortByScore.js create mode 100644 Packages/ohif-hanging-protocols/client/matcher/HPMatcher.js diff --git a/Packages/ohif-hanging-protocols/both/schema.js b/Packages/ohif-hanging-protocols/both/schema.js index 9de98cf99..d0e54fb61 100644 --- a/Packages/ohif-hanging-protocols/both/schema.js +++ b/Packages/ohif-hanging-protocols/both/schema.js @@ -50,6 +50,10 @@ HP.Protocol = class Protocol { // default protocols. 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 this.name = name; @@ -73,28 +77,42 @@ HP.Protocol = class Protocol { // and Stages this.protocolMatchingRules = []; this.stages = []; + + // Define auxiliary values for priors this.numberOfPriorsReferenced = 0; + this.numberOfPriorsReferencedRequired = 0; } updateNumberOfPriorsReferenced() { - var numPriorsReferenced = 0; + let numPriorsReferenced = 0; + let numberOfPriorsReferencedRequired = 0; - this.stages.forEach(function(stage) { + this.stages.forEach(stage => { if (!stage.viewports) { return; } - stage.viewports.forEach(function(viewport) { + stage.viewports.forEach(viewport => { if (!viewport.studyMatchingRules) { return; } - viewport.studyMatchingRules.forEach(function(rule) { + viewport.studyMatchingRules.forEach(rule => { if (rule.attribute === 'abstractPriorValue') { + // If the rule is required + if (rule.required) { + numberOfPriorsReferencedRequired++; + } + // TODO: Double check here that the abstractPriorValue is not // set as zero numPriorsReferenced++; } else if (rule.attribute === 'relativeTime') { + // If the rule is required + if (rule.required) { + numberOfPriorsReferencedRequired++; + } + numPriorsReferenced++; } }); @@ -102,6 +120,10 @@ HP.Protocol = class Protocol { }); 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; } + // Protocol has been modified, so mark priors information + // as "outdated" + this.hasUpdatedPriorsInformation = false; + + // Update number of priors referenced info this.updateNumberOfPriorsReferenced(); // Update the modifiedDate with the current Date/Time diff --git a/Packages/ohif-hanging-protocols/client/customAttributes/index.js b/Packages/ohif-hanging-protocols/client/customAttributes/index.js new file mode 100644 index 000000000..b8fb90893 --- /dev/null +++ b/Packages/ohif-hanging-protocols/client/customAttributes/index.js @@ -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 + }; +}; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/client/customViewportSettings/index.js b/Packages/ohif-hanging-protocols/client/customViewportSettings/index.js new file mode 100644 index 000000000..929ca1069 --- /dev/null +++ b/Packages/ohif-hanging-protocols/client/customViewportSettings/index.js @@ -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 + }; +}; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/client/lib/sortByScore.js b/Packages/ohif-hanging-protocols/client/lib/sortByScore.js new file mode 100644 index 000000000..21975bf41 --- /dev/null +++ b/Packages/ohif-hanging-protocols/client/lib/sortByScore.js @@ -0,0 +1,8 @@ +// Sorts an array by score +const sortByScore = arr => { + arr.sort((a, b) => { + return b.score - a.score; + }); +}; + +export { sortByScore }; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/client/matcher/HPMatcher.js b/Packages/ohif-hanging-protocols/client/matcher/HPMatcher.js new file mode 100644 index 000000000..16dda502b --- /dev/null +++ b/Packages/ohif-hanging-protocols/client/matcher/HPMatcher.js @@ -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 }; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/client/protocolEngine.js b/Packages/ohif-hanging-protocols/client/protocolEngine.js index 66be9c773..7663740d6 100644 --- a/Packages/ohif-hanging-protocols/client/protocolEngine.js +++ b/Packages/ohif-hanging-protocols/client/protocolEngine.js @@ -1,17 +1,24 @@ import { Meteor } from 'meteor/meteor'; import { _ } from 'meteor/underscore'; +import { Session } from 'meteor/session'; // OHIF Modules import { OHIF } from 'meteor/ohif:core'; import 'meteor/ohif:viewerbase'; +// Hanging Protocol local imports +import { HPMatcher } from './matcher/HPMatcher'; +import { sortByScore } from './lib/sortByScore'; +import './customViewportSettings'; + /** * Import Constants */ -const { OHIFError } = OHIF.viewerbase; -const { StudyMetadata, SeriesMetadata, InstanceMetadata, StudySummary } = OHIF.viewerbase.metadata; +const { OHIFError, metadata: { StudyMetadata, SeriesMetadata, InstanceMetadata, StudySummary } } = OHIF.viewerbase; +// Useful constants +const ABSTRACT_PRIOR_VALUE = 'abstractPriorValue'; // 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 @@ -29,172 +36,18 @@ ProtocolEngine = undefined; * @param protocolEngine An instantiated ProtocolEngine linked to a LayoutManager from the * Viewerbase package */ -HP.setEngine = function(protocolEngine) { +HP.setEngine = 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.create(null), function(element, optionValue) { +Meteor.startup(() => { + HP.addCustomViewportSetting('wlPreset', 'Window/Level Preset', Object.create(null), (element, optionValue) => { if (optionValue in OHIF.viewer.wlPresets) { 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 { /** * Constructor @@ -241,7 +94,8 @@ HP.ProtocolEngine = class ProtocolEngine { * Resets the ProtocolEngine to the best match */ reset() { - const protocol = this.getBestMatch(); + const protocol = this.getBestProtocolMatch(); + this.setHangingProtocol(protocol); } @@ -254,51 +108,112 @@ HP.ProtocolEngine = class ProtocolEngine { 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) { OHIF.log.info('ProtocolEngine::findMatchByStudy'); const matched = []; 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 => { // Clone the protocol's protocolMatchingRules array // We clone it so that we don't accidentally add the // numberOfPriorsReferenced rule to the Protocol itself. - const rules = protocol.protocolMatchingRules.slice(0); + let rules = protocol.protocolMatchingRules.slice(0); if (!rules) { return; } - // Set custom attribute for study metadata - const numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study.getObjectID()); - studyInstance.setCustomAttribute('numberOfPriorsReferenced', numberOfPriorsReferenced); + // To make sure the protocol has updated priors information. + // This is reasonable for Viewers that use different APIs to + // manage ProtocolStore or protocols. + if (!protocol.hasUpdatedPriorsInformation) { - const requiredPriorsRule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', { - numericality: { - greaterThanOrEqualTo: protocol.numberOfPriorsReferenced + // If protocol is not an instance of HP.Protocol + // make it be. + 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', { - numericality: { - equalTo: protocol.numberOfPriorsReferenced - }, - }, false, 1); + // Skip protocols that require more priors than the study has + const protocolRequiresPriors = this.protocolRequiresPriors(protocol); + if (protocolRequiresPriors && numberOfPriorsReferenced < protocol.numberOfPriorsReferenced) { + return; + } - 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({ - score: matchedDetails.score, - protocol: protocol + score, + protocol }); } }); + // If no matches were found, select the default protocol if (!matched.length) { const defaultProtocol = HP.ProtocolStore.getProtocol('defaultProtocol'); @@ -308,6 +223,7 @@ HP.ProtocolEngine = class ProtocolEngine { }]; } + // Sort the matched list by score sortByScore(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 */ - updateMatches() { - OHIF.log.info('ProtocolEngine::updateMatches'); + updateProtocolMatches() { + OHIF.log.info('ProtocolEngine::updateProtocolMatches'); // Clear all data from the MatchedProtocols Collection 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 (!protocolInCollection) { + OHIF.log.info('ProtocolEngine::updateProtocolMatches inserting protocol match', 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 * @returns {*} */ - getBestMatch() { + getBestProtocolMatch() { // Run the matching to populate the MatchedProtocols Collection - this.updateMatches(); + this.updateProtocolMatches(); // Retrieve the highest scoring Protocol const sorted = MatchedProtocols.find({}, { @@ -366,7 +283,8 @@ HP.ProtocolEngine = class ProtocolEngine { // Highest scoring Protocol const bestMatch = sorted[0].protocol; - OHIF.log.info('ProtocolEngine::getBestMatch bestMatch', bestMatch); + OHIF.log.info('ProtocolEngine::getBestProtocolMatch bestMatch', bestMatch); + return bestMatch; } @@ -378,6 +296,7 @@ HP.ProtocolEngine = class ProtocolEngine { */ getNumberOfAvailablePriors(studyObjectID) { const priors = this.getAvailableStudyPriors(studyObjectID); + return priors.length; } @@ -389,6 +308,7 @@ HP.ProtocolEngine = class ProtocolEngine { */ getAvailableStudyPriors(studyObjectID) { const priors = this.priorStudies.get(studyObjectID); + return priors instanceof Array ? priors : []; } @@ -407,7 +327,7 @@ HP.ProtocolEngine = class ProtocolEngine { if (prior instanceof StudyMetadata) { prior = prior.getFirstInstance(); } - const matchDetails = HP.match(prior, protocolMatchingRules); + const matchDetails = HPMatcher.match(prior, protocolMatchingRules); 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 - matchImages(viewport) { + matchImages(viewport, viewportIndex) { OHIF.log.info('ProtocolEngine::matchImages'); const { studyMatchingRules, seriesMatchingRules, imageMatchingRules: instanceMatchingRules } = viewport; @@ -431,13 +351,13 @@ HP.ProtocolEngine = class ProtocolEngine { let bestMatch; // 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) { - firstInstance.setCustomAttribute('abstractPriorValue', 0); + firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0); } studyMatchingRules.forEach(rule => { - if (rule.attribute === 'abstractPriorValue') { + if (rule.attribute === ABSTRACT_PRIOR_VALUE) { const validatorType = Object.keys(rule.constraint)[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) this.studyMetadataSource.loadStudy(priorStudy).then(studyMetadata => { // Set the custom attribute abstractPriorValue for the study metadata - studyMetadata.setCustomAttribute('abstractPriorValue', abstractPriorValue); + studyMetadata.setCustomAttribute(ABSTRACT_PRIOR_VALUE, abstractPriorValue); // Also add custom attribute const firstInstance = studyMetadata.getFirstInstance(); if (firstInstance instanceof InstanceMetadata) { - firstInstance.setCustomAttribute('abstractPriorValue', abstractPriorValue); + firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, abstractPriorValue); } // Insert the new study metadata this.studies.push(studyMetadata); // Update the viewport to refresh layout manager with new study - this.updateViewports(); + this.updateViewports(viewportIndex); }, error => { OHIF.log.warn(error); 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 => { - const studyMatchDetails = HP.match(study.getFirstInstance(), studyMatchingRules); + const studyMatchDetails = HPMatcher.match(study.getFirstInstance(), studyMatchingRules); if ((studyMatchingRules.length && !studyMatchDetails.score) || studyMatchDetails.score < highestStudyMatchingScore) { return; @@ -504,7 +424,7 @@ HP.ProtocolEngine = class ProtocolEngine { highestStudyMatchingScore = studyMatchDetails.score; study.forEachSeries(series => { - const seriesMatchDetails = HP.match(series.getFirstInstance(), seriesMatchingRules); + const seriesMatchDetails = HPMatcher.match(series.getFirstInstance(), seriesMatchingRules); if ((seriesMatchingRules.length && !seriesMatchDetails.score) || seriesMatchDetails.score < highestSeriesMatchingScore) { return; @@ -522,7 +442,7 @@ HP.ProtocolEngine = class ProtocolEngine { return; } - const instanceMatchDetails = HP.match(instance, instanceMatchingRules); + const instanceMatchDetails = HPMatcher.match(instance, instanceMatchingRules); const matchDetails = { passed: [], @@ -609,7 +529,7 @@ HP.ProtocolEngine = class ProtocolEngine { OHIF.log.info(`ProtocolEngine::updateViewports viewportIndex: ${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) { + if (!this.getNumProtocolStages()) { return; } @@ -649,7 +569,7 @@ HP.ProtocolEngine = class ProtocolEngine { // Loop through each viewport stageModel.viewports.forEach((viewport, viewportIndex) => { - const details = this.matchImages(viewport); + const details = this.matchImages(viewport, viewportIndex); this.matchDetails[viewportIndex] = details; @@ -747,7 +667,7 @@ HP.ProtocolEngine = class ProtocolEngine { * @param newProtocol * @param updateViewports */ - setHangingProtocol(newProtocol, updateViewports=true) { + setHangingProtocol(newProtocol, updateViewports = true) { OHIF.log.info('ProtocolEngine::setHangingProtocol newProtocol', newProtocol); OHIF.log.info(`ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`); @@ -783,30 +703,64 @@ HP.ProtocolEngine = class ProtocolEngine { 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 - * - * @param stage An integer value specifying the index of the desired Stage + /** + * Check if the next stage is available + * @return {Boolean} True if next stage is available or false otherwise */ - setCurrentProtocolStage(stage) { - OHIF.log.info(`ProtocolEngine::setCurrentProtocolStage stage = ${stage}`); + isNextStageAvailable() { + const numberOfStages = this.getNumProtocolStages(); - if (!this.protocol || !this.protocol.stages || !this.protocol.stages.length) { - return; - } + return this.stage + 1 < numberOfStages; + } - if (stage >= this.protocol.stages.length) { - return; - } - - this.stage = stage; - this.updateViewports(); + /** + * Check if the previous stage is available + * @return {Boolean} True if previous stage is available or false otherwise + */ + isPreviousStageAvailable() { + 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() { 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 */ 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 */ previousProtocolStage() { - this.setCurrentProtocolStage(--this.stage); + OHIF.log.info('ProtocolEngine::previousProtocolStage'); + + if (!this.setCurrentProtocolStage(-1)) { + // Just for logging purpose + OHIF.log.info('ProtocolEngine::previousProtocolStage failed'); + } } }; diff --git a/Packages/ohif-viewerbase/client/lib/viewportUtils.js b/Packages/ohif-viewerbase/client/lib/viewportUtils.js index d243c20cc..f4720484a 100644 --- a/Packages/ohif-viewerbase/client/lib/viewportUtils.js +++ b/Packages/ohif-viewerbase/client/lib/viewportUtils.js @@ -288,14 +288,19 @@ const stopAllClips = () => { const isStackScrollLinkingDisabled = () => { + let linkableViewportsCount = 0; + // Its called everytime active viewport and/or layout change Session.get('viewportActivated'); Session.get('LayoutManagerUpdated'); 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