diff --git a/Packages/ohif-hanging-protocols/client/protocolEngine.js b/Packages/ohif-hanging-protocols/client/protocolEngine.js index b77df9a7b..a675fc70b 100644 --- a/Packages/ohif-hanging-protocols/client/protocolEngine.js +++ b/Packages/ohif-hanging-protocols/client/protocolEngine.js @@ -8,8 +8,9 @@ import 'meteor/ohif:viewerbase'; * Import Constants */ -const StudyMetadata = OHIF.viewerbase.metadata.StudyMetadata; -const StudySummary = OHIF.viewerbase.metadata.StudySummary; +const { OHIFError } = OHIF.viewerbase; +const { StudyMetadata, StudySummary } = OHIF.viewerbase.metadata; + // 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 @@ -90,40 +91,50 @@ Meteor.startup(function() { }); }); -// Log decisions regarding matching -HP.match = function(attributes, rules) { - var options = { +/** + * Match a Metadata instance against rules using Validate.js for validation. + * @param {StudyMetadata|SeriesMetadata|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) { + const options = { format: 'grouped' }; - var score = 0; - var details = { + const details = { passed: [], failed: [] }; - - var requiredFailed = false; + + let requiredFailed = false; + let score = 0; rules.forEach(rule => { - var attribute = rule.attribute; + const attribute = rule.attribute; - // If the attributes we are testing (e.g. study, series, or instance attributes) do + // 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. - - // TODO: Investigate why attributes.hasOwnProperty(attribute) doesn't work? - if (attributes[attribute] === undefined && + if (!metadataInstance.customAttributeExists(attribute) && HP.CustomAttributeRetrievalCallbacks.hasOwnProperty(attribute)) { - var customAttribute = HP.CustomAttributeRetrievalCallbacks[attribute]; - attributes[attribute] = customAttribute.callback(attributes); + const customAttribute = HP.CustomAttributeRetrievalCallbacks[attribute]; + metadataInstance.setCustomAttribute(attribute, customAttribute.callback(metadataInstance)); } // Format the constraint as required by Validate.js - var testConstraint = {}; - testConstraint[attribute] = rule.constraint; + const testConstraint = { + [attribute]: rule.constraint + }; - // Use Validate.js to evaluate the constraints on the specified attributes - var errorMessages = validate(attributes, testConstraint, [options]); + // Create a single attribute object to be validated, since metadataInstance is an + // instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata) + const attributeMap = { + [attribute]: metadataInstance.getCustomAttribute(attribute) + }; + + // Use Validate.js to evaluate the constraints on the specified metadataInstance + const errorMessages = validate(attributeMap, testConstraint, [options]); if (!errorMessages) { // If no errorMessages were returned, then validation passed. @@ -133,7 +144,7 @@ HP.match = function(attributes, rules) { // Log that this rule passed in the matching details object details.passed.push({ - rule: rule + rule }); } else { // If errorMessages were present, then validation failed @@ -147,8 +158,8 @@ HP.match = function(attributes, rules) { // Log that this rule failed in the matching details object // and include any error messages details.failed.push({ - rule: rule, - errorMessages: errorMessages + rule, + errorMessages }); } }); @@ -159,13 +170,13 @@ HP.match = function(attributes, rules) { } return { - score: score, - details: details + score, + details }; }; -var sortByScore = function(arr) { - arr.sort(function(a, b) { +const sortByScore = arr => { + arr.sort((a, b) => { return b.score - a.score; }); }; @@ -173,25 +184,28 @@ var sortByScore = function(arr) { HP.ProtocolEngine = class ProtocolEngine { /** * Constructor - * @param {Object} LayoutManager Layout Manager Object + * @param {Object} layoutManager Layout Manager Object * @param {Array} studies Array of study metadata - * @param {Array} relatedStudies Array of related studies + * @param {Map} priorStudies Map of prior studies * @param {Object} studyMedadataSource Instance of StudyMetadataSource (ohif-viewerbase) Object to get study metadata */ constructor(layoutManager, studies, priorStudies, studyMetadataSource) { + const { LayoutManager, StudyMetadataSource } = OHIF.viewerbase; // ----------- - // Validations + // Type Validations - if (!(layoutManager instanceof OHIF.viewerbase.LayoutManager)) { - throw new OHIF.viewerbase.OHIFError('ProtocolEngine::constructor layoutManager is not an instance of LayoutManager'); + if (!(layoutManager instanceof LayoutManager)) { + throw new OHIFError('ProtocolEngine::constructor layoutManager is not an instance of LayoutManager'); } - if (!(studyMetadataSource instanceof OHIF.viewerbase.StudyMetadataSource)) { - throw new OHIF.viewerbase.OHIFError('ProtocolEngine::constructor studyMetadataSource is not an instance of StudyMetadataSource'); + if (!(studyMetadataSource instanceof StudyMetadataSource)) { + throw new OHIFError('ProtocolEngine::constructor studyMetadataSource is not an instance of StudyMetadataSource'); } - // @TODO: Validate "studies" parameter + if (!(studies instanceof Array) && !studies.every(study => study instanceof StudyMetadata)) { + throw new OHIFError('ProtocolEngine::constructor studies is not an array or it\'s items are not instances of StudyMetadata'); + } // -------------- // Initialization @@ -213,7 +227,7 @@ HP.ProtocolEngine = class ProtocolEngine { * Resets the ProtocolEngine to the best match */ reset() { - var protocol = this.getBestMatch(); + const protocol = this.getBestMatch(); this.setHangingProtocol(protocol); } @@ -240,7 +254,10 @@ HP.ProtocolEngine = class ProtocolEngine { return; } - study.numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study.getStudyInstanceUID()); + // Set custom attribute for study metadata + const numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study.getStudyInstanceUID()); + study.setCustomAttribute('numberOfPriorsReferenced', numberOfPriorsReferenced); + const rule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', { numericality: { greaterThanOrEqualTo: protocol.numberOfPriorsReferenced @@ -356,9 +373,8 @@ HP.ProtocolEngine = class ProtocolEngine { matchImages(viewport) { OHIF.log.info('ProtocolEngine::matchImages'); - const studyMatchingRules = viewport.studyMatchingRules; - const seriesMatchingRules = viewport.seriesMatchingRules; - const instanceMatchingRules = viewport.imageMatchingRules; + const { studyMatchingRules, seriesMatchingRules, imageMatchingRules: instanceMatchingRules } = viewport; + const matchingScores = []; const currentStudy = this.studies[0]; @@ -367,7 +383,8 @@ HP.ProtocolEngine = class ProtocolEngine { let highestImageMatchingScore = 0; let bestMatch; - currentStudy.abstractPriorValue = 0; + // Set custom attribute for study metadata + currentStudy.setCustomAttribute('abstractPriorValue', 0); studyMatchingRules.forEach(rule => { if (rule.attribute === 'abstractPriorValue') { @@ -391,41 +408,35 @@ HP.ProtocolEngine = class ProtocolEngine { priorStudy = studies[studyIndex]; } + // Invalid data if (!(priorStudy instanceof StudyMetadata) && !(priorStudy instanceof StudySummary)) { return; } - // @TODO: Make sure this study is already loaded into the viewer (OHIF.viewer.Studies) - // @TypeSafeStudies const priorStudyInstanceUID = priorStudy.getStudyInstanceUID(); - const alreadyLoaded = OHIF.viewer.Studies.findBy({ - studyInstanceUid: priorStudyInstanceUID - }); - if (!alreadyLoaded) { - this.studyMetadataSource.getByInstanceUID(priorStudyInstanceUID).then(study => { - study.abstractPriorValue = abstractPriorValue; + // Check if study metadata is already in studies list + if (this.studies.find(study => study.getStudyInstanceUID() === priorStudyInstanceUID)) { + // Update the viewport to refresh layout manager with new study + this.updateViewports(); - const studyMetadata = new OHIF.metadata.StudyMetadata(study); - const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata); - - studyMetadata.setDisplaySets(displaySets); - - study.selected = true; - study.displaySets = displaySets; - OHIF.viewer.Studies.insert(study); - OHIF.viewer.StudyMetadataList.insert(studyMetadata); - }, error => { console.alert(error) }); - // this.studyMetadataSource.getByInstanceUID(priorStudy.studyInstanceUid).then(, study => { - // study.abstractPriorValue = abstractPriorValue; - // study.displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(study); - // OHIF.viewer.Studies.insert(study); - - // this.studies.push(study); - // this.matchImages(viewport); - // this.updateViewports(); - // }); + return; } + + // 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); + + // Insert the new study metadata + this.studies.push(studyMetadata); + + // Re-match images + this.matchImages(viewport); + }, error => { + OHIF.log.warn(error); + throw new OHIFError(`ProtocolEngine::matchImages could not get study metadata for studyInstanceUID: ${priorStudyInstanceUID}`); + }); } // TODO: Add relative Date / time }); @@ -527,8 +538,8 @@ HP.ProtocolEngine = class ProtocolEngine { OHIF.log.info('ProtocolEngine::matchImages bestMatch', bestMatch); return { - bestMatch: bestMatch, - matchingScores: matchingScores + bestMatch, + matchingScores }; } @@ -658,8 +669,7 @@ HP.ProtocolEngine = class ProtocolEngine { } if (!currentViewportData.displaySetInstanceUid) { - //@TODO: make OHIFError use Meteor.Error instead of simple Error() - throw new OHIF.viewerbase.OHIFError('ProtocolEngine::updateViewports No matching display set found?'); + throw new OHIFError('ProtocolEngine::updateViewports No matching display set found?'); } viewportData.push(currentViewportData); diff --git a/Packages/ohif-measurements/client/lib/hangingProtocolCustomizations.js b/Packages/ohif-measurements/client/lib/hangingProtocolCustomizations.js index ad8a27dc6..0feb764c5 100644 --- a/Packages/ohif-measurements/client/lib/hangingProtocolCustomizations.js +++ b/Packages/ohif-measurements/client/lib/hangingProtocolCustomizations.js @@ -1,14 +1,21 @@ import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; +import { Viewerbase } from 'meteor/ohif:viewerbase'; +// TODO: [LT-refactor] move this to ohif:hanging-protocols package +/** + * Get a timepoint type for a given study metadata + * @param {StudyMetadata} study StudyMetadata instance + * @return {String|undefined} Timepoint type if found or undefined if not found or any error/missing information + */ const getTimepointType = study => { - const instance = Template.instance(); - const timepointApi = instance.timepointApi || instance.data.timepointApi; - if (!timepointApi) { + const timepointApi = Template.instance().timepointApi; + + if (!timepointApi || !(study instanceof Viewerbase.metadata.StudyMetadata)) { return; } - const timepoint = timepointApi.study(study.studyInstanceUid)[0]; + const timepoint = timepointApi.study(study.getStudyInstanceUID())[0]; if (!timepoint) { return; } diff --git a/Packages/ohif-study-list/client/lib/OHIFStudyMetadataSource.js b/Packages/ohif-study-list/client/lib/OHIFStudyMetadataSource.js index 51d08bd10..0909eb906 100644 --- a/Packages/ohif-study-list/client/lib/OHIFStudyMetadataSource.js +++ b/Packages/ohif-study-list/client/lib/OHIFStudyMetadataSource.js @@ -1,11 +1,16 @@ import { OHIF } from 'meteor/ohif:core'; import 'meteor/ohif:viewerbase'; +// Important metadata classes +const { OHIFError, metadata } = OHIF.viewerbase; +const { StudySummary, StudyMetadata } = metadata; + export class OHIFStudyMetadataSource extends OHIF.viewerbase.StudyMetadataSource { /** * Get study metadata for a study with given study InstanceUID * @param {String} studyInstanceUID Study InstanceUID + * @return {Promise} A Promise object */ getByInstanceUID(studyInstanceUID) { return new Promise((resolve, reject) => { @@ -13,4 +18,60 @@ export class OHIFStudyMetadataSource extends OHIF.viewerbase.StudyMetadataSource }); } + /** + * Load study info (OHIF.viewer.Studies) and study metadata (OHIF.viewer.StudyMetadataList) for a given study. + * @param {StudySummary|StudyMetadata} Instance of StudySummary or StudyMetadata object + */ + loadStudy(study) { + if (!(study instanceof StudyMetadata) && !(study instanceof StudySummary)) { + throw new OHIFError('OHIFStudyMetadataSource::loadStudy study is not an instance of StudySummary or StudyMetadata'); + } + + return new Promise((resolve, reject) => { + const studyInstanceUID = study.getStudyInstanceUID(); + + if (study instanceof StudyMetadata) { + const alreadyLoaded = OHIF.viewer.Studies.findBy({ + studyInstanceUid: studyInstanceUID + }); + + if (!alreadyLoaded) { + this._updateStudyCollections(study); + } + + resolve(study); + return; + } + + this.getByInstanceUID(study.getStudyInstanceUID()).then(studyInfo => { + // Create study metadata object + const studyMetadata = new OHIF.metadata.StudyMetadata(studyInfo); + + // Get Study display sets + const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata); + + // Set studyMetadata display sets + studyMetadata.setDisplaySets(displaySets); + + this._updateStudyCollections(studyMetadata); + resolve(studyMetadata); + }, reject); + }); + } + + // Static methods + static _updateStudyCollections(studyMetadata) { + const studyInfo = studyMetadata.getData(); + + // Set some studyInfo properties + studyInfo.selected = true; + studyInfo.displaySets = displaySets; + + // Insert new study info object in Studies TypeSafeCollection + OHIF.viewer.Studies.insert(studyInfo); + + // Insert new study metadata in StudyMetadataList TypeSafeCollection + OHIF.viewer.StudyMetadataList.insert(studyMetadata); + } + } diff --git a/Packages/ohif-viewerbase/client/lib/classes/metadata/Metadata.js b/Packages/ohif-viewerbase/client/lib/classes/metadata/Metadata.js index 8cac78f4d..5b5336145 100644 --- a/Packages/ohif-viewerbase/client/lib/classes/metadata/Metadata.js +++ b/Packages/ohif-viewerbase/client/lib/classes/metadata/Metadata.js @@ -23,6 +23,15 @@ export class Metadata { writable: false, value: data }); + + // Define _custom properties as an immutable property + // IMPORTANT: This property can only be set during instance construction. + Object.defineProperty(this, '_custom', { + configurable: false, + enumerable: false, + writable: false, + value: Object.create(null) + }); } getData() { @@ -38,6 +47,33 @@ export class Metadata { return propertyValue; } + /** + * Set custom attribute value + * @param {String} attribute Custom attribute name + * @param {Any} value Custom attribute value + */ + setCustomAttribute(attribute, value) { + this._custom[attribute] = value; + } + + /** + * Get custom attribute value + * @param {String} attribute Custom attribute name + * @return {Any} Custom attribute value + */ + getCustomAttribute(attribute) { + return this._custom[attribute]; + } + + /** + * Check if a custom attribute exists + * @param {String} attribute Custom attribute name + * @return {Boolean} True if custom attribute exists or false if not + */ + customAttributeExists(attribute) { + return attribute in this._custom; + } + /** * Static Methods */