diff --git a/OHIFViewer/client/components/viewer/viewer.js b/OHIFViewer/client/components/viewer/viewer.js index 139a39b9a..15c0fa03c 100644 --- a/OHIFViewer/client/components/viewer/viewer.js +++ b/OHIFViewer/client/components/viewer/viewer.js @@ -28,8 +28,11 @@ const initHangingProtocol = () => { // Instantiate StudyMetadataSource: necessary for Hanging Protocol to get study metadata const studyMetadataSource = new StudyList.classes.OHIFStudyMetadataSource(); + // Get prior studies map + const studyPriorsMap = StudyList.functions.getStudyPriorsMap(studyMetadataList); + // Creates Protocol Engine object with required arguments - const ProtocolEngine = new HP.ProtocolEngine(layoutManager, studyMetadataList, [], studyMetadataSource); + const ProtocolEngine = new HP.ProtocolEngine(layoutManager, studyMetadataList, studyPriorsMap, studyMetadataSource); // Sets up Hanging Protocol engine HP.setEngine(ProtocolEngine); @@ -132,7 +135,7 @@ Template.viewer.onRendered(function() { // To avoid first run if (isOHIFViewerMainRendered) { - // To run only when ViewerMainRendered dependency has changed. + // To run only when OHIFViewerMainRendered dependency has changed. // because initHangingProtocol can have other reactive components Tracker.nonreactive(initHangingProtocol); } diff --git a/Packages/ohif-hanging-protocols/client/protocolEngine.js b/Packages/ohif-hanging-protocols/client/protocolEngine.js index d210f8eb7..b77df9a7b 100644 --- a/Packages/ohif-hanging-protocols/client/protocolEngine.js +++ b/Packages/ohif-hanging-protocols/client/protocolEngine.js @@ -4,6 +4,13 @@ import { _ } from 'meteor/underscore'; import { OHIF } from 'meteor/ohif:core'; import 'meteor/ohif:viewerbase'; +/** + * Import Constants + */ + +const StudyMetadata = OHIF.viewerbase.metadata.StudyMetadata; +const StudySummary = OHIF.viewerbase.metadata.StudySummary; + // Define a global variable that will be used to refer to the Protocol Engine // It must be populated by HP.setEngine when the Viewer is initialized and a ProtocolEngine // is instantiated on top of the LayoutManager. If the global ProtocolEngine variable remains @@ -171,18 +178,30 @@ HP.ProtocolEngine = class ProtocolEngine { * @param {Array} relatedStudies Array of related studies * @param {Object} studyMedadataSource Instance of StudyMetadataSource (ohif-viewerbase) Object to get study metadata */ - constructor(LayoutManager, studies, relatedStudies, studyMetadataSource) { - OHIF.log.info('ProtocolEngine::constructor'); + constructor(layoutManager, studies, priorStudies, studyMetadataSource) { + + // ----------- + // Validations + + if (!(layoutManager instanceof OHIF.viewerbase.LayoutManager)) { + throw new OHIF.viewerbase.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'); } - this.LayoutManager = LayoutManager; + // @TODO: Validate "studies" parameter + + // -------------- + // Initialization + + this.LayoutManager = layoutManager; this.studies = studies; - this.relatedStudies = relatedStudies; + this.priorStudies = priorStudies instanceof Map ? priorStudies : new Map(); this.studyMetadataSource = studyMetadataSource; + // Put protocol engine in a known states this.reset(); // Create an array for new stage ids to be stored @@ -221,7 +240,7 @@ HP.ProtocolEngine = class ProtocolEngine { return; } - study.numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study); + study.numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study.getStudyInstanceUID()); const rule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', { numericality: { greaterThanOrEqualTo: protocol.numberOfPriorsReferenced @@ -312,69 +331,25 @@ HP.ProtocolEngine = class ProtocolEngine { } /** - * Calculates the number of previous studies in the cached StudyList that - * have the same patientId and an earlier study date + * Get the number of prior studies supplied in the priorStudies map property. * - * @param study The input study - * @returns {any|*} The number of available prior studies with the same patientId + * @param {String} studyInstanceUID The StudyInstanceUID of the study whose priors are needed + * @returns {number} The number of available prior studies with the same PatientID */ - getNumberOfAvailablePriors(study) { - const instance = study.getFirstInstance(); - const studies = StudyListStudies.find({ - patientId: instance.getRawValue('x00100020'), // PatientID, - studyDate: { - $lt: instance.getRawValue('x00080020') // StudyDate - } - }); - - return studies.count(); + getNumberOfAvailablePriors(studyInstanceUID) { + const priors = this.getAvailableStudyPriors(studyInstanceUID); + return priors.length; } - findRelatedStudies(protocol, study) { - if (!protocol.protocolMatchingRules) { - return; - } - - var studies = StudyListStudies.find({ - patientId: study.patientId, - studyDate: { - $lt: study.studyDate - } - }, { - sort: { - studyDate: -1 - } - }); - - var related = []; - var currentDate = moment(study.studyDate, 'YYYYMMDD'); - - studies.forEach(function(priorStudy, priorIndex) { - // Calculate an abstract prior value for the study in question - if (priorIndex === (studies.length - 1)) { - priorStudy.abstractPriorValue = -1; - } else { - // Abstract prior index starts from 1 in the DICOM standard - priorStudy.abstractPriorValue = priorIndex + 1; - } - - // Calculate the relative time using Moment.js - var priorDate = moment(priorStudy.studyDate, 'YYYYMMDD'); - priorStudy.relativeTime = currentDate.diff(priorDate); - - var details = HP.match(priorStudy, protocol.protocolMatchingRules); - if (details.score) { - related.push({ - score: details.score, - study: priorStudy - }); - } - }); - - sortByScore(related); - return related.map(function(v) { - return v.study; - }); + /** + * Get the array prior studies from a specific study. + * + * @param {String} studyInstanceUID The StudyInstanceUID of the study whose priors are needed + * @returns {Array} The array of available priors or an empty array + */ + getAvailableStudyPriors(studyInstanceUID) { + const priors = this.priorStudies.get(studyInstanceUID); + return priors instanceof Array ? priors : []; } // Match images given a list of Studies and a Viewport's image matching reqs @@ -403,17 +378,7 @@ HP.ProtocolEngine = class ProtocolEngine { abstractPriorValue = parseInt(abstractPriorValue, 10); // TODO: Restrict or clarify validators for abstractPriorValue? - // @TODO replace for this.relatedStudies (TypeSafeCollection of StudyMetadatas) - const studies = StudyListStudies.find({ - patientId: currentStudy.patientId, - studyDate: { - $lt: currentStudy.studyDate - } - }, { - sort: { - studyDate: -1 - } - }).fetch(); + const studies = this.getAvailableStudyPriors(currentStudy.getStudyInstanceUID()); // TODO: Revisit this later: What about two studies with the same // study date? @@ -426,25 +391,40 @@ HP.ProtocolEngine = class ProtocolEngine { priorStudy = studies[studyIndex]; } - if (!priorStudy) { + 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: priorStudy.studyInstanceUid + studyInstanceUid: priorStudyInstanceUID }); if (!alreadyLoaded) { - this.studyMetadataSource.getByInstanceUID(priorStudy.studyInstanceUid, study => { + this.studyMetadataSource.getByInstanceUID(priorStudyInstanceUID).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(); - }); + 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(); + // }); } } // TODO: Add relative Date / time diff --git a/Packages/ohif-study-list/client/lib/OHIFStudySummary.js b/Packages/ohif-study-list/client/lib/OHIFStudySummary.js new file mode 100644 index 000000000..4cb1a9ae1 --- /dev/null +++ b/Packages/ohif-study-list/client/lib/OHIFStudySummary.js @@ -0,0 +1,12 @@ +import { OHIF } from 'meteor/ohif:core'; +import 'meteor/ohif:viewerbase'; + +export class OHIFStudySummary extends OHIF.viewerbase.metadata.StudySummary { + + // @Override + addTags(tagMap) { + // @TODO: Map OHIF tag names to standard DICOM tag names + super.addTags(tagMap); + } + +} diff --git a/Packages/ohif-study-list/client/lib/getStudyPriors.js b/Packages/ohif-study-list/client/lib/getStudyPriors.js new file mode 100644 index 000000000..3b64c4df2 --- /dev/null +++ b/Packages/ohif-study-list/client/lib/getStudyPriors.js @@ -0,0 +1,52 @@ +import { OHIF } from 'meteor/ohif:core'; +import 'meteor/ohif:viewerbase'; +// Local Dependencies +import { OHIFStudySummary } from './OHIFStudySummary'; + +const StudyMetadata = OHIF.viewerbase.metadata.StudyMetadata; +const StudySummary = OHIF.viewerbase.metadata.StudySummary; +const PatientID = 'x00100020'; +const StudyDate = 'x00080020'; + +const getStudyPriors = study => { + + const priorStudies = []; + let patientID; + let studyDate; + + if (study instanceof StudyMetadata) { + const instance = study.getFirstInstance(); + patientID = instance.getRawValue(PatientID); // PatientID + studyDate = instance.getRawValue(StudyDate); // StudyDate + } else if (study instanceof StudySummary) { + patientID = study.getTagValue(PatientID); // PatientID + studyDate = study.getTagValue(StudyDate); // StudyDate + } + + // Find prior studies in global StudyListStudies Minimongo collection... + const cursor = StudyListStudies.find({ + patientId: patientID, + studyDate: { + $lt: studyDate + } + }, { + sort: { + studyDate: 'desc' + } + }); + + // Create an OHIFStudySummary object for each prior study found... + cursor.forEach(study => { + const summary = new OHIFStudySummary(); + summary.addTags(study); + priorStudies.push(summary); + console.log('@StudyListStudies'); + console.log(study); + console.log(study); + }); + + return priorStudies; + +}; + +export { getStudyPriors }; diff --git a/Packages/ohif-study-list/client/lib/getStudyPriorsMap.js b/Packages/ohif-study-list/client/lib/getStudyPriorsMap.js new file mode 100644 index 000000000..d4b062212 --- /dev/null +++ b/Packages/ohif-study-list/client/lib/getStudyPriorsMap.js @@ -0,0 +1,33 @@ +import { OHIF } from 'meteor/ohif:core'; +import 'meteor/ohif:viewerbase'; +// Local Dependencies +import { getStudyPriors } from './getStudyPriors'; + +const StudyMetadata = OHIF.viewerbase.metadata.StudyMetadata; +const StudySummary = OHIF.viewerbase.metadata.StudySummary; + +/** + * Create a Map of study priors where the key of each entry is the StudyInstanceUID and its value is an array of StudySummary instances. + * @returns {Map} The study map. + */ +const getStudyPriorsMap = studies => { + + const priorsMap = new Map(); + + if (studies instanceof Array) { + studies.forEach(study => { + if (study instanceof StudyMetadata || study instanceof StudySummary) { + const studyInstanceUID = study.getStudyInstanceUID(); + if (studyInstanceUID) { + const priors = getStudyPriors(study); + priorsMap.set(studyInstanceUID, priors); + } + } + }); + } + + return priorsMap; + +}; + +export { getStudyPriorsMap }; diff --git a/Packages/ohif-study-list/client/lib/studylist.js b/Packages/ohif-study-list/client/lib/studylist.js index 47d0a8d7f..977e7ccad 100644 --- a/Packages/ohif-study-list/client/lib/studylist.js +++ b/Packages/ohif-study-list/client/lib/studylist.js @@ -1,11 +1,20 @@ import { Viewerbase } from 'meteor/ohif:viewerbase'; +// Classes import { OHIFStudyMetadataSource } from './OHIFStudyMetadataSource'; +import { OHIFStudySummary } from './OHIFStudySummary'; +// Functions +import { getStudyPriors } from './getStudyPriors'; +import { getStudyPriorsMap } from './getStudyPriorsMap'; StudyList = { - functions: {}, + functions: { + getStudyPriors, + getStudyPriorsMap + }, callbacks: {}, classes: { - OHIFStudyMetadataSource + OHIFStudyMetadataSource, + OHIFStudySummary } }; diff --git a/Packages/ohif-viewerbase/client/index.js b/Packages/ohif-viewerbase/client/index.js index 9102c5a12..35a0d2dcb 100644 --- a/Packages/ohif-viewerbase/client/index.js +++ b/Packages/ohif-viewerbase/client/index.js @@ -188,7 +188,8 @@ Viewerbase.ResizeViewportManager = ResizeViewportManager; import { StudyMetadata } from './lib/classes/metadata/StudyMetadata'; import { SeriesMetadata } from './lib/classes/metadata/SeriesMetadata'; import { InstanceMetadata } from './lib/classes/metadata/InstanceMetadata'; -Viewerbase.metadata = { StudyMetadata, SeriesMetadata, InstanceMetadata }; +import { StudySummary } from './lib/classes/metadata/StudySummary'; +Viewerbase.metadata = { StudyMetadata, SeriesMetadata, InstanceMetadata, StudySummary }; // TypeSafeCollection import { TypeSafeCollection } from './lib/classes/TypeSafeCollection'; diff --git a/Packages/ohif-viewerbase/client/lib/DICOMTagDescriptions.js b/Packages/ohif-viewerbase/client/lib/DICOMTagDescriptions.js index ebd294eb3..507b109b6 100644 --- a/Packages/ohif-viewerbase/client/lib/DICOMTagDescriptions.js +++ b/Packages/ohif-viewerbase/client/lib/DICOMTagDescriptions.js @@ -1,6 +1,7 @@ const NUMBER = 'number'; const STRING = 'string'; +const REGEX_TAG = /^x[0-9a-fx]{8}$/; const DICOMTagDescriptions = Object.create(Object.prototype, { _descriptions: { @@ -9,22 +10,94 @@ const DICOMTagDescriptions = Object.create(Object.prototype, { writable: false, value: Object.create(null) }, + tagNumberToString: { + configurable: false, + enumerable: true, + writable: false, + value: function tagNumberToString(tag) { + let string; // by default, undefined is returned... + if (this.isValidTagNumber(tag)) { + // if it's a number, build its hexadecimal representation... + string = 'x' + ('00000000' + tag.toString(16)).substr(-8); + } + return string; + } + }, + isValidTagNumber: { + configurable: false, + enumerable: true, + writable: false, + value: function isValidTagNumber(tag) { + return (typeof tag === NUMBER && tag >= 0 && tag <= 0xFFFFFFFF); + } + }, + isValidTag: { + configurable: false, + enumerable: true, + writable: false, + value: function isValidTag(tag) { + return (typeof tag === STRING ? REGEX_TAG.test(tag) : this.isValidTagNumber(tag)); + } + }, find: { configurable: false, enumerable: true, writable: false, value: function find(name) { - let description; // by default, undefined... - if (typeof name === NUMBER) { - // if it's a number, build its hexadecimal representation... - name = 'x' + ('00000000' + name.toString(16)).substr(-8); + let description; // by default, undefined is returned... + if (typeof name !== STRING) { + // if it's a number, a tag string will be returned... + name = this.tagNumberToString(name); } if (typeof name === STRING) { description = this._descriptions[name]; } return description; } - } + }, + init: { + configurable: false, + enumerable: true, + writable: false, + value: function init(descriptionMap) { + const _hasOwn = Object.prototype.hasOwnProperty; + const _descriptions = this._descriptions; + for (let tag in descriptionMap) { + if (_hasOwn.call(descriptionMap, tag)) { + if (!this.isValidTag(tag)) { + // Skip in case tag is not valid... + console.info(`DICOMTagDescriptions: Invalid tag "${tag}"...`); + continue; + } + if (tag in _descriptions) { + // Skip in case the tag is duplicated... + console.info(`DICOMTagDescriptions: Duplicated tag "${tag}"...`); + continue; + } + // Save keyword... + const keyword = descriptionMap[tag]; + // Create a description entry and freeze it... + const entry = Object.create(null); + entry.tag = tag; + entry.keyword = keyword; + Object.freeze(entry); + // Add tag references to entry... + _descriptions[tag] = entry; + // Add keyword references to entry (if not present already)... + if (keyword in _descriptions) { + const currentEntry = _descriptions[keyword]; + console.info(`DICOMTagDescriptions: Using <${currentEntry.tag},${currentEntry.keyword}> instead of <${entry.tag},${entry.keyword}> for keyword "${keyword}"...`); + } else { + _descriptions[keyword] = entry; + } + } + } + // Freeze internal description map... + Object.freeze(_descriptions); + // Freeze itself... + Object.freeze(this); + } + }, }); /** @@ -3179,41 +3252,7 @@ let initialTagDescriptionMap = { xfffee0dd: 'EndOfSequence' }; -// Safely populate DICOMTagDescriptions object -(function(initialTagDescriptionMap) { - const _hasOwn = Object.prototype.hasOwnProperty; - const descriptionMap = DICOMTagDescriptions._descriptions; - for (let tag in initialTagDescriptionMap) { - if (_hasOwn.call(initialTagDescriptionMap, tag)) { - if (tag in descriptionMap) { - // Skip in case the tag is duplicated... - console.info(`DICOMTagDescriptions: Duplicated tag ${tag}...`); - continue; - } - // Save keyword... - const keyword = initialTagDescriptionMap[tag]; - // Create a description entry and freeze it... - const entry = Object.create(null); - entry.tag = tag; - entry.keyword = keyword; - Object.freeze(entry); - // Add tag references to entry... - descriptionMap[tag] = entry; - // Add keyword references to entry (if not present already)... - if (keyword in descriptionMap) { - const currentEntry = descriptionMap[keyword]; - console.info(`DICOMTagDescriptions: Using <${currentEntry.tag},${currentEntry.keyword}> instead of <${entry.tag},${entry.keyword}> for keyword "${keyword}"...`); - } else { - descriptionMap[keyword] = entry; - } - } - } - // Freeze internal description map (nothing should be changed in this object afterwords)... - Object.freeze(descriptionMap); -}(initialTagDescriptionMap)); - -// Freeze exported object... -Object.freeze(DICOMTagDescriptions); +DICOMTagDescriptions.init(initialTagDescriptionMap); // Discard original map... initialTagDescriptionMap = null; diff --git a/Packages/ohif-viewerbase/client/lib/classes/metadata/InstanceMetadata.js b/Packages/ohif-viewerbase/client/lib/classes/metadata/InstanceMetadata.js index d8cfaf23f..ab422214c 100644 --- a/Packages/ohif-viewerbase/client/lib/classes/metadata/InstanceMetadata.js +++ b/Packages/ohif-viewerbase/client/lib/classes/metadata/InstanceMetadata.js @@ -4,7 +4,6 @@ import { OHIFError } from '../OHIFError'; const UNDEFINED = 'undefined'; const NUMBER = 'number'; const STRING = 'string'; -const REGEX_TAG = /^x[0-9a-fx]{8}$/; export class InstanceMetadata extends Metadata { diff --git a/Packages/ohif-viewerbase/client/lib/classes/metadata/StudySummary.js b/Packages/ohif-viewerbase/client/lib/classes/metadata/StudySummary.js index c3dd6fad2..2ff216b26 100644 --- a/Packages/ohif-viewerbase/client/lib/classes/metadata/StudySummary.js +++ b/Packages/ohif-viewerbase/client/lib/classes/metadata/StudySummary.js @@ -1,42 +1,94 @@ +import { DICOMTagDescriptions } from '../../DICOMTagDescriptions'; -class StudySummary { +const StudyInstanceUID = 'x0020000D'; - constructor(propertyMap) { +export class StudySummary { + + constructor(tagMap, propertyMap) { // Define the main immutable "_data" private property. - Object.defineProperty(this, '_data', { - configurable: false, - enumerable: false, - writable: false, - value: Object.create(null) + Object.defineProperties(this, { + _tags: { + configurable: false, + enumerable: false, + writable: false, + value: Object.create(null) + }, + _properties: { + configurable: false, + enumerable: false, + writable: false, + value: Object.create(null) + } }); - const hasOwn = Object.prototype.hasOwnProperty; - const data = this._data; - for (let property in propertyMap) { - if (hasOwn.call(propertyMap, property)) { - data[property] = propertyMap[property]; - } + // Initialize internal tag map if first argument is given. + if (tagMap !== void 0) { + this.addTags(tagMap); + } + + // Initialize internal property map if second argument is given. + if (propertyMap !== void 0) { + this.addProperties(addProperties); } } + getStudyInstanceUID() { + // This method should return null if StudyInstanceUID is not available to keep compatibility StudyMetadata API + return this.getTagValue(StudyInstanceUID) || null; + } + + addProperties(propertyMap) { + const _hasOwn = Object.prototype.hasOwnProperty; + const _properties = this._properties; + for (let property in propertyMap) { + if (_hasOwn.call(propertyMap, property)) { + _properties[property] = propertyMap[property]; + } + } + } + + addTags(tagMap) { + const _hasOwn = Object.prototype.hasOwnProperty; + const _tags = this._tags; + for (let tag in tagMap) { + if (_hasOwn.call(tagMap, tag)) { + const description = DICOMTagDescriptions.find(tag); + // When a description is available, use its tag as internal key... + if (description) { + _tags[description.tag] = tagMap[tag]; + } else { + _tags[tag] = tagMap[tag]; + } + } + } + } + + tagExists(tagName) { + const _tags = this._tags; + const description = DICOMTagDescriptions.find(tagName); + if (description) { + return (description.tag in _tags); + } + return (tagName in _tags); + } + + getTagValue(tagName) { + const _tags = this._tags; + const description = DICOMTagDescriptions.find(tagName); + if (description) { + return _tags[description.tag]; + } + return _tags[tagName]; + } + propertyExists(propertyName) { - const internalPropertyName = this.getInternalPropertyName(propertyName); - return (internalPropertyName in this._data); + return (propertyName in this._properties); } getPropertyValue(propertyName) { - const internalPropertyName = this.getInternalPropertyName(propertyName); - return this._data[internalPropertyName]; - } - - /** - * This function is supposed to translate external property names into internal property names. - * By default it implements the simplest translation possible. - */ - getInternalPropertyName(propertyName) { - return propertyName; + return this._properties[propertyName]; } }