From 35b855de560acc561742636616129832ba47a8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elo=C3=ADzio=20Salgado?= Date: Wed, 1 Mar 2017 16:41:32 -0300 Subject: [PATCH] Hanging protocol improvements on matching process and other improvements --- .../both/classes/Protocol.js | 243 +++++++ .../both/classes/Rule.js | 173 +++++ .../both/classes/Stage.js | 87 +++ .../both/classes/Viewport.js | 81 +++ .../both/classes/ViewportStructure.js | 53 ++ .../both/classes/rules/ImageMatchingRule.js | 9 + .../classes/rules/ProtocolMatchingRule.js | 9 + .../both/classes/rules/SeriesMatchingRule.js | 9 + .../both/classes/rules/StudyMatchingRule.js | 9 + .../both/lib/comparators.js | 72 +++ .../both/lib/removeFromArray.js | 33 + .../ohif-hanging-protocols/both/schema.js | 602 +----------------- .../ohif-hanging-protocols/both/testData.js | 80 +-- .../client/collections.js | 73 +-- .../client/matcher/HPMatcher.js | 2 +- .../client/protocolEngine.js | 107 +--- .../client/protocolStore/defaultStrategy.js | 2 +- .../client/protocolStore/protocolStore.js | 30 +- 18 files changed, 881 insertions(+), 793 deletions(-) create mode 100644 Packages/ohif-hanging-protocols/both/classes/Protocol.js create mode 100644 Packages/ohif-hanging-protocols/both/classes/Rule.js create mode 100644 Packages/ohif-hanging-protocols/both/classes/Stage.js create mode 100644 Packages/ohif-hanging-protocols/both/classes/Viewport.js create mode 100644 Packages/ohif-hanging-protocols/both/classes/ViewportStructure.js create mode 100644 Packages/ohif-hanging-protocols/both/classes/rules/ImageMatchingRule.js create mode 100644 Packages/ohif-hanging-protocols/both/classes/rules/ProtocolMatchingRule.js create mode 100644 Packages/ohif-hanging-protocols/both/classes/rules/SeriesMatchingRule.js create mode 100644 Packages/ohif-hanging-protocols/both/classes/rules/StudyMatchingRule.js create mode 100644 Packages/ohif-hanging-protocols/both/lib/comparators.js create mode 100644 Packages/ohif-hanging-protocols/both/lib/removeFromArray.js diff --git a/Packages/ohif-hanging-protocols/both/classes/Protocol.js b/Packages/ohif-hanging-protocols/both/classes/Protocol.js new file mode 100644 index 000000000..801e441bc --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/classes/Protocol.js @@ -0,0 +1,243 @@ +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; + +// Local imports +import { removeFromArray } from '../lib/removeFromArray'; + +/** + * This class represents a Hanging Protocol at the highest level + * + * @type {Protocol} + */ +HP.Protocol = class Protocol { + /** + * The Constructor for the Class to create a Protocol with the bare + * minimum information + * + * @param name The desired name for the Protocol + */ + constructor(name) { + // Create a new UUID for this Protocol + this.id = Random.id(); + + // Store a value which determines whether or not a Protocol is locked + // This is probably temporary, since we will eventually have role / user + // checks for editing. For now we just need it to prevent changes to the + // 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; + + // Set the created and modified dates to Now + this.createdDate = new Date(); + this.modifiedDate = new Date(); + + // If we are logged in while creating this Protocol, + // store this information as well + if (Meteor.users && Meteor.userId) { + this.createdBy = Meteor.userId; + this.modifiedBy = Meteor.userId; + } + + // Create two empty Sets specifying which roles + // have read and write access to this Protocol + this.availableTo = new Set(); + this.editableBy = new Set(); + + // Define empty arrays for the Protocol matching rules + // and Stages + this.protocolMatchingRules = []; + this.stages = []; + + // Define auxiliary values for priors + this.numberOfPriorsReferenced = -1; + } + + getNumberOfPriorsReferenced(skipCache = false) { + let numberOfPriorsReferenced = skipCache !== true ? this.numberOfPriorsReferenced : -1; + + // Check if information is cached already + if (numberOfPriorsReferenced > -1) { + return numberOfPriorsReferenced; + } + + numberOfPriorsReferenced = 0; + + // Search each study matching rule for prior rules + // Each stage can have many viewports that can have + // multiple study matching rules. + this.stages.forEach(stage => { + if (!stage.viewports) { + return; + } + + stage.viewports.forEach(viewport => { + if (!viewport.studyMatchingRules) { + return; + } + + viewport.studyMatchingRules.forEach(rule => { + // If the current rule is not a priors rule, it will return -1 then numberOfPriorsReferenced will continue to be 0 + const priorsReferenced = rule.getNumberOfPriorsReferenced(); + if (priorsReferenced > numberOfPriorsReferenced) { + numberOfPriorsReferenced = priorsReferenced; + } + }); + }); + }); + + this.numberOfPriorsReferenced = numberOfPriorsReferenced; + + return numberOfPriorsReferenced + } + + updateNumberOfPriorsReferenced() { + this.getNumberOfPriorsReferenced(true); + } + + /** + * Method to update the modifiedDate when the Protocol + * has been changed + */ + protocolWasModified() { + // If we are logged in while modifying this Protocol, + // store this information as well + if (Meteor.users && 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(); + + // Update the modifiedDate with the current Date/Time + this.modifiedDate = new Date(); + } + + /** + * Occasionally the Protocol class needs to be instantiated from a JavaScript Object + * containing the Protocol data. This function fills in a Protocol with the Object + * data. + * + * @param input A Protocol as a JavaScript Object, e.g. retrieved from MongoDB or JSON + */ + fromObject(input) { + // Check if the input already has an ID + // If so, keep it. It not, create a new UUID + this.id = input.id || Random.id(); + + // Assign the input name to the Protocol + this.name = input.name; + + // Retrieve locked status, use !! to make it truthy + // so that undefined values will be set to false + this.locked = !!input.locked; + + // TODO: Check how to regenerate Set from Object + //this.availableTo = new Set(input.availableTo); + //this.editableBy = new Set(input.editableBy); + + // If the input contains Protocol matching rules + if (input.protocolMatchingRules) { + input.protocolMatchingRules.forEach(ruleObject => { + // Create new Rules from the stored data + var rule = new HP.ProtocolMatchingRule(); + rule.fromObject(ruleObject); + + // Add them to the Protocol + this.protocolMatchingRules.push(rule); + }); + } + + // If the input contains data for various Stages in the + // display set sequence + if (input.stages) { + input.stages.forEach(stageObject => { + // Create Stages from the stored data + var stage = new HP.Stage(); + stage.fromObject(stageObject); + + // Add them to the Protocol + this.stages.push(stage); + }); + } + } + + /** + * Creates a clone of the current Protocol with a new name + * + * @param name + * @returns {Protocol|*} + */ + createClone(name) { + // Create a new JavaScript independent of the current Protocol + var currentProtocol = Object.assign({}, this); + + // Create a new Protocol to return + var clonedProtocol = new HP.Protocol(); + + // Apply the desired properties + currentProtocol.id = clonedProtocol.id; + clonedProtocol.fromObject(currentProtocol); + + // If we have specified a name, assign it + if (name) { + clonedProtocol.name = name; + } + + // Unlock the clone + clonedProtocol.locked = false; + + // Return the cloned Protocol + return clonedProtocol; + } + + /** + * Adds a Stage to this Protocol's display set sequence + * + * @param stage + */ + addStage(stage) { + this.stages.push(stage); + + // Update the modifiedDate and User that last + // modified this Protocol + this.protocolWasModified(); + } + + /** + * Adds a Rule to this Protocol's array of matching rules + * + * @param rule + */ + addProtocolMatchingRule(rule) { + this.protocolMatchingRules.push(rule); + + // Update the modifiedDate and User that last + // modified this Protocol + this.protocolWasModified(); + } + + /** + * Removes a Rule from this Protocol's array of matching rules + * + * @param rule + */ + removeProtocolMatchingRule(rule) { + var wasRemoved = removeFromArray(this.protocolMatchingRules, rule); + + // Update the modifiedDate and User that last + // modified this Protocol + if (wasRemoved) { + this.protocolWasModified(); + } + } +}; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/both/classes/Rule.js b/Packages/ohif-hanging-protocols/both/classes/Rule.js new file mode 100644 index 000000000..236242785 --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/classes/Rule.js @@ -0,0 +1,173 @@ +import { Random } from 'meteor/random'; + +import { comparators } from '../lib/comparators'; + +const EQUALS_REGEXP = /^equals$/; + +/** + * This Class represents a Rule to be evaluated given a set of attributes + * Rules have: + * - An attribute (e.g. 'seriesDescription') + * - A constraint Object, in the form required by Validate.js: + * + * rule.constraint = { + * contains: { + * value: 'T-1' + * } + * }; + * + * Note: In this example we use the 'contains' Validator, which is a custom Validator defined in Viewerbase + * + * - A value for whether or not they are Required to be matched (default: False) + * - A value for their relative weighting during Protocol or Image matching (default: 1) + */ +export class Rule { + /** + * The Constructor for the Class to create a Rule with the bare + * minimum information + * + * @param name The desired name for the Rule + */ + constructor(attribute, constraint, required, weight) { + // Create a new UUID for this Rule + this.id = Random.id(); + + // Set the Rule's weight (defaults to 1) + this.weight = weight || 1; + + // If an attribute is specified, assign it + if (attribute) { + this.attribute = attribute; + } + + // If a constraint is specified, assign it + if (constraint) { + this.constraint = constraint; + } + + // If a value for 'required' is specified, assign it + if (required === undefined) { + // If no value was specified, default to False + this.required = false; + } else { + this.required = required; + } + + // Cache for constraint info object + this._constraintInfo = void 0; + + // Cache for validator and value object + this._validatorAndValue = void 0; + } + + /** + * Occasionally the Rule class needs to be instantiated from a JavaScript Object. + * This function fills in a Protocol with the Object data. + * + * @param input A Rule as a JavaScript Object, e.g. retrieved from MongoDB or JSON + */ + fromObject(input) { + // Check if the input already has an ID + // If so, keep it. It not, create a new UUID + this.id = input.id || Random.id(); + + // Assign the specified input data to the Rule + this.required = input.required; + this.weight = input.weight; + this.attribute = input.attribute; + this.constraint = input.constraint; + } + + /** + * Get the constraint info object for the current constraint + * @return {Object\undefined} Constraint object or undefined if current constraint + * is not valid or not found in comparators list + */ + getConstraintInfo() { + let constraintInfo = this._constraintInfo; + // Check if info is cached already + if (constraintInfo !== void 0) { + return constraintInfo; + } + + const ruleConstraint = Object.keys(this.constraint)[0]; + + if (ruleConstraint !== void 0) { + constraintInfo = comparators.find(comparator => ruleConstraint === comparator.id) + } + + // Cache this information for later use + this._constraintInfo = constraintInfo; + + return constraintInfo; + } + + /** + * Check if current rule is related to priors + * @return {Boolean} True if a rule is related to priors or false otherwise + */ + isRuleForPrior() { + // @TODO: Should we check this too? this.attribute === 'relativeTime' + return this.attribute === 'abstractPriorValue'; + } + + /** + * If the current rule is a rule for priors, returns the number of referenced priors. Otherwise, returns -1. + * @return {Number} The number of referenced priors or -1 if not applicable. Returns zero if the actual value could not be determined. + */ + getNumberOfPriorsReferenced() { + if (!this.isRuleForPrior()) { + return -1; + } + + // Get rule's validator and value + const ruleValidatorAndValue = this.getConstraintValidatorAndValue(); + const { value, validator } = ruleValidatorAndValue; + const intValue = parseInt(value, 10) || 0; // avoid possible NaN + + // "Equal to" validators + if (EQUALS_REGEXP.test(validator)) { + // In this case, -1 (the oldest prior) indicates that at least one study is used + return intValue < 0 ? 1 : intValue; + } + + // Default cases return value + return 0; + } + + /** + * Get the constraint validator and value + * @return {Object|undefined} Returns an object containing the validator and it's value or undefined + */ + getConstraintValidatorAndValue() { + let validatorAndValue = this._validatorAndValue; + + // Check if validator and value are cached already + if (validatorAndValue !== void 0) { + return validatorAndValue; + } + + // Get the constraint info object + const constraintInfo = this.getConstraintInfo(); + + // Constraint info object exists and is valid + if (constraintInfo !== void 0) { + const validator = constraintInfo.validator; + const currentValidator = this.constraint[validator]; + + if (currentValidator) { + const constraintValidator = constraintInfo.validatorOption; + const constraintValue = currentValidator[constraintValidator]; + + validatorAndValue = { + value: constraintValue, + validator: constraintInfo.id + }; + + this._validatorAndValue = validatorAndValue; + } + } + + return validatorAndValue; + } +} diff --git a/Packages/ohif-hanging-protocols/both/classes/Stage.js b/Packages/ohif-hanging-protocols/both/classes/Stage.js new file mode 100644 index 000000000..8c0952095 --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/classes/Stage.js @@ -0,0 +1,87 @@ +import { Random } from 'meteor/random'; + +/** + * A Stage is one step in the Display Set Sequence for a Hanging Protocol + * + * Stages are defined as a ViewportStructure and an array of Viewports + * + * @type {Stage} + */ +HP.Stage = class Stage { + constructor(ViewportStructure, name) { + // Create a new UUID for this Stage + this.id = Random.id(); + + // Assign the name and ViewportStructure provided + this.name = name; + this.viewportStructure = ViewportStructure; + + // Create an empty array for the Viewports + this.viewports = []; + + // Set the created date to Now + this.createdDate = new Date(); + } + + /** + * Creates a clone of the current Stage with a new name + * + * Note! This method absolutely cannot be renamed 'clone', because + * Minimongo's insert method uses 'clone' internally and this + * somehow causes very bizarre behaviour + * + * @param name + * @returns {Stage|*} + */ + createClone(name) { + // Create a new JavaScript independent of the current Protocol + var currentStage = Object.assign({}, this); + + // Create a new Stage to return + var clonedStage = new HP.Stage(); + + // Assign the desired properties + currentStage.id = clonedStage.id; + clonedStage.fromObject(currentStage); + + // If we have specified a name, assign it + if (name) { + clonedStage.name = name; + } + + // Return the cloned Stage + return clonedStage; + } + + /** + * Occasionally the Stage class needs to be instantiated from a JavaScript Object. + * This function fills in a Protocol with the Object data. + * + * @param input A Stage as a JavaScript Object, e.g. retrieved from MongoDB or JSON + */ + fromObject(input) { + // Check if the input already has an ID + // If so, keep it. It not, create a new UUID + this.id = input.id || Random.id(); + + // Assign the input name to the Stage + this.name = input.name; + + // If a ViewportStructure is present in the input, add it from the + // input data + this.viewportStructure = new HP.ViewportStructure(); + this.viewportStructure.fromObject(input.viewportStructure); + + // If any viewports are present in the input object + if (input.viewports) { + input.viewports.forEach(viewportObject => { + // Create a new Viewport with their data + var viewport = new HP.Viewport(); + viewport.fromObject(viewportObject); + + // Add it to the viewports array + this.viewports.push(viewport); + }); + } + } +}; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/both/classes/Viewport.js b/Packages/ohif-hanging-protocols/both/classes/Viewport.js new file mode 100644 index 000000000..38fbaeff1 --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/classes/Viewport.js @@ -0,0 +1,81 @@ +// Local imports +import { removeFromArray } from '../lib/removeFromArray'; + +/** + * This Class defines a Viewport in the Hanging Protocol Stage. A Viewport contains + * arrays of Rules that are matched in the ProtocolEngine in order to determine which + * images should be hung. + * + * @type {Viewport} + */ +HP.Viewport = class Viewport { + constructor() { + this.viewportSettings = {}; + this.imageMatchingRules = []; + this.seriesMatchingRules = []; + this.studyMatchingRules = []; + } + + /** + * Occasionally the Viewport class needs to be instantiated from a JavaScript Object. + * This function fills in a Viewport with the Object data. + * + * @param input The Viewport as a JavaScript Object, e.g. retrieved from MongoDB or JSON + */ + fromObject(input) { + // If ImageMatchingRules exist, create them from the Object data + // and add them to the Viewport's imageMatchingRules array + if (input.imageMatchingRules) { + input.imageMatchingRules.forEach(ruleObject => { + var rule = new HP.ImageMatchingRule(); + rule.fromObject(ruleObject); + this.imageMatchingRules.push(rule); + }); + } + + // If SeriesMatchingRules exist, create them from the Object data + // and add them to the Viewport's seriesMatchingRules array + if (input.seriesMatchingRules) { + input.seriesMatchingRules.forEach(ruleObject => { + var rule = new HP.SeriesMatchingRule(); + rule.fromObject(ruleObject); + this.seriesMatchingRules.push(rule); + }); + } + + // If StudyMatchingRules exist, create them from the Object data + // and add them to the Viewport's studyMatchingRules array + if (input.studyMatchingRules) { + input.studyMatchingRules.forEach(ruleObject => { + var rule = new HP.StudyMatchingRule(); + rule.fromObject(ruleObject); + this.studyMatchingRules.push(rule); + }); + } + + // If ViewportSettings exist, add them to the current protocol + if (input.viewportSettings) { + this.viewportSettings = input.viewportSettings; + } + } + + /** + * Finds and removes a rule from whichever array it exists in. + * It is not required to specify if it exists in studyMatchingRules, + * seriesMatchingRules, or imageMatchingRules + * + * @param rule + */ + removeRule(rule) { + var array; + if (rule instanceof HP.StudyMatchingRule) { + array = this.studyMatchingRules; + } else if (rule instanceof HP.SeriesMatchingRule) { + array = this.seriesMatchingRules; + } else if (rule instanceof HP.ImageMatchingRule) { + array = this.imageMatchingRules; + } + + removeFromArray(array, rule); + } +}; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/both/classes/ViewportStructure.js b/Packages/ohif-hanging-protocols/both/classes/ViewportStructure.js new file mode 100644 index 000000000..df4416c90 --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/classes/ViewportStructure.js @@ -0,0 +1,53 @@ +/** + * The ViewportStructure class represents the layout and layout properties that + * Viewports are displayed in. ViewportStructure has a type, which corresponds to + * a layout template, and a set of properties, which depend on the type. + * + * @type {ViewportStructure} + */ +HP.ViewportStructure = class ViewportStructure { + constructor(type, properties) { + this.type = type; + this.properties = properties; + } + + /** + * Occasionally the ViewportStructure class needs to be instantiated from a JavaScript Object. + * This function fills in a ViewportStructure with the Object data. + * + * @param input The ViewportStructure as a JavaScript Object, e.g. retrieved from MongoDB or JSON + */ + fromObject(input) { + this.type = input.type; + this.properties = input.properties; + } + + /** + * Retrieve the layout template name based on the layout type + * + * @returns {string} + */ + getLayoutTemplateName() { + // Viewport structure can be updated later when we build more complex display layouts + switch (this.type) { + case 'grid': + return 'gridLayout'; + } + } + + /** + * Retrieve the number of Viewports required for this layout + * given the layout type and properties + * + * @returns {string} + */ + getNumViewports() { + // Viewport structure can be updated later when we build more complex display layouts + switch (this.type) { + case 'grid': + // For the typical grid layout, we only need to multiply rows by columns to + // obtain the number of viewports + return this.properties.rows * this.properties.columns; + } + } +}; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/both/classes/rules/ImageMatchingRule.js b/Packages/ohif-hanging-protocols/both/classes/rules/ImageMatchingRule.js new file mode 100644 index 000000000..492b939cf --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/classes/rules/ImageMatchingRule.js @@ -0,0 +1,9 @@ +import { Rule } from '../Rule'; + +/** + * The ImageMatchingRule class extends the Rule Class. + * + * At present it does not add any new methods or attributes + * @type {ImageMatchingRule} + */ +HP.ImageMatchingRule = class ImageMatchingRule extends Rule {}; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/both/classes/rules/ProtocolMatchingRule.js b/Packages/ohif-hanging-protocols/both/classes/rules/ProtocolMatchingRule.js new file mode 100644 index 000000000..7ee5174fc --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/classes/rules/ProtocolMatchingRule.js @@ -0,0 +1,9 @@ +import { Rule } from '../Rule'; + +/** + * The ProtocolMatchingRule Class extends the Rule Class. + * + * At present it does not add any new methods or attributes + * @type {ProtocolMatchingRule} + */ +HP.ProtocolMatchingRule = class ProtocolMatchingRule extends Rule {}; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/both/classes/rules/SeriesMatchingRule.js b/Packages/ohif-hanging-protocols/both/classes/rules/SeriesMatchingRule.js new file mode 100644 index 000000000..8dd3f4488 --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/classes/rules/SeriesMatchingRule.js @@ -0,0 +1,9 @@ +import { Rule } from '../Rule'; + +/** + * The SeriesMatchingRule Class extends the Rule Class. + * + * At present it does not add any new methods or attributes + * @type {SeriesMatchingRule} + */ +HP.SeriesMatchingRule = class SeriesMatchingRule extends Rule {}; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/both/classes/rules/StudyMatchingRule.js b/Packages/ohif-hanging-protocols/both/classes/rules/StudyMatchingRule.js new file mode 100644 index 000000000..7ff7f7b53 --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/classes/rules/StudyMatchingRule.js @@ -0,0 +1,9 @@ +import { Rule } from '../Rule'; + +/** + * The StudyMatchingRule Class extends the Rule Class. + * + * At present it does not add any new methods or attributes + * @type {StudyMatchingRule} + */ +HP.StudyMatchingRule = class StudyMatchingRule extends Rule {}; diff --git a/Packages/ohif-hanging-protocols/both/lib/comparators.js b/Packages/ohif-hanging-protocols/both/lib/comparators.js new file mode 100644 index 000000000..4a6e2da66 --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/lib/comparators.js @@ -0,0 +1,72 @@ +const comparators = [{ + id: 'equals', + name: '= (Equals)', + validator: 'equals', + validatorOption: 'value', + description: 'The attribute must equal this value.' +}, { + id: 'doesNotEqual', + name: '!= (Does not equal)', + validator: 'doesNotEqual', + validatorOption: 'value', + description: 'The attribute must not equal this value.' +}, { + id: 'contains', + name: 'Contains', + validator: 'contains', + validatorOption: 'value', + description: 'The attribute must contain this value.' +}, { + id: 'doesNotContain', + name: 'Does not contain', + validator: 'doesNotContain', + validatorOption: 'value', + description: 'The attribute must not contain this value.' +}, { + id: 'onlyInteger', + name: 'Only Integers', + validator: 'numericality', + validatorOption: 'onlyInteger', + description: "Real numbers won't be allowed." +}, { + id: 'greaterThan', + name: '> (Greater than)', + validator: 'numericality', + validatorOption: 'greaterThan', + description: 'The attribute has to be greater than this value.' +}, { + id: 'greaterThanOrEqualTo', + name: '>= (Greater than or equal to)', + validator: 'numericality', + validatorOption: 'greaterThanOrEqualTo', + description: 'The attribute has to be at least this value.' +}, { + id: 'lessThanOrEqualTo', + name: '<= (Less than or equal to)', + validator: 'numericality', + validatorOption: 'lessThanOrEqualTo', + description: 'The attribute can be this value at the most.' +}, { + id: 'lessThan', + name: '< (Less than)', + validator: 'numericality', + validatorOption: 'lessThan', + description: 'The attribute has to be less than this value.' +}, { + id: 'odd', + name: 'Odd', + validator: 'numericality', + validatorOption: 'odd', + description: 'The attribute has to be odd.' +}, { + id: 'even', + name: 'Even', + validator: 'numericality', + validatorOption: 'even', + description: 'The attribute has to be even.' +}]; + +// Immutable object +Object.freeze(comparators); + +export { comparators } \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/both/lib/removeFromArray.js b/Packages/ohif-hanging-protocols/both/lib/removeFromArray.js new file mode 100644 index 000000000..4bcfdda05 --- /dev/null +++ b/Packages/ohif-hanging-protocols/both/lib/removeFromArray.js @@ -0,0 +1,33 @@ +import { _ } from 'meteor/underscore'; + +/** + * Removes the first instance of an element from an array, if an equal value exists + * + * @param array + * @param input + * + * @returns {boolean} Whether or not the element was found and removed + */ +const removeFromArray = (array, input) => { + // If the array is empty, stop here + if (!array || + !array.length) { + return false; + } + + array.forEach((value, index) => { + if (_.isEqual(value, input)) { + indexToRemove = index; + return false; + } + }); + + if (indexToRemove === void 0) { + return false; + } + + array.splice(indexToRemove, 1); + return true; +}; + +export { removeFromArray }; \ No newline at end of file diff --git a/Packages/ohif-hanging-protocols/both/schema.js b/Packages/ohif-hanging-protocols/both/schema.js index d0e54fb61..5da30557f 100644 --- a/Packages/ohif-hanging-protocols/both/schema.js +++ b/Packages/ohif-hanging-protocols/both/schema.js @@ -1,589 +1,13 @@ -/** - * Removes the first instance of an element from an array, if an equal value exists - * - * @param array - * @param input - * - * @returns {boolean} Whether or not the element was found and removed - */ -function removeFromArray(array, input) { - // If the array is empty, stop here - if (!array || - !array.length) { - return false; - } - - array.forEach(function(value, index) { - if (_.isEqual(value, input)) { - indexToRemove = index; - return false; - } - }); - - if (indexToRemove === undefined) { - return false; - } - - array.splice(indexToRemove, 1); - return true; -} - -/** - * This class represents a Hanging Protocol at the highest level - * - * @type {Protocol} - */ -HP.Protocol = class Protocol { - /** - * The Constructor for the Class to create a Protocol with the bare - * minimum information - * - * @param name The desired name for the Protocol - */ - constructor(name) { - // Create a new UUID for this Protocol - this.id = Random.id(); - - // Store a value which determines whether or not a Protocol is locked - // This is probably temporary, since we will eventually have role / user - // checks for editing. For now we just need it to prevent changes to the - // 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; - - // Set the created and modified dates to Now - this.createdDate = new Date(); - this.modifiedDate = new Date(); - - // If we are logged in while creating this Protocol, - // store this information as well - if (Meteor.users && Meteor.userId) { - this.createdBy = Meteor.userId; - this.modifiedBy = Meteor.userId; - } - - // Create two empty Sets specifying which roles - // have read and write access to this Protocol - this.availableTo = new Set(); - this.editableBy = new Set(); - - // Define empty arrays for the Protocol matching rules - // and Stages - this.protocolMatchingRules = []; - this.stages = []; - - // Define auxiliary values for priors - this.numberOfPriorsReferenced = 0; - this.numberOfPriorsReferencedRequired = 0; - } - - updateNumberOfPriorsReferenced() { - let numPriorsReferenced = 0; - let numberOfPriorsReferencedRequired = 0; - - this.stages.forEach(stage => { - if (!stage.viewports) { - return; - } - - stage.viewports.forEach(viewport => { - if (!viewport.studyMatchingRules) { - return; - } - - 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++; - } - }); - }); - }); - - this.numberOfPriorsReferenced = numPriorsReferenced; - this.numberOfPriorsReferencedRequired = numberOfPriorsReferencedRequired; - - // To indicate that the priors informations were updated - this.hasUpdatedPriorsInformation = true; - } - - /** - * Method to update the modifiedDate when the Protocol - * has been changed - */ - protocolWasModified() { - // If we are logged in while modifying this Protocol, - // store this information as well - if (Meteor.users && 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(); - - // Update the modifiedDate with the current Date/Time - this.modifiedDate = new Date(); - } - - /** - * Occasionally the Protocol class needs to be instantiated from a JavaScript Object - * containing the Protocol data. This function fills in a Protocol with the Object - * data. - * - * @param input A Protocol as a JavaScript Object, e.g. retrieved from MongoDB or JSON - */ - fromObject(input) { - // Check if the input already has an ID - // If so, keep it. It not, create a new UUID - this.id = input.id || Random.id(); - - // Assign the input name to the Protocol - this.name = input.name; - - // Retrieve locked status, use !! to make it truthy - // so that undefined values will be set to false - this.locked = !!input.locked; - - // TODO: Check how to regenerate Set from Object - //this.availableTo = new Set(input.availableTo); - //this.editableBy = new Set(input.editableBy); - - // If the input contains Protocol matching rules - if (input.protocolMatchingRules) { - input.protocolMatchingRules.forEach(ruleObject => { - // Create new Rules from the stored data - var rule = new HP.ProtocolMatchingRule(); - rule.fromObject(ruleObject); - - // Add them to the Protocol - this.protocolMatchingRules.push(rule); - }); - } - - // If the input contains data for various Stages in the - // display set sequence - if (input.stages) { - input.stages.forEach(stageObject => { - // Create Stages from the stored data - var stage = new HP.Stage(); - stage.fromObject(stageObject); - - // Add them to the Protocol - this.stages.push(stage); - }); - } - } - - /** - * Creates a clone of the current Protocol with a new name - * - * @param name - * @returns {Protocol|*} - */ - createClone(name) { - // Create a new JavaScript independent of the current Protocol - var currentProtocol = $.extend({}, this); - - // Create a new Protocol to return - var clonedProtocol = new HP.Protocol(); - - // Apply the desired properties - currentProtocol.id = clonedProtocol.id; - clonedProtocol.fromObject(currentProtocol); - - // If we have specified a name, assign it - if (name) { - clonedProtocol.name = name; - } - - // Unlock the clone - clonedProtocol.locked = false; - - // Return the cloned Protocol - return clonedProtocol; - } - - /** - * Adds a Stage to this Protocol's display set sequence - * - * @param stage - */ - addStage(stage) { - this.stages.push(stage); - - // Update the modifiedDate and User that last - // modified this Protocol - this.protocolWasModified(); - } - - /** - * Adds a Rule to this Protocol's array of matching rules - * - * @param rule - */ - addProtocolMatchingRule(rule) { - this.protocolMatchingRules.push(rule); - - // Update the modifiedDate and User that last - // modified this Protocol - this.protocolWasModified(); - } - - /** - * Removes a Rule from this Protocol's array of matching rules - * - * @param rule - */ - removeProtocolMatchingRule(rule) { - var wasRemoved = removeFromArray(this.protocolMatchingRules, rule); - - // Update the modifiedDate and User that last - // modified this Protocol - if (wasRemoved) { - this.protocolWasModified(); - } - } -}; - -/** - * This Class represents a Rule to be evaluated given a set of attributes - * Rules have: - * - An attribute (e.g. 'seriesDescription') - * - A constraint Object, in the form required by Validate.js: - * - * rule.constraint = { - * contains: { - * value: 'T-1' - * } - * }; - * - * Note: In this example we use the 'contains' Validator, which is a custom Validator defined in Viewerbase - * - * - A value for whether or not they are Required to be matched (default: False) - * - A value for their relative weighting during Protocol or Image matching (default: 1) - */ -class Rule { - /** - * The Constructor for the Class to create a Rule with the bare - * minimum information - * - * @param name The desired name for the Rule - */ - constructor(attribute, constraint, required, weight) { - // Create a new UUID for this Rule - this.id = Random.id(); - - // Set the Rule's weight (defaults to 1) - this.weight = weight || 1; - - // If an attribute is specified, assign it - if (attribute) { - this.attribute = attribute; - } - - // If a constraint is specified, assign it - if (constraint) { - this.constraint = constraint; - } - - // If a value for 'required' is specified, assign it - if (required === undefined) { - // If no value was specified, default to False - this.required = false; - } else { - this.required = required; - } - } - - /** - * Occasionally the Rule class needs to be instantiated from a JavaScript Object. - * This function fills in a Protocol with the Object data. - * - * @param input A Rule as a JavaScript Object, e.g. retrieved from MongoDB or JSON - */ - fromObject(input) { - // Check if the input already has an ID - // If so, keep it. It not, create a new UUID - this.id = input.id || Random.id(); - - // Assign the specified input data to the Rule - this.required = input.required; - this.weight = input.weight; - this.attribute = input.attribute; - this.constraint = input.constraint; - } -} - -/** - * The ProtocolMatchingRule Class extends the Rule Class. - * - * At present it does not add any new methods or attributes - * @type {ProtocolMatchingRule} - */ -HP.ProtocolMatchingRule = class ProtocolMatchingRule extends Rule {}; - -/** - * A Stage is one step in the Display Set Sequence for a Hanging Protocol - * - * Stages are defined as a ViewportStructure and an array of Viewports - * - * @type {Stage} - */ -HP.Stage = class Stage { - constructor(ViewportStructure, name) { - // Create a new UUID for this Stage - this.id = Random.id(); - - // Assign the name and ViewportStructure provided - this.name = name; - this.viewportStructure = ViewportStructure; - - // Create an empty array for the Viewports - this.viewports = []; - - // Set the created date to Now - this.createdDate = new Date(); - } - - /** - * Creates a clone of the current Stage with a new name - * - * Note! This method absolutely cannot be renamed 'clone', because - * Minimongo's insert method uses 'clone' internally and this - * somehow causes very bizarre behaviour - * - * @param name - * @returns {Stage|*} - */ - createClone(name) { - // Create a new JavaScript independent of the current Protocol - var currentStage = $.extend({}, this); - - // Create a new Stage to return - var clonedStage = new HP.Stage(); - - // Assign the desired properties - currentStage.id = clonedStage.id; - clonedStage.fromObject(currentStage); - - // If we have specified a name, assign it - if (name) { - clonedStage.name = name; - } - - // Return the cloned Stage - return clonedStage; - } - - /** - * Occasionally the Stage class needs to be instantiated from a JavaScript Object. - * This function fills in a Protocol with the Object data. - * - * @param input A Stage as a JavaScript Object, e.g. retrieved from MongoDB or JSON - */ - fromObject(input) { - // Check if the input already has an ID - // If so, keep it. It not, create a new UUID - this.id = input.id || Random.id(); - - // Assign the input name to the Stage - this.name = input.name; - - // If a ViewportStructure is present in the input, add it from the - // input data - this.viewportStructure = new HP.ViewportStructure(); - this.viewportStructure.fromObject(input.viewportStructure); - - // If any viewports are present in the input object - if (input.viewports) { - input.viewports.forEach(viewportObject => { - // Create a new Viewport with their data - var viewport = new HP.Viewport(); - viewport.fromObject(viewportObject); - - // Add it to the viewports array - this.viewports.push(viewport); - }); - } - } -}; - -/** - * The ViewportStructure class represents the layout and layout properties that - * Viewports are displayed in. ViewportStructure has a type, which corresponds to - * a layout template, and a set of properties, which depend on the type. - * - * @type {ViewportStructure} - */ -HP.ViewportStructure = class ViewportStructure { - constructor(type, properties) { - this.type = type; - this.properties = properties; - } - - /** - * Occasionally the ViewportStructure class needs to be instantiated from a JavaScript Object. - * This function fills in a ViewportStructure with the Object data. - * - * @param input The ViewportStructure as a JavaScript Object, e.g. retrieved from MongoDB or JSON - */ - fromObject(input) { - this.type = input.type; - this.properties = input.properties; - } - - /** - * Retrieve the layout template name based on the layout type - * - * @returns {string} - */ - getLayoutTemplateName() { - // Viewport structure can be updated later when we build more complex display layouts - switch (this.type) { - case 'grid': - return 'gridLayout'; - } - } - - /** - * Retrieve the number of Viewports required for this layout - * given the layout type and properties - * - * @returns {string} - */ - getNumViewports() { - // Viewport structure can be updated later when we build more complex display layouts - switch (this.type) { - case 'grid': - // For the typical grid layout, we only need to multiply rows by columns to - // obtain the number of viewports - return this.properties.rows * this.properties.columns; - } - } -}; - -/** - * This Class defines a Viewport in the Hanging Protocol Stage. A Viewport contains - * arrays of Rules that are matched in the ProtocolEngine in order to determine which - * images should be hung. - * - * @type {Viewport} - */ -HP.Viewport = class Viewport { - constructor() { - this.viewportSettings = {}; - this.imageMatchingRules = []; - this.seriesMatchingRules = []; - this.studyMatchingRules = []; - } - - /** - * Occasionally the Viewport class needs to be instantiated from a JavaScript Object. - * This function fills in a Viewport with the Object data. - * - * @param input The Viewport as a JavaScript Object, e.g. retrieved from MongoDB or JSON - */ - fromObject(input) { - // If ImageMatchingRules exist, create them from the Object data - // and add them to the Viewport's imageMatchingRules array - if (input.imageMatchingRules) { - input.imageMatchingRules.forEach(ruleObject => { - var rule = new HP.ImageMatchingRule(); - rule.fromObject(ruleObject); - this.imageMatchingRules.push(rule); - }); - } - - // If SeriesMatchingRules exist, create them from the Object data - // and add them to the Viewport's seriesMatchingRules array - if (input.seriesMatchingRules) { - input.seriesMatchingRules.forEach(ruleObject => { - var rule = new HP.SeriesMatchingRule(); - rule.fromObject(ruleObject); - this.seriesMatchingRules.push(rule); - }); - } - - // If StudyMatchingRules exist, create them from the Object data - // and add them to the Viewport's studyMatchingRules array - if (input.studyMatchingRules) { - input.studyMatchingRules.forEach(ruleObject => { - var rule = new HP.StudyMatchingRule(); - rule.fromObject(ruleObject); - this.studyMatchingRules.push(rule); - }); - } - - // If ViewportSettings exist, add them to the current protocol - if (input.viewportSettings) { - this.viewportSettings = input.viewportSettings; - } - } - - /** - * Finds and removes a rule from whichever array it exists in. - * It is not required to specify if it exists in studyMatchingRules, - * seriesMatchingRules, or imageMatchingRules - * - * @param rule - */ - removeRule(rule) { - var array; - if (rule instanceof HP.StudyMatchingRule) { - array = this.studyMatchingRules; - } else if (rule instanceof HP.SeriesMatchingRule) { - array = this.seriesMatchingRules; - } else if (rule instanceof HP.ImageMatchingRule) { - array = this.imageMatchingRules; - } - - removeFromArray(array, rule); - } -}; - -/** - * The ImageMatchingRule class extends the Rule Class. - * - * At present it does not add any new methods or attributes - * @type {ImageMatchingRule} - */ -HP.ImageMatchingRule = class ImageMatchingRule extends Rule {}; - -/** - * The SeriesMatchingRule Class extends the Rule Class. - * - * At present it does not add any new methods or attributes - * @type {SeriesMatchingRule} - */ -HP.SeriesMatchingRule = class SeriesMatchingRule extends Rule {}; - -/** - * The StudyMatchingRule Class extends the Rule Class. - * - * At present it does not add any new methods or attributes - * @type {StudyMatchingRule} - */ -HP.StudyMatchingRule = class StudyMatchingRule extends Rule {}; +// @TODO start using namespace instead + +// Base classes +import './classes/Protocol'; +import './classes/Stage'; +import './classes/Viewport'; +import './classes/ViewportStructure'; + +// Specialized Rule classes +import './classes/rules/ProtocolMatchingRule'; +import './classes/rules/StudyMatchingRule'; +import './classes/rules/SeriesMatchingRule'; +import './classes/rules/ImageMatchingRule'; diff --git a/Packages/ohif-hanging-protocols/both/testData.js b/Packages/ohif-hanging-protocols/both/testData.js index 87dba06e8..30359c7ae 100644 --- a/Packages/ohif-hanging-protocols/both/testData.js +++ b/Packages/ohif-hanging-protocols/both/testData.js @@ -416,7 +416,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL56z7mac", "weight": 1, "required": false, "attribute": "x0008103e", @@ -431,7 +431,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "ygz4nb28iJZcJhnYa", + "id": "ygz4nb28iJZcJhnYc", "weight": 1, "required": false, "attribute": "x0008103e", @@ -442,7 +442,7 @@ function getDemoProtocols() { } }], "studyMatchingRules": [{ - "id": "uDoEgLTvnXTByWnPz", + "id": "uDoEgLTvnXTByWnPt", "weight": 1, "required": false, "attribute": "abstractPriorValue", @@ -540,7 +540,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL56z7mtr", "weight": 1, "required": false, "attribute": "x0008103e", @@ -567,7 +567,7 @@ function getDemoProtocols() { }, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "ygz4nb28iJZcJhnYa", + "id": "ygz4nb28iJZcJhnYb", "weight": 2, "required": false, "attribute": "x0008103e", @@ -704,7 +704,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL55z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -733,7 +733,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL56z7nTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -743,7 +743,7 @@ function getDemoProtocols() { } } }, { - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL56z7rTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -758,7 +758,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL56r7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -768,7 +768,7 @@ function getDemoProtocols() { } } }, { - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL56a7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -783,7 +783,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcRzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -793,7 +793,7 @@ function getDemoProtocols() { } } }, { - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzTL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -808,7 +808,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcMzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -818,7 +818,7 @@ function getDemoProtocols() { } } }, { - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcAzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -873,7 +873,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcAzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -888,7 +888,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZR56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -898,7 +898,7 @@ function getDemoProtocols() { } } }, { - "id": "mXnsCcNzZL56z7mTZ", + "id": "mRnsCcNzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x00200011", @@ -926,7 +926,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsGcNzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -941,7 +941,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsHcNzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -971,7 +971,7 @@ function getDemoProtocols() { }, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXneCcNzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -986,7 +986,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCuNzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1041,7 +1041,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL59z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1056,7 +1056,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL56z7lTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1067,7 +1067,7 @@ function getDemoProtocols() { } }], "studyMatchingRules": [{ - "id": "uDoEgLTvnXTByWnPz", + "id": "uDoEgLTbnXTByWnPz", "weight": 1, "required": false, "attribute": "abstractPriorValue", @@ -1094,7 +1094,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNjZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1104,7 +1104,7 @@ function getDemoProtocols() { } } }, { - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL56z7gTZ", "weight": 1, "required": false, "attribute": "x00200011", @@ -1119,7 +1119,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcCzZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1140,7 +1140,7 @@ function getDemoProtocols() { } }], "studyMatchingRules": [{ - "id": "uDoEgLTvnXTByWnPz", + "id": "uDoEgLTvn1TByWnPz", "weight": 1, "required": false, "attribute": "abstractPriorValue", @@ -1167,7 +1167,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL26z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1182,7 +1182,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL46z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1197,7 +1197,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL57z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1208,7 +1208,7 @@ function getDemoProtocols() { } }], "studyMatchingRules": [{ - "id": "uDoEgLTvnXTByWnPz", + "id": "uDoEgLTvnYTByWnPz", "weight": 1, "required": false, "attribute": "abstractPriorValue", @@ -1222,7 +1222,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZQ56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1233,7 +1233,7 @@ function getDemoProtocols() { } }], "studyMatchingRules": [{ - "id": "uDoEgLTvnXTByWnPz", + "id": "uDoEgLTvnKTByWnPz", "weight": 1, "required": false, "attribute": "abstractPriorValue", @@ -1262,7 +1262,7 @@ function getDemoProtocols() { }, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZL56z7nTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1277,7 +1277,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNxZL56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1294,7 +1294,7 @@ function getDemoProtocols() { }, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZA56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1305,7 +1305,7 @@ function getDemoProtocols() { } }], "studyMatchingRules": [{ - "id": "uDoEgLTvnXTByWnPz", + "id": "uDoEgHTvnXTByWnPz", "weight": 1, "required": false, "attribute": "abstractPriorValue", @@ -1319,7 +1319,7 @@ function getDemoProtocols() { "viewportSettings": {}, "imageMatchingRules": [], "seriesMatchingRules": [{ - "id": "mXnsCcNzZL56z7mTZ", + "id": "mXnsCcNzZP56z7mTZ", "weight": 1, "required": false, "attribute": "x0008103e", @@ -1330,7 +1330,7 @@ function getDemoProtocols() { } }], "studyMatchingRules": [{ - "id": "uDoEgLTvnXTByWnPz", + "id": "uDoEgITvnXTByWnPz", "weight": 1, "required": false, "attribute": "abstractPriorValue", diff --git a/Packages/ohif-hanging-protocols/client/collections.js b/Packages/ohif-hanging-protocols/client/collections.js index d5430b0f2..132256fe3 100644 --- a/Packages/ohif-hanging-protocols/client/collections.js +++ b/Packages/ohif-hanging-protocols/client/collections.js @@ -1,77 +1,12 @@ +import { Meteor } from 'meteor/meteor'; +import { comparators } from '../both/lib/comparators'; + MatchedProtocols = new Meteor.Collection(null); MatchedProtocols._debugName = 'MatchedProtocols'; Comparators = new Meteor.Collection(null); Comparators._debugName = 'Comparators'; -var comparators = [{ - id: 'equals', - name: '= (Equals)', - validator: 'equals', - validatorOption: 'value', - description: 'The attribute must equal this value.' -}, { - id: 'doesNotEqual', - name: '!= (Does not equal)', - validator: 'doesNotEqual', - validatorOption: 'value', - description: 'The attribute must not equal this value.' -}, { - id: 'contains', - name: 'Contains', - validator: 'contains', - validatorOption: 'value', - description: 'The attribute must contain this value.' -}, { - id: 'doesNotContain', - name: 'Does not contain', - validator: 'doesNotContain', - validatorOption: 'value', - description: 'The attribute must not contain this value.' -}, { - id: 'onlyInteger', - name: 'Only Integers', - validator: 'numericality', - validatorOption: 'onlyInteger', - description: "Real numbers won't be allowed." -}, { - id: 'greaterThan', - name: '> (Greater than)', - validator: 'numericality', - validatorOption: 'greaterThan', - description: 'The attribute has to be greater than this value.' -}, { - id: 'greaterThanOrEqualTo', - name: '>= (Greater than or equal to)', - validator: 'numericality', - validatorOption: 'greaterThanOrEqualTo', - description: 'The attribute has to be at least this value.' -}, { - id: 'lessThanOrEqualTo', - name: '<= (Less than or equal to)', - validator: 'numericality', - validatorOption: 'lessThanOrEqualTo', - description: 'The attribute can be this value at the most.' -}, { - id: 'lessThan', - name: '< (Less than)', - validator: 'numericality', - validatorOption: 'lessThan', - description: 'The attribute has to be less than this value.' -}, { - id: 'odd', - name: 'Odd', - validator: 'numericality', - validatorOption: 'odd', - description: 'The attribute has to be odd.' -}, { - id: 'even', - name: 'Even', - validator: 'numericality', - validatorOption: 'even', - description: 'The attribute has to be even.' -}]; - -comparators.forEach(function(item) { +comparators.forEach(item => { Comparators.insert(item); }); diff --git a/Packages/ohif-hanging-protocols/client/matcher/HPMatcher.js b/Packages/ohif-hanging-protocols/client/matcher/HPMatcher.js index 16dda502b..c6ca73698 100644 --- a/Packages/ohif-hanging-protocols/client/matcher/HPMatcher.js +++ b/Packages/ohif-hanging-protocols/client/matcher/HPMatcher.js @@ -71,7 +71,7 @@ const match = (metadataInstance, rules) => { // If no errorMessages were returned, then validation passed. // Add the rule's weight to the total score - score += rule.weight; + score += parseInt(rule.weight, 10); // Log that this rule passed in the matching details object details.passed.push({ diff --git a/Packages/ohif-hanging-protocols/client/protocolEngine.js b/Packages/ohif-hanging-protocols/client/protocolEngine.js index 7663740d6..903af573c 100644 --- a/Packages/ohif-hanging-protocols/client/protocolEngine.js +++ b/Packages/ohif-hanging-protocols/client/protocolEngine.js @@ -108,44 +108,6 @@ 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 @@ -161,45 +123,23 @@ HP.ProtocolEngine = class ProtocolEngine { const studyInstance = study.getFirstInstance(); // Set custom attribute for study metadata - const numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study.getObjectID()); - studyInstance.setCustomAttribute('numberOfPriorsReferenced', numberOfPriorsReferenced); + const numberOfAvailablePriors = this.getNumberOfAvailablePriors(study.getObjectID()); 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. - let rules = protocol.protocolMatchingRules.slice(0); + let rules = protocol.protocolMatchingRules.slice(); if (!rules) { return; } - // 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) { - - // 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; - } - - protocol.updateNumberOfPriorsReferenced(); - } - - // Skip protocols that require more priors than the study has - const protocolRequiresPriors = this.protocolRequiresPriors(protocol); - if (protocolRequiresPriors && numberOfPriorsReferenced < protocol.numberOfPriorsReferenced) { + // Check if the study has the minimun number of priors used by the protocol. + const numberOfPriorsReferenced = protocol.getNumberOfPriorsReferenced(); + if (numberOfPriorsReferenced > numberOfAvailablePriors) { return; } - // Get additional rules for number of priors referenced - const priorsRules = this.getNumberOfPriorsRules(protocol.numberOfPriorsReferenced); - // Concatenate rules - rules = rules.concat(priorsRules); - // Run the matcher and get matching details const matchedDetails = HPMatcher.match(studyInstance, rules); const score = matchedDetails.score; @@ -312,29 +252,6 @@ HP.ProtocolEngine = class ProtocolEngine { return priors instanceof Array ? priors : []; } - /** - * Get the array of prior studies based on current protocol matching rules. - * - * @param {String} studyObjectID The study object ID of the study whose priors are needed - * @returns {Array} The array of available priors that match the given rules or an empty array - */ - getPriorsByProtocolMatchingRules(studyObjectID) { - const allPriors = this.getAvailableStudyPriors(studyObjectID); - const protocolMatchingRules = this.protocol.protocolMatchingRules; - - if (protocolMatchingRules instanceof Array && protocolMatchingRules.length > 0) { - return allPriors.filter(prior => { - if (prior instanceof StudyMetadata) { - prior = prior.getFirstInstance(); - } - const matchDetails = HPMatcher.match(prior, protocolMatchingRules); - return matchDetails.score > 0; - }); - } - - return allPriors; - } - // Match images given a list of Studies and a Viewport's image matching reqs matchImages(viewport, viewportIndex) { OHIF.log.info('ProtocolEngine::matchImages'); @@ -342,7 +259,7 @@ HP.ProtocolEngine = class ProtocolEngine { const { studyMatchingRules, seriesMatchingRules, imageMatchingRules: instanceMatchingRules } = viewport; const matchingScores = []; - const currentStudy = this.studies[0]; + const currentStudy = this.studies[0]; // @TODO: Should this be: this.studies[this.currentStudy] ??? const firstInstance = currentStudy.getFirstInstance(); let highestStudyMatchingScore = 0; @@ -356,6 +273,9 @@ HP.ProtocolEngine = class ProtocolEngine { firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0); } + // Only used if study matching rules has abstract prior values defined... + let priorStudies; + studyMatchingRules.forEach(rule => { if (rule.attribute === ABSTRACT_PRIOR_VALUE) { const validatorType = Object.keys(rule.constraint)[0]; @@ -365,17 +285,20 @@ HP.ProtocolEngine = class ProtocolEngine { abstractPriorValue = parseInt(abstractPriorValue, 10); // TODO: Restrict or clarify validators for abstractPriorValue? - const studies = this.getPriorsByProtocolMatchingRules(currentStudy.getObjectID()); + // No need to call it more than once... + if (!priorStudies) { + priorStudies = this.getAvailableStudyPriors(currentStudy.getObjectID()); + } // TODO: Revisit this later: What about two studies with the same // study date? let priorStudy; if (abstractPriorValue === -1) { - priorStudy = studies[studies.length - 1]; + priorStudy = priorStudies[priorStudies.length - 1]; } else { const studyIndex = Math.max(abstractPriorValue - 1, 0); - priorStudy = studies[studyIndex]; + priorStudy = priorStudies[studyIndex]; } // Invalid data diff --git a/Packages/ohif-hanging-protocols/client/protocolStore/defaultStrategy.js b/Packages/ohif-hanging-protocols/client/protocolStore/defaultStrategy.js index dc42da2c2..6c20e3e78 100644 --- a/Packages/ohif-hanging-protocols/client/protocolStore/defaultStrategy.js +++ b/Packages/ohif-hanging-protocols/client/protocolStore/defaultStrategy.js @@ -60,7 +60,7 @@ var defaultStrategy = (function () { } // Otherwise, return all protocols - return HangingProtocols.find(); + return HangingProtocols.find().fetch(); } /** diff --git a/Packages/ohif-hanging-protocols/client/protocolStore/protocolStore.js b/Packages/ohif-hanging-protocols/client/protocolStore/protocolStore.js index 67d9b631f..dc79a3ece 100644 --- a/Packages/ohif-hanging-protocols/client/protocolStore/protocolStore.js +++ b/Packages/ohif-hanging-protocols/client/protocolStore/protocolStore.js @@ -26,6 +26,18 @@ HP.ProtocolStore = (function () { strategy.onReady(callback); } + /** + * Get a HP.Protocol instance for the given protocol object + * @param {Object} protocolObject Protocol plain object + * @return {HP.Protocol} HP.Protocol instance for the given protocol object + */ + function getProtocolInstance(protocolObject) { + const protocolInstance = new HP.Protocol(); + protocolInstance.fromObject(protocolObject); + + return protocolInstance; + } + /** * Gets the hanging protocol by protocolId if defined, otherwise all stored hanging protocols * @@ -35,7 +47,23 @@ HP.ProtocolStore = (function () { * @returns {object|array} The hanging protocol by protocolId or array of the stored hanging protocols */ function getProtocol(protocolId) { - return strategy.getProtocol(protocolId); + let result = strategy.getProtocol(protocolId); + + // If result is an array of protocols objects + if (result instanceof Array) { + result.forEach( (protocol, index) => { + // Check if protocol is an instance of HP.Protocol + if (!(protocol instanceof HP.Protocol)) { + result[index] = getProtocolInstance(protocol); + } + }); + } else if (result !== void 0 && !(result instanceof HP.Protocol)) { + // Check if result exists and is not an instance of HP.Protocol + result = getProtocolInstance(result); + } + + + return result; } /**