Moving Hanging Protocols updates back to the viewers

This commit is contained in:
Eloízio Salgado 2017-02-22 11:58:49 -03:00
parent e10453b919
commit bec8f21c0a
19 changed files with 1626 additions and 183 deletions

View File

@ -89,7 +89,7 @@ Template.viewer.onCreated(() => {
OHIF.viewer.StudyMetadataList.removeAll(); OHIF.viewer.StudyMetadataList.removeAll();
instance.data.studies.forEach(study => { instance.data.studies.forEach(study => {
const studyMetadata = new OHIF.metadata.StudyMetadata(study); const studyMetadata = new OHIF.metadata.StudyMetadata(study, study.studyInstanceUid);
const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata); const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata);
studyMetadata.setDisplaySets(displaySets); studyMetadata.setDisplaySets(displaySets);
@ -125,6 +125,7 @@ Template.viewer.onCreated(() => {
return; return;
} }
// @TODO: Maybe this should be a setCustomAttribute?
study.timepointType = timepoint.timepointType; study.timepointType = timepoint.timepointType;
}); });
}); });

View File

@ -112,7 +112,7 @@ Template.viewer.onCreated(() => {
ViewerData[contentId].studyInstanceUids = []; ViewerData[contentId].studyInstanceUids = [];
instance.data.studies.forEach(study => { instance.data.studies.forEach(study => {
const studyMetadata = new OHIF.metadata.StudyMetadata(study); const studyMetadata = new OHIF.metadata.StudyMetadata(study, study.studyInstanceUid);
const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata); const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata);
studyMetadata.setDisplaySets(displaySets); studyMetadata.setDisplaySets(displaySets);

View File

@ -12,3 +12,10 @@ HangingProtocols.allow({
return true; return true;
} }
}); });
// @TODO: Remove this after stabilizing ProtocolEngine
if (Meteor.isDevelopment && Meteor.isServer) {
Meteor.startup(() => {
HangingProtocols.remove({});
});
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { _ } from 'meteor/underscore'; import { _ } from 'meteor/underscore';
// OHIF Modules // OHIF Modules
import { OHIF } from 'meteor/ohif:core'; import { OHIF } from 'meteor/ohif:core';
import 'meteor/ohif:viewerbase'; import 'meteor/ohif:viewerbase';
@ -84,8 +85,8 @@ HP.addCustomViewportSetting = function(settingId, settingName, options, callback
}; };
Meteor.startup(function() { Meteor.startup(function() {
HP.addCustomViewportSetting('wlPreset', 'Window/Level Preset', Object.keys(OHIF.viewer.wlPresets), function(element, optionValue) { HP.addCustomViewportSetting('wlPreset', 'Window/Level Preset', Object.create(null), function(element, optionValue) {
if (OHIF.viewer.wlPresets.hasOwnProperty(optionValue)) { if (optionValue in OHIF.viewer.wlPresets) {
OHIF.viewerbase.wlPresets.applyWLPreset(optionValue, element); OHIF.viewerbase.wlPresets.applyWLPreset(optionValue, element);
} }
}); });
@ -93,14 +94,15 @@ Meteor.startup(function() {
/** /**
* Match a Metadata instance against rules using Validate.js for validation. * Match a Metadata instance against rules using Validate.js for validation.
* @param {StudyMetadata|SeriesMetadata|InstanceMetadata} metadataInstance Metadata instance object * @param {StudySummary|InstanceMetadata} metadataInstance Metadata instance object
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match * @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) * @return {Object} Matching Object with score and details (which rule passed or failed)
*/ */
HP.match = function(metadataInstance, rules) { HP.match = function(metadataInstance, rules) {
if (!(metadataInstance instanceof StudyMetadata || metadataInstance instanceof SeriesMetadata || metadataInstance instanceof InstanceMetadata)) { // Make sure the supplied data is valid.
throw new OHIFError('HP::match metadataInstance must be an instance of StudyMetadata, SeriesMetadata or InstanceMetadata'); if (!(metadataInstance instanceof StudySummary || metadataInstance instanceof InstanceMetadata)) {
throw new OHIFError('HP::match metadataInstance must be an instance of StudySummary or InstanceMetadata');
} }
const options = { const options = {
@ -114,15 +116,6 @@ HP.match = function(metadataInstance, rules) {
let requiredFailed = false; let requiredFailed = false;
let score = 0; let score = 0;
let instance;
if (metadataInstance instanceof StudyMetadata) {
instance = metadataInstance.getFirstInstance();
} else if (metadataInstance instanceof SeriesMetadata) {
instance = metadataInstance.getInstanceByIndex(0);
} else {
instance = metadataInstance;
}
rules.forEach(rule => { rules.forEach(rule => {
const attribute = rule.attribute; const attribute = rule.attribute;
@ -144,13 +137,18 @@ HP.match = function(metadataInstance, rules) {
// Create a single attribute object to be validated, since metadataInstance is an // Create a single attribute object to be validated, since metadataInstance is an
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata) // instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
const attributeValue = customAttributeExists ? metadataInstance.getCustomAttribute(attribute) : instance.getRawValue(attribute); const attributeValue = customAttributeExists ? metadataInstance.getCustomAttribute(attribute) : metadataInstance.getTagValue(attribute);
const attributeMap = { const attributeMap = {
[attribute]: attributeValue + '' [attribute]: attributeValue
}; };
// Use Validate.js to evaluate the constraints on the specified metadataInstance // Use Validate.js to evaluate the constraints on the specified metadataInstance
const errorMessages = validate(attributeMap, testConstraint, [options]); let errorMessages;
try {
errorMessages = validate(attributeMap, testConstraint, [options]);
} catch (e) {
errorMessages = [ 'Something went wrong during validation.', e ];
}
if (!errorMessages) { if (!errorMessages) {
// If no errorMessages were returned, then validation passed. // If no errorMessages were returned, then validation passed.
@ -260,6 +258,7 @@ HP.ProtocolEngine = class ProtocolEngine {
OHIF.log.info('ProtocolEngine::findMatchByStudy'); OHIF.log.info('ProtocolEngine::findMatchByStudy');
const matched = []; const matched = [];
const studyInstance = study.getFirstInstance();
HP.ProtocolStore.getProtocol().forEach(protocol => { HP.ProtocolStore.getProtocol().forEach(protocol => {
// Clone the protocol's protocolMatchingRules array // Clone the protocol's protocolMatchingRules array
@ -271,18 +270,26 @@ HP.ProtocolEngine = class ProtocolEngine {
} }
// Set custom attribute for study metadata // Set custom attribute for study metadata
const numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study.getStudyInstanceUID()); const numberOfPriorsReferenced = this.getNumberOfAvailablePriors(study.getObjectID());
study.setCustomAttribute('numberOfPriorsReferenced', numberOfPriorsReferenced); studyInstance.setCustomAttribute('numberOfPriorsReferenced', numberOfPriorsReferenced);
const rule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', { const requiredPriorsRule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', {
numericality: { numericality: {
greaterThanOrEqualTo: protocol.numberOfPriorsReferenced greaterThanOrEqualTo: protocol.numberOfPriorsReferenced
} }
}); });
rules.push(rule); rules.push(requiredPriorsRule);
const matchedDetails = HP.match(study, rules); const exactPriorsDesiredRule = new HP.ProtocolMatchingRule('numberOfPriorsReferenced', {
numericality: {
equalTo: protocol.numberOfPriorsReferenced
},
}, false, 1);
rules.push(exactPriorsDesiredRule);
const matchedDetails = HP.match(studyInstance, rules);
if (matchedDetails.score > 0) { if (matchedDetails.score > 0) {
matched.push({ matched.push({
@ -366,25 +373,48 @@ HP.ProtocolEngine = class ProtocolEngine {
/** /**
* Get the number of prior studies supplied in the priorStudies map property. * Get the number of prior studies supplied in the priorStudies map property.
* *
* @param {String} studyInstanceUID The StudyInstanceUID of the study whose priors are needed * @param {String} studyObjectID The study object ID of the study whose priors are needed
* @returns {number} The number of available prior studies with the same PatientID * @returns {number} The number of available prior studies with the same PatientID
*/ */
getNumberOfAvailablePriors(studyInstanceUID) { getNumberOfAvailablePriors(studyObjectID) {
const priors = this.getAvailableStudyPriors(studyInstanceUID); const priors = this.getAvailableStudyPriors(studyObjectID);
return priors.length; return priors.length;
} }
/** /**
* Get the array prior studies from a specific study. * Get the array of prior studies from a specific study.
* *
* @param {String} studyInstanceUID The StudyInstanceUID of the study whose priors are needed * @param {String} studyObjectID The study object ID of the study whose priors are needed
* @returns {Array} The array of available priors or an empty array * @returns {Array} The array of available priors or an empty array
*/ */
getAvailableStudyPriors(studyInstanceUID) { getAvailableStudyPriors(studyObjectID) {
const priors = this.priorStudies.get(studyInstanceUID); const priors = this.priorStudies.get(studyObjectID);
return priors instanceof Array ? priors : []; 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 = HP.match(prior, protocolMatchingRules);
return matchDetails.score > 0;
});
}
return allPriors;
}
// Match images given a list of Studies and a Viewport's image matching reqs // Match images given a list of Studies and a Viewport's image matching reqs
matchImages(viewport) { matchImages(viewport) {
OHIF.log.info('ProtocolEngine::matchImages'); OHIF.log.info('ProtocolEngine::matchImages');
@ -393,14 +423,18 @@ HP.ProtocolEngine = class ProtocolEngine {
const matchingScores = []; const matchingScores = [];
const currentStudy = this.studies[0]; const currentStudy = this.studies[0];
const firstInstance = currentStudy.getFirstInstance();
let highestStudyMatchingScore = 0; let highestStudyMatchingScore = 0;
let highestSeriesMatchingScore = 0; let highestSeriesMatchingScore = 0;
let highestImageMatchingScore = 0; let highestImageMatchingScore = 0;
let bestMatch; let bestMatch;
// Set custom attribute for study metadata // Set custom attribute for study metadata and it's first instance
currentStudy.setCustomAttribute('abstractPriorValue', 0); currentStudy.setCustomAttribute('abstractPriorValue', 0);
if (firstInstance instanceof InstanceMetadata) {
firstInstance.setCustomAttribute('abstractPriorValue', 0);
}
studyMatchingRules.forEach(rule => { studyMatchingRules.forEach(rule => {
if (rule.attribute === 'abstractPriorValue') { if (rule.attribute === 'abstractPriorValue') {
@ -411,7 +445,7 @@ HP.ProtocolEngine = class ProtocolEngine {
abstractPriorValue = parseInt(abstractPriorValue, 10); abstractPriorValue = parseInt(abstractPriorValue, 10);
// TODO: Restrict or clarify validators for abstractPriorValue? // TODO: Restrict or clarify validators for abstractPriorValue?
const studies = this.getAvailableStudyPriors(currentStudy.getStudyInstanceUID()); const studies = this.getPriorsByProtocolMatchingRules(currentStudy.getObjectID());
// TODO: Revisit this later: What about two studies with the same // TODO: Revisit this later: What about two studies with the same
// study date? // study date?
@ -429,10 +463,10 @@ HP.ProtocolEngine = class ProtocolEngine {
return; return;
} }
const priorStudyInstanceUID = priorStudy.getStudyInstanceUID(); const priorStudyObjectID = priorStudy.getObjectID();
// Check if study metadata is already in studies list // Check if study metadata is already in studies list
if (this.studies.find(study => study.getStudyInstanceUID() === priorStudyInstanceUID)) { if (this.studies.find(study => study.getObjectID() === priorStudyObjectID)) {
return; return;
} }
@ -441,6 +475,12 @@ HP.ProtocolEngine = class ProtocolEngine {
// Set the custom attribute abstractPriorValue for the study metadata // Set the custom attribute abstractPriorValue for the study metadata
studyMetadata.setCustomAttribute('abstractPriorValue', abstractPriorValue); studyMetadata.setCustomAttribute('abstractPriorValue', abstractPriorValue);
// Also add custom attribute
const firstInstance = studyMetadata.getFirstInstance();
if (firstInstance instanceof InstanceMetadata) {
firstInstance.setCustomAttribute('abstractPriorValue', abstractPriorValue);
}
// Insert the new study metadata // Insert the new study metadata
this.studies.push(studyMetadata); this.studies.push(studyMetadata);
@ -448,14 +488,14 @@ HP.ProtocolEngine = class ProtocolEngine {
this.updateViewports(); this.updateViewports();
}, error => { }, error => {
OHIF.log.warn(error); OHIF.log.warn(error);
throw new OHIFError(`ProtocolEngine::matchImages could not get study metadata for studyInstanceUID: ${priorStudyInstanceUID}`); throw new OHIFError(`ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`);
}); });
} }
// TODO: Add relative Date / time // TODO: Add relative Date / time
}); });
this.studies.forEach(study => { this.studies.forEach(study => {
const studyMatchDetails = HP.match(study, studyMatchingRules); const studyMatchDetails = HP.match(study.getFirstInstance(), studyMatchingRules);
if ((studyMatchingRules.length && !studyMatchDetails.score) || if ((studyMatchingRules.length && !studyMatchDetails.score) ||
studyMatchDetails.score < highestStudyMatchingScore) { studyMatchDetails.score < highestStudyMatchingScore) {
return; return;
@ -464,7 +504,7 @@ HP.ProtocolEngine = class ProtocolEngine {
highestStudyMatchingScore = studyMatchDetails.score; highestStudyMatchingScore = studyMatchDetails.score;
study.forEachSeries(series => { study.forEachSeries(series => {
const seriesMatchDetails = HP.match(series, seriesMatchingRules); const seriesMatchDetails = HP.match(series.getFirstInstance(), seriesMatchingRules);
if ((seriesMatchingRules.length && !seriesMatchDetails.score) || if ((seriesMatchingRules.length && !seriesMatchDetails.score) ||
seriesMatchDetails.score < highestSeriesMatchingScore) { seriesMatchDetails.score < highestSeriesMatchingScore) {
return; return;
@ -478,7 +518,7 @@ HP.ProtocolEngine = class ProtocolEngine {
// See https://ohiforg.atlassian.net/browse/LT-227 // See https://ohiforg.atlassian.net/browse/LT-227
// sopClassUid = x00080016 // sopClassUid = x00080016
// rows = x00280010 // rows = x00280010
if (!OHIF.viewerbase.isImage(instance.getRawValue('x00080016')) && !instance.getRawValue('x00280010')) { if (!OHIF.viewerbase.isImage(instance.getTagValue('x00080016')) && !instance.getTagValue('x00280010')) {
return; return;
} }
@ -509,9 +549,9 @@ HP.ProtocolEngine = class ProtocolEngine {
matchDetails: matchDetails, matchDetails: matchDetails,
sortingInfo: { sortingInfo: {
score: totalMatchScore, score: totalMatchScore,
study: instance.getRawValue('x00080020') + instance.getRawValue('x00080030'), // StudyDate = x00080020 StudyTime = x00080030 study: instance.getTagValue('x00080020') + instance.getTagValue('x00080030'), // StudyDate = x00080020 StudyTime = x00080030
series: parseInt(instance.getRawValue('x00200011')), // TODO: change for seriesDateTime SeriesNumber = x00200011 series: parseInt(instance.getTagValue('x00200011')), // TODO: change for seriesDateTime SeriesNumber = x00200011
instance: parseInt(instance.getRawValue('x00200013')) // TODO: change for acquisitionTime InstanceNumber = x00200013 instance: parseInt(instance.getTagValue('x00200013')) // TODO: change for acquisitionTime InstanceNumber = x00200013
} }
}; };

View File

@ -147,10 +147,12 @@ var defaultStrategy = (function () {
Meteor.startup(() => { Meteor.startup(() => {
HP.ProtocolStore.setStrategy(defaultStrategy); HP.ProtocolStore.setStrategy(defaultStrategy);
HP.ProtocolStore.onReady(() => { HP.ProtocolStore.onReady(() => {
console.log('Inserting default protocols'); console.log('Inserting default protocols');
HP.ProtocolStore.addProtocol(HP.defaultProtocol); HP.ProtocolStore.addProtocol(HP.defaultProtocol);
//HP.ProtocolStore.addProtocol(HP.testProtocol); //HP.ProtocolStore.addProtocol(HP.testProtocol);
HP.demoProtocols.forEach(protocol => {
HP.ProtocolStore.addProtocol(protocol);
});
}); });
}); });

View File

@ -8,8 +8,8 @@ export class OHIFInstanceMetadata extends InstanceMetadata {
/** /**
* @param {Object} Instance object. * @param {Object} Instance object.
*/ */
constructor(data, series, study) { constructor(data, series, study, uid) {
super(data); super(data, uid);
this.init(series, study); this.init(series, study);
} }
@ -52,7 +52,7 @@ export class OHIFInstanceMetadata extends InstanceMetadata {
} }
// Override // Override
getRawValue(tagOrProperty, defaultValue, bypassCache) { getTagValue(tagOrProperty, defaultValue, bypassCache) {
// check if this property has been cached... // check if this property has been cached...
if (tagOrProperty in this._cache && bypassCache !== true) { if (tagOrProperty in this._cache && bypassCache !== true) {

View File

@ -6,8 +6,8 @@ export class OHIFSeriesMetadata extends Viewerbase.metadata.SeriesMetadata {
/** /**
* @param {Object} Series object. * @param {Object} Series object.
*/ */
constructor(data, study) { constructor(data, study, uid) {
super(data); super(data, uid);
this.init(study); this.init(study);
} }

View File

@ -6,8 +6,8 @@ export class OHIFStudyMetadata extends Viewerbase.metadata.StudyMetadata {
/** /**
* @param {Object} Study object. * @param {Object} Study object.
*/ */
constructor(data) { constructor(data, uid) {
super(data); super(data, uid);
this.init(); this.init();
} }

View File

@ -43,9 +43,9 @@ export class OHIFStudyMetadataSource extends OHIF.viewerbase.StudyMetadataSource
return; return;
} }
this.getByInstanceUID(study.getStudyInstanceUID()).then(studyInfo => { this.getByInstanceUID(studyInstanceUID).then(studyInfo => {
// Create study metadata object // Create study metadata object
const studyMetadata = new OHIF.metadata.StudyMetadata(studyInfo); const studyMetadata = new OHIF.metadata.StudyMetadata(studyInfo, studyInfo.studyInstanceUid);
// Get Study display sets // Get Study display sets
const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata); const displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata);

View File

@ -3,27 +3,30 @@ import 'meteor/ohif:viewerbase';
// Local Dependencies // Local Dependencies
import { OHIFStudySummary } from './OHIFStudySummary'; import { OHIFStudySummary } from './OHIFStudySummary';
const StudyMetadata = OHIF.viewerbase.metadata.StudyMetadata; const { StudyMetadata, StudySummary } = OHIF.viewerbase.metadata;
const StudySummary = OHIF.viewerbase.metadata.StudySummary; const PATIENT_ID = 'x00100020';
const PatientID = 'x00100020'; const STUDY_DATE = 'x00080020';
const StudyDate = 'x00080020';
/**
* Get the priors of a given study
* @param {StudyMetadata|StudySummary} study An instance of StudyMetadata|StudySummary class to get it's priors
* @return {Array} An Array of StudySummary objects representing the study priors for the given study or an empty array if none
*/
const getStudyPriors = study => { const getStudyPriors = study => {
const priorStudies = []; if (!(study instanceof StudyMetadata) && !(study instanceof StudySummary)) {
let patientID; throw new OHIF.viewerbase.OHIFError('getStudyPriors study must be an instance of StudySummary or StudyMetadata');
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... if (study instanceof StudyMetadata) {
study = study.getFirstInstance();
}
const priorStudies = [];
const patientID = study.getTagValue(PATIENT_ID); // PatientID
const studyDate = study.getTagValue(STUDY_DATE); // StudyDate
// Find prior studies in global StudyListStudies Minimongo collection
const cursor = StudyListStudies.find({ const cursor = StudyListStudies.find({
patientId: patientID, patientId: patientID,
studyDate: { studyDate: {
@ -35,10 +38,9 @@ const getStudyPriors = study => {
} }
}); });
// Create an OHIFStudySummary object for each prior study found... // Create an OHIFStudySummary object for each prior study found
cursor.forEach(study => { cursor.forEach(study => {
const summary = new OHIFStudySummary(); const summary = new OHIFStudySummary(study, null, study.studyInstanceUid);
summary.addTags(study);
priorStudies.push(summary); priorStudies.push(summary);
}); });

View File

@ -3,8 +3,7 @@ import 'meteor/ohif:viewerbase';
// Local Dependencies // Local Dependencies
import { getStudyPriors } from './getStudyPriors'; import { getStudyPriors } from './getStudyPriors';
const StudyMetadata = OHIF.viewerbase.metadata.StudyMetadata; const { StudyMetadata, StudySummary } = OHIF.viewerbase.metadata;
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. * Create a Map of study priors where the key of each entry is the StudyInstanceUID and its value is an array of StudySummary instances.
@ -17,10 +16,10 @@ const getStudyPriorsMap = studies => {
if (studies instanceof Array) { if (studies instanceof Array) {
studies.forEach(study => { studies.forEach(study => {
if (study instanceof StudyMetadata || study instanceof StudySummary) { if (study instanceof StudyMetadata || study instanceof StudySummary) {
const studyInstanceUID = study.getStudyInstanceUID(); const studyObjectID = study.getObjectID();
if (studyInstanceUID) { if (studyObjectID) {
const priors = getStudyPriors(study); const priors = getStudyPriors(study);
priorsMap.set(studyInstanceUID, priors); priorsMap.set(studyObjectID, priors);
} }
} }
}); });

View File

@ -130,7 +130,7 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
seriesInstanceUid, seriesInstanceUid,
displaySetInstanceUid, displaySetInstanceUid,
currentImageIdIndex, currentImageIdIndex,
viewport, viewport: viewport || data.viewport,
viewportIndex viewportIndex
}; };
@ -235,7 +235,7 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
// Call the handler function that represents the end of the image loading phase // Call the handler function that represents the end of the image loading phase
// (e.g. hide the progress text box) // (e.g. hide the progress text box)
endLoadingHandler(element); endLoadingHandler(element, image);
// Remove the 'empty' class from the viewport to hide any instruction text // Remove the 'empty' class from the viewport to hide any instruction text
element.classList.remove('empty'); element.classList.remove('empty');

View File

@ -1,7 +1,12 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { OHIF } from 'meteor/ohif:core'; import { OHIF } from 'meteor/ohif:core';
import { viewportUtils } from './viewportUtils'; import { viewportUtils } from './viewportUtils';
const WL_PRESET_CUSTOM = 'Custom';
const WL_PRESET_DEFAULT = 'Default';
// TODO: add this to a namespace definition // TODO: add this to a namespace definition
Meteor.startup(function() { Meteor.startup(function() {
OHIF.viewer.defaultWLPresets = { OHIF.viewer.defaultWLPresets = {
@ -27,30 +32,75 @@ Meteor.startup(function() {
} }
}; };
// For now setOHIFWLPresets(OHIF.viewer.defaultWLPresets);
OHIF.viewer.wlPresets = OHIF.viewer.defaultWLPresets;
}); });
function updateElementWLPresetData(element) {
const wlPresetData = cornerstone.getElementData(element, 'wlPreset');
const enabledElement = cornerstone.getEnabledElement(element);
const { viewport, image } = enabledElement;
const { windowCenter, windowWidth } = viewport.voi;
let presetName;
if (windowWidth === image.windowWidth && windowCenter === image.windowCenter) {
presetName = WL_PRESET_DEFAULT;
} else {
const WLPresets = OHIF.viewer.wlPresets;
for (let name in WLPresets) {
const preset = WLPresets[name];
if (windowCenter === preset.wc && windowWidth === preset.ww) {
presetName = name;
break;
}
}
}
wlPresetData.name = presetName || WL_PRESET_CUSTOM;
wlPresetData.ww = windowWidth;
wlPresetData.wc = windowCenter;
if (wlPresetData.name === WL_PRESET_CUSTOM) {
const custom = wlPresetData.custom || (wlPresetData.custom = Object.create(null));
custom.ww = windowWidth;
custom.wc = windowCenter;
}
}
/**
* Set specified W/L preset on given element on fallback to default W/L preset if the specified preset is not valid.
* @param {String} presetName The desired W/L preset to be applied
* @param {DOMElement} element An enabled viewport DOM Element.
*/
function applyWLPreset(presetName, element) { function applyWLPreset(presetName, element) {
OHIF.log.info('Applying WL Preset: ' + presetName); const wlPresets = OHIF.viewer.wlPresets;
if (presetName !== 'Custom') { const wlPresetData = cornerstone.getElementData(element, 'wlPreset');
const viewport = cornerstone.getViewport(element); const viewport = cornerstone.getViewport(element);
if (presetName === 'Default') { if (presetName === WL_PRESET_CUSTOM && wlPresetData.custom) {
viewport.voi.windowWidth = wlPresetData.custom.ww;
viewport.voi.windowCenter = wlPresetData.custom.wc;
} else if (presetName in wlPresets) {
const preset = wlPresets[presetName];
viewport.voi.windowWidth = preset.ww;
viewport.voi.windowCenter = preset.wc;
} else {
const enabledElement = cornerstone.getEnabledElement(element); const enabledElement = cornerstone.getEnabledElement(element);
viewport.voi.windowWidth = enabledElement.image.windowWidth; viewport.voi.windowWidth = enabledElement.image.windowWidth;
viewport.voi.windowCenter = enabledElement.image.windowCenter; viewport.voi.windowCenter = enabledElement.image.windowCenter;
} else { presetName = WL_PRESET_DEFAULT;
const preset = OHIF.viewer.wlPresets[presetName];
if(preset) {
viewport.voi.windowWidth = preset.ww;
viewport.voi.windowCenter = preset.wc;
}
} }
wlPresetData.name = presetName;
wlPresetData.ww = viewport.voi.windowWidth;
wlPresetData.wc = viewport.voi.windowCenter;
// Update the viewport // Update the viewport
cornerstone.setViewport(element, viewport); cornerstone.setViewport(element, viewport);
}
OHIF.log.info('WLPresets::Applying WL Preset: ' + presetName);
// Notify other components about W/L Preset changes
Session.set('OHIFWlPresetApplied', presetName);
} }
function applyWLPresetToActiveElement(presetName) { function applyWLPresetToActiveElement(presetName) {
@ -60,9 +110,6 @@ function applyWLPresetToActiveElement(presetName) {
} }
applyWLPreset(presetName, element); applyWLPreset(presetName, element);
// To perform reactivity in other components
Session.set('WLPresetActiveElement', presetName);
} }
/** /**
@ -70,7 +117,14 @@ function applyWLPresetToActiveElement(presetName) {
* @param {Object} wlPresets Object with wlPresets mapping * @param {Object} wlPresets Object with wlPresets mapping
*/ */
function setOHIFWLPresets(wlPresets) { function setOHIFWLPresets(wlPresets) {
OHIF.viewer.wlPresets = wlPresets; const hasOwn = Object.prototype.hasOwnProperty;
const presetMap = Object.create(null); // Objects without prototype have much faster lookup times
for (let name in wlPresets) {
if (hasOwn.call(wlPresets, name)) {
presetMap[name] = wlPresets[name];
}
}
OHIF.viewer.wlPresets = presetMap;
} }
/** /**
@ -80,7 +134,8 @@ function setOHIFWLPresets(wlPresets) {
const WLPresets = { const WLPresets = {
applyWLPreset, applyWLPreset,
applyWLPresetToActiveElement, applyWLPresetToActiveElement,
setOHIFWLPresets setOHIFWLPresets,
updateElementWLPresetData
}; };
export { WLPresets }; export { WLPresets };

View File

@ -1,14 +1,21 @@
import { Metadata } from './Metadata'; import { Metadata } from './Metadata';
import { OHIFError } from '../OHIFError'; import { OHIFError } from '../OHIFError';
/**
* ATTENTION! This class should never depend on StudyMetadata or SeriesMetadata classes as this could
* possibly cause circular dependency issues.
*/
const UNDEFINED = 'undefined'; const UNDEFINED = 'undefined';
const NUMBER = 'number'; const NUMBER = 'number';
const STRING = 'string'; const STRING = 'string';
const STUDY_INSTANCE_UID = 'x0020000d';
const SERIES_INSTANCE_UID = 'x0020000e';
export class InstanceMetadata extends Metadata { export class InstanceMetadata extends Metadata {
constructor(data) { constructor(data, uid) {
super(data); super(data, uid);
// Initialize Private Properties // Initialize Private Properties
Object.defineProperties(this, { Object.defineProperties(this, {
_sopInstanceUID: { _sopInstanceUID: {
@ -60,6 +67,20 @@ export class InstanceMetadata extends Metadata {
* Public Methods * Public Methods
*/ */
/**
* Returns the StudyInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
*/
getStudyInstanceUID() {
return this.getTagValue(STUDY_INSTANCE_UID, null);
}
/**
* Returns the SeriesInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
*/
getSeriesInstanceUID() {
return this.getTagValue(SERIES_INSTANCE_UID, null);
}
/** /**
* Returns the SOPInstanceUID of the current instance. * Returns the SOPInstanceUID of the current instance.
*/ */
@ -69,7 +90,7 @@ export class InstanceMetadata extends Metadata {
// @TODO: Improve this... (E.g.: blob data) // @TODO: Improve this... (E.g.: blob data)
getStringValue(tagOrProperty, index, defaultValue) { getStringValue(tagOrProperty, index, defaultValue) {
let value = this.getRawValue(tagOrProperty, defaultValue); let value = this.getTagValue(tagOrProperty, defaultValue);
if (typeof value !== STRING && typeof value !== UNDEFINED) { if (typeof value !== STRING && typeof value !== UNDEFINED) {
value = value.toString(); value = value.toString();
@ -80,7 +101,7 @@ export class InstanceMetadata extends Metadata {
// @TODO: Improve this... (E.g.: blob data) // @TODO: Improve this... (E.g.: blob data)
getFloatValue(tagOrProperty, index, defaultValue) { getFloatValue(tagOrProperty, index, defaultValue) {
let value = this.getRawValue(tagOrProperty, defaultValue); let value = this.getTagValue(tagOrProperty, defaultValue);
value = InstanceMetadata.getIndexedValue(value, index, defaultValue); value = InstanceMetadata.getIndexedValue(value, index, defaultValue);
if(value instanceof Array) { if(value instanceof Array) {
@ -96,7 +117,7 @@ export class InstanceMetadata extends Metadata {
// @TODO: Improve this... (E.g.: blob data) // @TODO: Improve this... (E.g.: blob data)
getIntValue(tagOrProperty, index, defaultValue) { getIntValue(tagOrProperty, index, defaultValue) {
let value = this.getRawValue(tagOrProperty, defaultValue); let value = this.getTagValue(tagOrProperty, defaultValue);
value = InstanceMetadata.getIndexedValue(value, index, defaultValue); value = InstanceMetadata.getIndexedValue(value, index, defaultValue);
if(value instanceof Array) { if(value instanceof Array) {
@ -111,13 +132,20 @@ export class InstanceMetadata extends Metadata {
} }
/** /**
* * @deprecated Please use getTagValue instead.
*/ */
getRawValue(tagOrProperty, defaultValue) { getRawValue(tagOrProperty, defaultValue) {
return this.getTagValue(tagOrProperty, defaultValue);
}
/**
* This function should be overriden by specialized classes in order to allow client libraries or viewers to take advantage of the Study Metadata API.
*/
getTagValue(tagOrProperty, defaultValue) {
/** /**
* Please override this method on a specialized class. * Please override this method on a specialized class.
*/ */
throw new OHIFError('InstanceMetadata::getRawValue is not overriden. Please, override it in a specialized class. See OHIFInstanceMetadata for example'); throw new OHIFError('InstanceMetadata::getTagValue is not overriden. Please, override it in a specialized class. See OHIFInstanceMetadata for example');
} }
/** /**

View File

@ -8,13 +8,17 @@ const NUMBER = 'number';
const FUNCTION = 'function'; const FUNCTION = 'function';
const OBJECT = 'object'; const OBJECT = 'object';
/**
* Class Definition
*/
export class Metadata { export class Metadata {
/** /**
* Constructor and Instance Methods * Constructor and Instance Methods
*/ */
constructor(data) { constructor(data, uid) {
// Define the main "_data" private property as an immutable property. // Define the main "_data" private property as an immutable property.
// IMPORTANT: This property can only be set during instance construction. // IMPORTANT: This property can only be set during instance construction.
Object.defineProperty(this, '_data', { Object.defineProperty(this, '_data', {
@ -24,7 +28,16 @@ export class Metadata {
value: data value: data
}); });
// Define "_custom" properties as an immutable property // Define the main "_uid" private property as an immutable property.
// IMPORTANT: This property can only be set during instance construction.
Object.defineProperty(this, '_uid', {
configurable: false,
enumerable: false,
writable: false,
value: uid
});
// Define "_custom" properties as an immutable property.
// IMPORTANT: This property can only be set during instance construction. // IMPORTANT: This property can only be set during instance construction.
Object.defineProperty(this, '_custom', { Object.defineProperty(this, '_custom', {
configurable: false, configurable: false,
@ -47,6 +60,13 @@ export class Metadata {
return propertyValue; return propertyValue;
} }
/**
* Get unique object ID
*/
getObjectID() {
return this._uid;
}
/** /**
* Set custom attribute value * Set custom attribute value
* @param {String} attribute Custom attribute name * @param {String} attribute Custom attribute name
@ -74,6 +94,20 @@ export class Metadata {
return attribute in this._custom; return attribute in this._custom;
} }
/**
* Set custom attributes in batch mode.
* @param {Object} attributeMap An object whose own properties will be used as custom attributes.
*/
setCustomAttributes(attributeMap) {
const _hasOwn = Object.prototype.hasOwnProperty;
const _custom = this._custom;
for (let attribute in attributeMap) {
if (_hasOwn.call(attributeMap, attribute)) {
_custom[attribute] = attributeMap[attribute];
}
}
}
/** /**
* Static Methods * Static Methods
*/ */

View File

@ -3,8 +3,8 @@ import { InstanceMetadata } from './InstanceMetadata';
export class SeriesMetadata extends Metadata { export class SeriesMetadata extends Metadata {
constructor(data) { constructor(data, uid) {
super(data); super(data, uid);
// Initialize Private Properties // Initialize Private Properties
Object.defineProperties(this, { Object.defineProperties(this, {
_seriesInstanceUID: { _seriesInstanceUID: {
@ -18,6 +18,12 @@ export class SeriesMetadata extends Metadata {
enumerable: false, enumerable: false,
writable: false, writable: false,
value: [] value: []
},
_firstInstance: {
configurable: false,
enumerable: false,
writable: true,
value: null
} }
}); });
// Initialize Public Properties // Initialize Public Properties
@ -77,6 +83,23 @@ export class SeriesMetadata extends Metadata {
return result; return result;
} }
/**
* Get the first instance of the current series retaining a consistent result across multiple calls.
* @return {InstanceMetadata} An instance of the InstanceMetadata class or null if it does not exist.
*/
getFirstInstance() {
let instance = this._firstInstance;
if (!(instance instanceof InstanceMetadata)) {
instance = null;
const found = this.getInstanceByIndex(0);
if (found instanceof InstanceMetadata) {
this._firstInstance = found;
instance = found;
}
}
return instance;
}
/** /**
* Find an instance by index. * Find an instance by index.
* @param {number} index An integer representing a list index. * @param {number} index An integer representing a list index.

View File

@ -1,12 +1,13 @@
import { Metadata } from './Metadata'; import { Metadata } from './Metadata';
import { SeriesMetadata } from './SeriesMetadata'; import { SeriesMetadata } from './SeriesMetadata';
import { InstanceMetadata } from './InstanceMetadata';
import { ImageSet } from '../ImageSet'; import { ImageSet } from '../ImageSet';
import { OHIFError } from '../OHIFError'; import { OHIFError } from '../OHIFError';
export class StudyMetadata extends Metadata { export class StudyMetadata extends Metadata {
constructor(data) { constructor(data, uid) {
super(data); super(data, uid);
// Initialize Private Properties // Initialize Private Properties
Object.defineProperties(this, { Object.defineProperties(this, {
_studyInstanceUID: { _studyInstanceUID: {
@ -26,6 +27,18 @@ export class StudyMetadata extends Metadata {
enumerable: false, enumerable: false,
writable: false, writable: false,
value: [] value: []
},
_firstSeries: {
configurable: false,
enumerable: false,
writable: true,
value: null
},
_firstInstance: {
configurable: false,
enumerable: false,
writable: true,
value: null
} }
}); });
// Initialize Public Properties // Initialize Public Properties
@ -233,21 +246,11 @@ export class StudyMetadata extends Metadata {
/** /**
* It sorts the series based on display sets order. Each series must be an instance * It sorts the series based on display sets order. Each series must be an instance
* of SeriesMetadata and each display sets must be an instance of ImageSet. * of SeriesMetadata and each display sets must be an instance of ImageSet.
* Both array must have the same length, otherwise it throws an error.
* Useful example of usage: * Useful example of usage:
* Study data provided by backend does not sort series at all and client-side * Study data provided by backend does not sort series at all and client-side
* needs series sorted by the same criteria used for sorting display sets. * needs series sorted by the same criteria used for sorting display sets.
*/ */
sortSeriesByDisplaySets() { sortSeriesByDisplaySets() {
// Check if the study is multiframe. If it is,
const firstInstance = this.getFirstInstance();
const isMultiframe = firstInstance.getRawValue('x00280008') > 1;
// @TODO: Check if is necessary to do a similar check for multiframe studies
// Check if both arrays have same length in case of non-multiframe studies
if (!isMultiframe && this.getSeriesCount() !== this.getDisplaySetCount()) {
throw new OHIFError('StudyMetadata::sortSeriesByDisplaySets series count and display set count does not match');
}
// Object for mapping display sets' index by seriesInstanceUid // Object for mapping display sets' index by seriesInstanceUid
const displaySetsMapping = {}; const displaySetsMapping = {};
@ -299,18 +302,40 @@ export class StudyMetadata extends Metadata {
} }
/** /**
* Get first instance of the first series * Get the first series of the current study retaining a consistent result across multiple calls.
* @return {InstanceMetadata} InstanceMetadata class object or undefined if it doenst exist * @return {SeriesMetadata} An instance of the SeriesMetadata class or null if it does not exist.
*/ */
getFirstInstance() { getFirstSeries() {
let firstInstance; let series = this._firstSeries;
const firstSeries = this.getSeriesByIndex(0); if (!(series instanceof SeriesMetadata)) {
series = null;
if (firstSeries) { const found = this.getSeriesByIndex(0);
firstInstance = firstSeries.getInstanceByIndex(0); if (found instanceof SeriesMetadata) {
this._firstSeries = found;
series = found;
}
}
return series;
} }
return firstInstance; /**
* Get the first instance of the current study retaining a consistent result across multiple calls.
* @return {InstanceMetadata} An instance of the InstanceMetadata class or null if it does not exist.
*/
getFirstInstance() {
let instance = this._firstInstance;
if (!(instance instanceof InstanceMetadata)) {
instance = null;
const firstSeries = this.getFirstSeries();
if (firstSeries instanceof SeriesMetadata) {
const found = firstSeries.getFirstInstance();
if (found instanceof InstanceMetadata) {
this._firstInstance = found;
instance = found;
}
}
}
return instance;
} }
} }

View File

@ -1,26 +1,24 @@
import { Metadata } from './Metadata';
import { OHIFError } from '../OHIFError';
import { DICOMTagDescriptions } from '../../DICOMTagDescriptions'; import { DICOMTagDescriptions } from '../../DICOMTagDescriptions';
const StudyInstanceUID = 'x0020000d'; /**
* Constants
*/
export class StudySummary { const STUDY_INSTANCE_UID = 'x0020000d';
constructor(tagMap, propertyMap) { /**
* Class Definition
*/
// Define the main immutable "_data" private property. export class StudySummary extends Metadata {
Object.defineProperties(this, {
_tags: { constructor(tagMap, attributeMap, uid) {
configurable: false,
enumerable: false, // Call the superclass constructor passing an plain object with no prototype to be used as the main "_data" attribute.
writable: false, const _data = Object.create(null);
value: Object.create(null) super(_data, uid);
},
_properties: {
configurable: false,
enumerable: false,
writable: false,
value: Object.create(null)
}
});
// Initialize internal tag map if first argument is given. // Initialize internal tag map if first argument is given.
if (tagMap !== void 0) { if (tagMap !== void 0) {
@ -28,67 +26,53 @@ export class StudySummary {
} }
// Initialize internal property map if second argument is given. // Initialize internal property map if second argument is given.
if (propertyMap !== void 0) { if (attributeMap !== void 0) {
this.addProperties(addProperties); this.setCustomAttributes(attributeMap);
} }
} }
getStudyInstanceUID() { getStudyInstanceUID() {
// This method should return null if StudyInstanceUID is not available to keep compatibility StudyMetadata API // This method should return null if StudyInstanceUID is not available to keep compatibility StudyMetadata API
return this.getTagValue(StudyInstanceUID) || null; return this.getTagValue(STUDY_INSTANCE_UID) || 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];
}
}
} }
/**
* Append tags to internal tag map.
* @param {Object} tagMap An object whose own properties will be used as tag values and appended to internal tag map.
*/
addTags(tagMap) { addTags(tagMap) {
const _hasOwn = Object.prototype.hasOwnProperty; const _hasOwn = Object.prototype.hasOwnProperty;
const _tags = this._tags; const _data = this._data;
for (let tag in tagMap) { for (let tag in tagMap) {
if (_hasOwn.call(tagMap, tag)) { if (_hasOwn.call(tagMap, tag)) {
const description = DICOMTagDescriptions.find(tag); const description = DICOMTagDescriptions.find(tag);
// When a description is available, use its tag as internal key... // When a description is available, use its tag as internal key...
if (description) { if (description) {
_tags[description.tag] = tagMap[tag]; _data[description.tag] = tagMap[tag];
} else { } else {
_tags[tag] = tagMap[tag]; _data[tag] = tagMap[tag];
} }
} }
} }
} }
tagExists(tagName) { tagExists(tagName) {
const _tags = this._tags; const _data = this._data;
const description = DICOMTagDescriptions.find(tagName); const description = DICOMTagDescriptions.find(tagName);
if (description) { if (description) {
return (description.tag in _tags); return (description.tag in _data);
} }
return (tagName in _tags); return (tagName in _data);
} }
getTagValue(tagName) { getTagValue(tagName) {
const _tags = this._tags; const _data = this._data;
const description = DICOMTagDescriptions.find(tagName); const description = DICOMTagDescriptions.find(tagName);
if (description) { if (description) {
return _tags[description.tag]; return _data[description.tag];
} }
return _tags[tagName]; return _data[tagName];
}
propertyExists(propertyName) {
return (propertyName in this._properties);
}
getPropertyValue(propertyName) {
return this._properties[propertyName];
} }
} }