feat: Add White Labelling, Hanging Protocols, and tons of Extensions / Mode fixes
* modified * new files in existing extensions/libraries/modes * fixed bugs and tests * Clean up * fix: viewport display fixed * Fixed configs for deployment * Removed unnecessary files and functions * Fixed default HP module * Added white labelling * Fixed hash routing * Removed unnecessary routers Co-authored-by: Alireza Sedghi <ar.sedghi@gmail.com>
This commit is contained in:
1 parent
0db81b30f3
commit
99b8dc4759
93 files changed
+3825
-1293
No files matched your search
@@ -45,7 +45,8 @@
|
||||
"mousetrap": "^1.6.3",
|
||||
"query-string": "^6.14.0",
|
||||
"object-hash": "2.1.1",
|
||||
"cornerstone-math": "0.1.9"
|
||||
"cornerstone-math": "0.1.9",
|
||||
"validate.js": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack-merge": "5.7.3"
|
||||
|
||||
@@ -17,9 +17,11 @@ function create({
|
||||
retrieve,
|
||||
store,
|
||||
reject,
|
||||
parseRouteParams,
|
||||
retrieveSeriesMetadata,
|
||||
deleteStudyMetadataPromise,
|
||||
getImageIdsForDisplaySet,
|
||||
getImageIdsForInstance,
|
||||
}) {
|
||||
const defaultQuery = {
|
||||
studies: {
|
||||
@@ -56,15 +58,16 @@ function create({
|
||||
};
|
||||
|
||||
const defaultReject = {};
|
||||
|
||||
return {
|
||||
query: query || defaultQuery,
|
||||
retrieve: retrieve || defaultRetrieve,
|
||||
reject: reject || defaultReject,
|
||||
store: store || defaultStore,
|
||||
getImageIdsForDisplaySet,
|
||||
parseRouteParams,
|
||||
retrieveSeriesMetadata,
|
||||
deleteStudyMetadataPromise,
|
||||
getImageIdsForDisplaySet,
|
||||
getImageIdsForInstance,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ class MetadataProvider {
|
||||
metadata = {
|
||||
modality: instance.Modality,
|
||||
seriesInstanceUID: instance.SeriesInstanceUID,
|
||||
seriesNumber: instance.SeriesNumber,
|
||||
seriesNumber: getNumberValues(instance.SeriesNumber),
|
||||
studyInstanceUID: instance.StudyInstanceUID,
|
||||
seriesDate,
|
||||
seriesTime,
|
||||
@@ -197,9 +197,9 @@ class MetadataProvider {
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
|
||||
metadata = {
|
||||
patientAge: instance.PatientAge,
|
||||
patientSize: instance.PatientSize,
|
||||
patientWeight: instance.PatientWeight,
|
||||
patientAge: getNumberValues(instance.PatientAge),
|
||||
patientSize: getNumberValues(instance.PatientSize),
|
||||
patientWeight: getNumberValues(instance.PatientWeight),
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE:
|
||||
@@ -228,51 +228,53 @@ class MetadataProvider {
|
||||
|
||||
metadata = {
|
||||
frameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
imageOrientationPatient: ImageOrientationPatient,
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
imagePositionPatient: instance.ImagePositionPatient,
|
||||
sliceThickness: instance.SliceThickness,
|
||||
sliceLocation: instance.SliceLocation,
|
||||
pixelSpacing: PixelSpacing,
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing,
|
||||
rows: getNumberValues(instance.Rows),
|
||||
columns: getNumberValues(instance.Columns),
|
||||
imageOrientationPatient: getNumberValues(ImageOrientationPatient),
|
||||
rowCosines: getNumberValues(rowCosines),
|
||||
columnCosines: getNumberValues(columnCosines),
|
||||
imagePositionPatient: getNumberValues(instance.ImagePositionPatient),
|
||||
sliceThickness: getNumberValues(instance.SliceThickness),
|
||||
sliceLocation: getNumberValues(instance.SliceLocation),
|
||||
pixelSpacing: getNumberValues(PixelSpacing),
|
||||
rowPixelSpacing: getNumberValues(rowPixelSpacing),
|
||||
columnPixelSpacing: getNumberValues(columnPixelSpacing),
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
|
||||
metadata = {
|
||||
samplesPerPixel: instance.SamplesPerPixel,
|
||||
photometricInterpretation: instance.PhotometricInterpretation,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
bitsAllocated: instance.BitsAllocated,
|
||||
bitsStored: instance.BitsStored,
|
||||
highBit: instance.HighBit,
|
||||
pixelRepresentation: instance.PixelRepresentation,
|
||||
planarConfiguration: instance.PlanarConfiguration,
|
||||
pixelAspectRatio: instance.PixelAspectRatio,
|
||||
smallestPixelValue: instance.SmallestPixelValue,
|
||||
largestPixelValue: instance.LargestPixelValue,
|
||||
samplesPerPixel: getNumberValues(instance.SamplesPerPixel),
|
||||
photometricInterpretation: getNumberValues(instance.PhotometricInterpretation),
|
||||
rows: getNumberValues(instance.Rows),
|
||||
columns: getNumberValues(instance.Columns),
|
||||
bitsAllocated: getNumberValues(instance.BitsAllocated),
|
||||
bitsStored: getNumberValues(instance.BitsStored),
|
||||
highBit: getNumberValues(instance.HighBit),
|
||||
pixelRepresentation: getNumberValues(instance.PixelRepresentation),
|
||||
planarConfiguration: getNumberValues(instance.PlanarConfiguration),
|
||||
pixelAspectRatio: getNumberValues(instance.PixelAspectRatio),
|
||||
smallestPixelValue: getNumberValues(instance.SmallestPixelValue),
|
||||
largestPixelValue: getNumberValues(instance.LargestPixelValue),
|
||||
redPaletteColorLookupTableDescriptor:
|
||||
instance.RedPaletteColorLookupTableDescriptor,
|
||||
getNumberValues(instance.RedPaletteColorLookupTableDescriptor),
|
||||
greenPaletteColorLookupTableDescriptor:
|
||||
instance.GreenPaletteColorLookupTableDescriptor,
|
||||
getNumberValues(instance.GreenPaletteColorLookupTableDescriptor),
|
||||
bluePaletteColorLookupTableDescriptor:
|
||||
instance.BluePaletteColorLookupTableDescriptor,
|
||||
getNumberValues(instance.BluePaletteColorLookupTableDescriptor),
|
||||
redPaletteColorLookupTableData:
|
||||
instance.RedPaletteColorLookupTableData,
|
||||
getNumberValues(instance.RedPaletteColorLookupTableData),
|
||||
greenPaletteColorLookupTableData:
|
||||
instance.GreenPaletteColorLookupTableData,
|
||||
getNumberValues(instance.GreenPaletteColorLookupTableData),
|
||||
bluePaletteColorLookupTableData:
|
||||
instance.BluePaletteColorLookupTableData,
|
||||
getNumberValues(instance.BluePaletteColorLookupTableData),
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
|
||||
const { WindowCenter, WindowWidth } = instance;
|
||||
|
||||
if (WindowCenter === undefined || WindowWidth === undefined) {
|
||||
return;
|
||||
}
|
||||
const windowCenter = Array.isArray(WindowCenter)
|
||||
? WindowCenter
|
||||
: [WindowCenter];
|
||||
@@ -281,15 +283,20 @@ class MetadataProvider {
|
||||
: [WindowWidth];
|
||||
|
||||
metadata = {
|
||||
windowCenter,
|
||||
windowWidth,
|
||||
windowCenter: getNumberValues(windowCenter),
|
||||
windowWidth: getNumberValues(windowWidth),
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
|
||||
const { RescaleIntercept, RescaleSlope } = instance;
|
||||
if (RescaleIntercept === undefined || RescaleSlope === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
metadata = {
|
||||
rescaleIntercept: instance.RescaleIntercept,
|
||||
rescaleSlope: instance.RescaleSlope,
|
||||
rescaleIntercept: getNumberValues(instance.RescaleIntercept),
|
||||
rescaleSlope: getNumberValues(instance.RescaleSlope),
|
||||
rescaleType: instance.RescaleType,
|
||||
};
|
||||
break;
|
||||
@@ -401,7 +408,7 @@ class MetadataProvider {
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
||||
metadata = {
|
||||
sopInstanceUid: instance.SOPInstanceUID,
|
||||
instanceNumber: instance.InstanceNumber,
|
||||
instanceNumber: getNumberValues(instance.InstanceNumber),
|
||||
lossyImageCompression: instance.LossyImageCompression,
|
||||
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
||||
lossyImageCompressionMethod: instance.LossyImageCompressionMethod,
|
||||
@@ -457,6 +464,31 @@ class MetadataProvider {
|
||||
|
||||
const metadataProvider = new MetadataProvider();
|
||||
|
||||
|
||||
/**
|
||||
* Returns the values as an array of javascript numbers
|
||||
*
|
||||
* @param element - The javascript object for the specified element in the metadata
|
||||
* @returns {*}
|
||||
*/
|
||||
function getNumberValues(element) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(element)) {
|
||||
const values = [];
|
||||
for (let i = 0; i < element.length; i++) {
|
||||
values.push(parseFloat(element[i]));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
return parseFloat(element)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default metadataProvider;
|
||||
|
||||
const WADO_IMAGE_LOADER_TAGS = {
|
||||
@@ -479,3 +511,4 @@ const WADO_IMAGE_LOADER_TAGS = {
|
||||
};
|
||||
|
||||
const INSTANCE = 'instance';
|
||||
const DICOMWEB = 'dicomweb';
|
||||
@@ -2,7 +2,12 @@ import MODULE_TYPES from './MODULE_TYPES.js';
|
||||
import log from './../log.js';
|
||||
|
||||
export default class ExtensionManager {
|
||||
constructor({ commandsManager, servicesManager, hotkeysManager, appConfig = {} }) {
|
||||
constructor({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
hotkeysManager,
|
||||
appConfig = {},
|
||||
}) {
|
||||
this.modules = {};
|
||||
this.registeredExtensionIds = [];
|
||||
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
||||
@@ -52,7 +57,7 @@ export default class ExtensionManager {
|
||||
onModeEnter({
|
||||
servicesManager: _servicesManager,
|
||||
commandsManager: _commandsManager,
|
||||
hotkeysManager: _hotkeysManager
|
||||
hotkeysManager: _hotkeysManager,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -115,9 +120,7 @@ export default class ExtensionManager {
|
||||
*/
|
||||
registerExtension = (extension, configuration = {}, dataSources = []) => {
|
||||
if (!extension) {
|
||||
throw new Error(
|
||||
'Attempting to register a null/undefined extension.'
|
||||
);
|
||||
throw new Error('Attempting to register a null/undefined extension.');
|
||||
}
|
||||
|
||||
let extensionId = extension.id;
|
||||
@@ -191,6 +194,13 @@ export default class ExtensionManager {
|
||||
] = element;
|
||||
});
|
||||
break;
|
||||
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||
extensionModule.forEach(element => {
|
||||
this.modulesMap[
|
||||
`${extensionId}.${moduleType}.${element.name}`
|
||||
] = element;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Module type invalid: ${moduleType}`);
|
||||
}
|
||||
@@ -224,6 +234,10 @@ export default class ExtensionManager {
|
||||
return this.dataSourceMap[this.activeDataSource];
|
||||
};
|
||||
|
||||
getDataSource = () => {
|
||||
return this.dataSourceMap[this.activeDataSource];
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} moduleType
|
||||
@@ -288,6 +302,32 @@ export default class ExtensionManager {
|
||||
});
|
||||
}
|
||||
|
||||
_initHangingProtocolModule(extensionModule, extensionId) {
|
||||
extensionModule.forEach(element => {
|
||||
const namespace = `${extensionId}.${MODULE_TYPES.HANGING_PROTOCOL}.${element.name}`;
|
||||
|
||||
dataSources.forEach(dataSource => {
|
||||
if (dataSource.namespace === namespace) {
|
||||
const dataSourceInstance = element.createDataSource(
|
||||
dataSource.configuration
|
||||
);
|
||||
|
||||
if (this.dataSourceMap[dataSource.sourceName]) {
|
||||
this.dataSourceMap[dataSource.sourceName].push(dataSourceInstance);
|
||||
} else {
|
||||
this.dataSourceMap[dataSource.sourceName] = [dataSourceInstance];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
extensionModule.forEach(element => {
|
||||
this.modulesMap[
|
||||
`${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`
|
||||
] = element;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
|
||||
@@ -215,6 +215,9 @@ describe('ExtensionManager.js', () => {
|
||||
getDataSourcesModule: () => {
|
||||
return [{}];
|
||||
},
|
||||
getHangingProtocolModule: () => {
|
||||
return [{}];
|
||||
},
|
||||
getContextModule: () => {
|
||||
return [{}];
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ export default {
|
||||
VIEWPORT: 'viewportModule',
|
||||
CONTEXT: 'contextModule',
|
||||
LAYOUT_TEMPLATE: 'layoutTemplateModule',
|
||||
HANGING_PROTOCOL: 'hangingProtocolModule',
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
MeasurementService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
pubSubServiceInterface,
|
||||
} from './services';
|
||||
|
||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||
@@ -67,6 +68,7 @@ const OHIF = {
|
||||
HangingProtocolService,
|
||||
IWebApiDataSource,
|
||||
DicomMetadataStore,
|
||||
pubSubServiceInterface,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -101,6 +103,7 @@ export {
|
||||
HangingProtocolService,
|
||||
IWebApiDataSource,
|
||||
DicomMetadataStore,
|
||||
pubSubServiceInterface,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -10,21 +10,6 @@ describe('Top level exports', () => {
|
||||
'HotkeysManager',
|
||||
'ServicesManager',
|
||||
//
|
||||
'DicomMetadataStore',
|
||||
//
|
||||
'CineService',
|
||||
'DisplaySetService',
|
||||
'HangingProtocolService',
|
||||
'ToolBarService',
|
||||
'UINotificationService',
|
||||
'UIModalService',
|
||||
'UIDialogService',
|
||||
'UIViewportDialogService',
|
||||
'MeasurementService',
|
||||
'ViewportGridService',
|
||||
//
|
||||
'IWebApiDataSource',
|
||||
//
|
||||
'defaults',
|
||||
'utils',
|
||||
'hotkeys',
|
||||
@@ -37,7 +22,21 @@ describe('Top level exports', () => {
|
||||
'log',
|
||||
'DICOMWeb',
|
||||
'DICOMSR',
|
||||
'OHIF', //
|
||||
'OHIF',
|
||||
//
|
||||
'CineService',
|
||||
'UIDialogService',
|
||||
'UIModalService',
|
||||
'UINotificationService',
|
||||
'UIViewportDialogService',
|
||||
'DisplaySetService',
|
||||
'MeasurementService',
|
||||
'ToolBarService',
|
||||
'ViewportGridService',
|
||||
'HangingProtocolService',
|
||||
'IWebApiDataSource',
|
||||
'DicomMetadataStore',
|
||||
'pubSubServiceInterface'
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(OHIF).sort();
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
const { cachedStats } = data;
|
||||
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
};
|
||||
|
||||
export const polygonRoi = {
|
||||
id: 'PolygonRoi',
|
||||
name: 'Polygon',
|
||||
toolGroup: 'allTools',
|
||||
cornerstoneToolType: 'FreehandRoiTool',
|
||||
options: {
|
||||
measurementTable: {
|
||||
displayFunction,
|
||||
},
|
||||
caseProgress: {
|
||||
include: true,
|
||||
evaluate: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -54,6 +54,18 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
||||
);
|
||||
}
|
||||
|
||||
function _getInstanceFromImageId(imageId) {
|
||||
for (let study of _model.studies) {
|
||||
for (let series of study.series) {
|
||||
for (let instance of series.instances) {
|
||||
if (instance.imageId === imageId) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BaseImplementation = {
|
||||
EVENTS,
|
||||
listeners: {},
|
||||
@@ -82,7 +94,7 @@ const BaseImplementation = {
|
||||
madeInClient,
|
||||
});
|
||||
},
|
||||
addSeriesMetadata(seriesSummaryMetadata) {
|
||||
addSeriesMetadata(seriesSummaryMetadata, madeInClient = false) {
|
||||
const { StudyInstanceUID } = seriesSummaryMetadata[0];
|
||||
let study = _getStudy(StudyInstanceUID);
|
||||
if (!study) {
|
||||
@@ -98,6 +110,7 @@ const BaseImplementation = {
|
||||
|
||||
this._broadcastEvent(EVENTS.SERIES_ADDED, {
|
||||
StudyInstanceUID,
|
||||
madeInClient,
|
||||
});
|
||||
},
|
||||
addStudy(study) {
|
||||
@@ -124,6 +137,7 @@ const BaseImplementation = {
|
||||
getStudy: _getStudy,
|
||||
getSeries: _getSeries,
|
||||
getInstance: _getInstance,
|
||||
getInstanceFromImageId: _getInstanceFromImageId,
|
||||
};
|
||||
|
||||
const DicomMetadataStore = Object.assign(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const EVENTS = {
|
||||
STUDY_ADDED: 'event::dicomMetadataStore:studyAdded',
|
||||
INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded',
|
||||
SERIES_ADDED: 'event::dicomMetadataStore:seriesAdded',
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import createSeriesMetadata from './createSeriesMetadata';
|
||||
function createStudyMetadata(StudyInstanceUID) {
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
isLoaded: false,
|
||||
series: [],
|
||||
/**
|
||||
*
|
||||
@@ -24,7 +25,7 @@ function createStudyMetadata(StudyInstanceUID) {
|
||||
}
|
||||
},
|
||||
|
||||
setSeriesMetadata: function (SeriesInstanceUID, seriesMetadata) {
|
||||
setSeriesMetadata: function(SeriesInstanceUID, seriesMetadata) {
|
||||
let existingSeries = this.series.find(
|
||||
s => s.SeriesInstanceUID === SeriesInstanceUID
|
||||
);
|
||||
@@ -34,7 +35,7 @@ function createStudyMetadata(StudyInstanceUID) {
|
||||
} else {
|
||||
this.series.push(Object.assign({ instances: [] }, seriesMetadata));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -96,10 +96,13 @@ export default class DisplaySetService {
|
||||
/**
|
||||
*
|
||||
* @param {*} input
|
||||
* @param {*} param1
|
||||
* @param {*} param1: settings: initialViewportSettings by HP or callbacks after rendering
|
||||
* @returns {string[]} - added displaySetInstanceUIDs
|
||||
*/
|
||||
makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => {
|
||||
makeDisplaySets = (
|
||||
input,
|
||||
{ batch = false, madeInClient = false, settings = {} } = {}
|
||||
) => {
|
||||
if (!input || !input.length) {
|
||||
throw new Error('No instances were provided.');
|
||||
}
|
||||
@@ -116,12 +119,15 @@ export default class DisplaySetService {
|
||||
if (batch) {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const instances = input[i];
|
||||
const displaySets = this.makeDisplaySetForInstances(instances);
|
||||
const displaySets = this.makeDisplaySetForInstances(
|
||||
instances,
|
||||
settings
|
||||
);
|
||||
|
||||
displaySetsAdded = [...displaySetsAdded, displaySets];
|
||||
}
|
||||
} else {
|
||||
const displaySets = this.makeDisplaySetForInstances(input);
|
||||
const displaySets = this.makeDisplaySetForInstances(input, settings);
|
||||
|
||||
displaySetsAdded = displaySets;
|
||||
}
|
||||
@@ -145,7 +151,7 @@ export default class DisplaySetService {
|
||||
}
|
||||
};
|
||||
|
||||
makeDisplaySetForInstances(instances) {
|
||||
makeDisplaySetForInstances(instances, settings) {
|
||||
const instance = instances[0];
|
||||
|
||||
const existingDisplaySets =
|
||||
@@ -168,6 +174,13 @@ export default class DisplaySetService {
|
||||
} else {
|
||||
displaySets = handler.getDisplaySetsFromSeries(instances);
|
||||
|
||||
// applying hp-defined viewport settings to the displaysets
|
||||
displaySets.forEach(ds => {
|
||||
Object.keys(settings).forEach(key => {
|
||||
ds[key] = settings[key];
|
||||
});
|
||||
});
|
||||
|
||||
this._addDisplaySetsToCache(displaySets);
|
||||
this._addActiveDisplaySets(displaySets);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import validate from './lib/validator';
|
||||
|
||||
|
||||
/**
|
||||
* Match a Metadata instance against rules using Validate.js for validation.
|
||||
* @param {InstanceMetadata} metadataInstance Metadata instance object
|
||||
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
|
||||
* @return {Object} Matching Object with score and details (which rule passed or failed)
|
||||
*/
|
||||
const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
|
||||
const options = {
|
||||
format: 'grouped',
|
||||
};
|
||||
|
||||
const details = {
|
||||
passed: [],
|
||||
failed: [],
|
||||
};
|
||||
|
||||
let requiredFailed = false;
|
||||
let score = 0;
|
||||
|
||||
rules.forEach(rule => {
|
||||
const attribute = rule.attribute;
|
||||
|
||||
// Do not use the custom attribute from the metadataInstance since it is subject to change
|
||||
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
||||
const customAttribute = customAttributeRetrievalCallbacks[attribute];
|
||||
metadataInstance[attribute] = customAttribute.callback(metadataInstance);
|
||||
}
|
||||
|
||||
// Format the constraint as required by Validate.js
|
||||
const testConstraint = {
|
||||
[attribute]: rule.constraint,
|
||||
};
|
||||
|
||||
// Create a single attribute object to be validated, since metadataInstance is an
|
||||
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
|
||||
const attributeValue = metadataInstance[attribute];
|
||||
const attributeMap = {
|
||||
[attribute]: attributeValue,
|
||||
};
|
||||
|
||||
// Use Validate.js to evaluate the constraints on the specified metadataInstance
|
||||
let errorMessages;
|
||||
try {
|
||||
errorMessages = validate(attributeMap, testConstraint, [options]);
|
||||
} catch (e) {
|
||||
errorMessages = ['Something went wrong during validation.', e];
|
||||
}
|
||||
|
||||
if (!errorMessages) {
|
||||
// If no errorMessages were returned, then validation passed.
|
||||
|
||||
// Add the rule's weight to the total score
|
||||
score += parseInt(rule.weight, 10);
|
||||
|
||||
// Log that this rule passed in the matching details object
|
||||
details.passed.push({
|
||||
rule,
|
||||
});
|
||||
} else {
|
||||
// If errorMessages were present, then validation failed
|
||||
|
||||
// If the rule that failed validation was Required, then
|
||||
// mark that a required Rule has failed
|
||||
if (rule.required) {
|
||||
requiredFailed = true;
|
||||
}
|
||||
|
||||
// Log that this rule failed in the matching details object
|
||||
// and include any error messages
|
||||
details.failed.push({
|
||||
rule,
|
||||
errorMessages,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If a required Rule has failed Validation, set the matching score to zero
|
||||
if (requiredFailed) {
|
||||
score = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
score,
|
||||
details,
|
||||
requiredFailed,
|
||||
};
|
||||
};
|
||||
|
||||
const HPMatcher = {
|
||||
match,
|
||||
};
|
||||
|
||||
export { HPMatcher };
|
||||
@@ -1,74 +1,476 @@
|
||||
const name = 'HangingProtocolService';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
||||
import sortBy from '../../utils/sortBy.js';
|
||||
import ProtocolEngine from './ProtocolEngine';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
getState: _getState,
|
||||
setHangingProtocol: _setHangingProtocol,
|
||||
setHangingProtocolAppliedForViewport: _setHangingProtocolAppliedForViewport,
|
||||
setServiceImplementation,
|
||||
reset: _reset,
|
||||
set: _set,
|
||||
const EVENTS = {
|
||||
STAGE_CHANGE: 'event::hanging_protocol_stage_change',
|
||||
NEW_LAYOUT: 'event::hanging_protocol_new_layout',
|
||||
};
|
||||
|
||||
const serviceImplementation = {
|
||||
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
|
||||
_setHangingProtocol: () =>
|
||||
console.warn('_setHangingProtocol() NOT IMPLEMENTED'),
|
||||
_setHangingProtocolAppliedForViewport: () =>
|
||||
console.warn('_setHangingProtocolAppliedForViewport() NOT IMPLEMENTED'),
|
||||
_reset: () => console.warn('reset() NOT IMPLEMENTED'),
|
||||
_set: () => console.warn('set() NOT IMPLEMENTED'),
|
||||
const VIEWPORT_SETTING_TYPES = {
|
||||
PROPS: 'props',
|
||||
VIEWPORT: 'viewport',
|
||||
};
|
||||
|
||||
function _getState() {
|
||||
return serviceImplementation._getState();
|
||||
}
|
||||
|
||||
function _setHangingProtocol(hangingProtocol) {
|
||||
return serviceImplementation._setHangingProtocol(hangingProtocol);
|
||||
}
|
||||
|
||||
function _setHangingProtocolAppliedForViewport(hpAlreadyApplied) {
|
||||
return serviceImplementation._setHangingProtocolAppliedForViewport(
|
||||
hpAlreadyApplied
|
||||
);
|
||||
}
|
||||
|
||||
function _set(state) {
|
||||
return serviceImplementation._set(state);
|
||||
}
|
||||
|
||||
function _reset() {
|
||||
return serviceImplementation._reset({});
|
||||
}
|
||||
|
||||
function setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setHangingProtocol: setHangingProtocolImplementation,
|
||||
setHangingProtocolAppliedForViewport: setHangingProtocolAppliedForViewportImplementation,
|
||||
reset: resetImplementation,
|
||||
set: setImplementation,
|
||||
}) {
|
||||
if (getStateImplementation) {
|
||||
serviceImplementation._getState = getStateImplementation;
|
||||
class HangingProtocolService {
|
||||
constructor(commandsManager) {
|
||||
this._commandsManager = commandsManager;
|
||||
this.protocols = [];
|
||||
this.ProtocolEngine = undefined;
|
||||
this.protocol = undefined;
|
||||
this.stage = undefined;
|
||||
this.matchDetails = [];
|
||||
this.hpAlreadyApplied = [];
|
||||
this.studies = [];
|
||||
this.customViewportSettings = [];
|
||||
this.customAttributeRetrievalCallbacks = {};
|
||||
this.listeners = {};
|
||||
Object.defineProperty(this, 'EVENTS', {
|
||||
value: EVENTS,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
}
|
||||
if (setHangingProtocolImplementation) {
|
||||
serviceImplementation._setHangingProtocol = setHangingProtocolImplementation;
|
||||
|
||||
reset() {
|
||||
this.studies = [];
|
||||
this.protocols = [];
|
||||
this.hpAlreadyApplied = [];
|
||||
this.matchDetails = [];
|
||||
// this.ProtocolEngine.reset()
|
||||
}
|
||||
if (setHangingProtocolAppliedForViewportImplementation) {
|
||||
serviceImplementation._setHangingProtocolAppliedForViewport = setHangingProtocolAppliedForViewportImplementation;
|
||||
|
||||
getState() {
|
||||
return [this.matchDetails, this.hpAlreadyApplied];
|
||||
}
|
||||
if (resetImplementation) {
|
||||
serviceImplementation._reset = resetImplementation;
|
||||
|
||||
getProtocols() {
|
||||
return this.protocols;
|
||||
}
|
||||
if (setImplementation) {
|
||||
serviceImplementation._set = setImplementation;
|
||||
|
||||
addProtocols(protocols) {
|
||||
protocols.forEach(protocol => {
|
||||
if (this.protocols.indexOf(protocol) === -1) {
|
||||
this.protocols.push(protocol);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(studyMetaData, protocol) {
|
||||
if (!this.studies.includes(studyMetaData)) {
|
||||
this.studies.push(studyMetaData);
|
||||
}
|
||||
// copy here so we don't mutate it
|
||||
const metaData = Object.assign({}, studyMetaData);
|
||||
|
||||
this.ProtocolEngine = new ProtocolEngine(
|
||||
this.protocols,
|
||||
this.customAttributeRetrievalCallbacks
|
||||
);
|
||||
|
||||
// if there is no pre-defiend protocol
|
||||
if (!protocol || protocol.id === undefined) {
|
||||
const matchedProtocol = this.ProtocolEngine.run(metaData);
|
||||
this._setProtocol(matchedProtocol);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setProtocol(protocol);
|
||||
}
|
||||
|
||||
setHangingProtocolAppliedForViewport(i) {
|
||||
this.hpAlreadyApplied[i] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom setting that can be chosen in the HangingProtocol UI and applied to a Viewport
|
||||
*
|
||||
* @param settingId The ID used to refer to the setting (e.g. 'displayCADMarkers')
|
||||
* @param settingName The name of the setting to be displayed (e.g. 'Display CAD Markers')
|
||||
* @param options
|
||||
* @param callback A function to be run after a viewport is rendered with a series
|
||||
*/
|
||||
addCustomViewportSetting(...params) {
|
||||
this.customViewportSettings.push(...params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom attribute to be used in the HangingProtocol UI and matching rules, including a
|
||||
* callback that will be used to calculate the attribute value.
|
||||
*
|
||||
* @param attributeId The ID used to refer to the attribute (e.g. 'timepointType')
|
||||
* @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type')
|
||||
* @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image)
|
||||
*/
|
||||
addCustomAttribute(attributeId, attributeName, callback) {
|
||||
this.customAttributeRetrievalCallbacks[attributeId] = {
|
||||
name: attributeName,
|
||||
callback: callback,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the next protocol stage in the display set sequence
|
||||
*/
|
||||
nextProtocolStage() {
|
||||
console.log('ProtocolEngine::nextProtocolStage');
|
||||
|
||||
if (!this._setCurrentProtocolStage(1)) {
|
||||
console.log('ProtocolEngine::nextProtocolStage failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the previous protocol stage in the display set sequence
|
||||
*/
|
||||
previousProtocolStage() {
|
||||
console.log('ProtocolEngine::previousProtocolStage');
|
||||
|
||||
if (!this._setCurrentProtocolStage(-1)) {
|
||||
console.log('ProtocolEngine::previousProtocolStage failed');
|
||||
}
|
||||
}
|
||||
|
||||
_setProtocol(protocol) {
|
||||
// TODO: Add proper Protocol class to validate the protocols
|
||||
// which are entered manually
|
||||
this.stage = 0;
|
||||
this.protocol = protocol;
|
||||
this._updateViewports(protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of Stages in the current Protocol or
|
||||
* undefined if no protocol or stages are set
|
||||
*/
|
||||
_getNumProtocolStages() {
|
||||
if (
|
||||
!this.protocol ||
|
||||
!this.protocol.stages ||
|
||||
!this.protocol.stages.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.protocol.stages.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current Stage from the current Protocol and stage index
|
||||
*
|
||||
* @returns {*} The Stage model for the currently displayed Stage
|
||||
*/
|
||||
_getCurrentStageModel() {
|
||||
return this.protocol.stages[this.stage];
|
||||
}
|
||||
|
||||
_updateViewports() {
|
||||
// Make sure we have an active protocol with a non-empty array of display sets
|
||||
if (!this._getNumProtocolStages()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the current stage
|
||||
const stageModel = this._getCurrentStageModel();
|
||||
|
||||
// If the current stage does not fulfill the requirements to be displayed,
|
||||
// stop here.
|
||||
if (
|
||||
!stageModel ||
|
||||
!stageModel.viewportStructure ||
|
||||
!stageModel.viewports ||
|
||||
!stageModel.viewports.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the layoutTemplate associated with the current display set's viewport structure
|
||||
// If no such template name exists, stop here.
|
||||
// const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
|
||||
const layoutTemplateName = 'gridLayout';
|
||||
if (!layoutTemplateName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the properties associated with the current display set's viewport structure template
|
||||
// If no such layout properties exist, stop here.
|
||||
const layoutProps = stageModel.viewportStructure.properties;
|
||||
if (!layoutProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { columns: numCols, rows: numRows } = layoutProps;
|
||||
this._broadcastChange(this.EVENTS.NEW_LAYOUT, {
|
||||
numRows,
|
||||
numCols,
|
||||
});
|
||||
|
||||
// Empty the matchDetails associated with the ProtocolEngine.
|
||||
// This will be used to store the pass/fail details and score
|
||||
// for each of the viewport matching procedures
|
||||
|
||||
// Loop through each viewport
|
||||
stageModel.viewports.forEach((viewport, viewportIndex) => {
|
||||
this.hpAlreadyApplied.push(false);
|
||||
const details = this._matchImages(viewport);
|
||||
|
||||
let currentMatch = details.bestMatch;
|
||||
|
||||
const currentViewportData = {
|
||||
viewportIndex,
|
||||
SeriesInstanceUID: currentMatch && currentMatch.SeriesInstanceUID,
|
||||
};
|
||||
|
||||
// Viewport Settings
|
||||
//
|
||||
// protocol defined callback
|
||||
const protocolCallbacks = viewport.viewportSettings.filter(
|
||||
setting => setting.type === VIEWPORT_SETTING_TYPES.PROPS
|
||||
);
|
||||
// manually added callback
|
||||
const customCallbacks = this.customViewportSettings.filter(
|
||||
setting => setting.type === VIEWPORT_SETTING_TYPES.PROPS
|
||||
);
|
||||
const callbacks = protocolCallbacks.concat(customCallbacks);
|
||||
|
||||
// if we have callbacks to applied at the app level or at the HP level
|
||||
if (callbacks.length) {
|
||||
currentViewportData.renderedCallback = (element, ToolBarService) => {
|
||||
callbacks.forEach(setting => {
|
||||
const { commandName, options } = setting;
|
||||
options.viewportIndex = viewportIndex;
|
||||
options.element = element;
|
||||
// Toolbar service to handle tool activation
|
||||
if (commandName === 'setToolActive') {
|
||||
ToolBarService.recordInteraction(options);
|
||||
return;
|
||||
}
|
||||
// other commands
|
||||
this._commandsManager.runCommand(commandName, options);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// initial viewport settings defined by protocol
|
||||
const protocolInitialViewport = viewport.viewportSettings.filter(
|
||||
setting => setting.type === VIEWPORT_SETTING_TYPES.VIEWPORT
|
||||
);
|
||||
// custom added initial viewport settings
|
||||
const customInitialViewport = this.customViewportSettings.filter(
|
||||
setting => setting.type === VIEWPORT_SETTING_TYPES.VIEWPORT
|
||||
);
|
||||
// TODO: conflict might happen between protocol and custom viewport settings
|
||||
const viewportSettings = protocolInitialViewport.concat(
|
||||
customInitialViewport
|
||||
);
|
||||
|
||||
if (viewportSettings.length) {
|
||||
const initialViewport = {};
|
||||
viewportSettings.forEach(setting => {
|
||||
const { options } = setting;
|
||||
if (!options) return;
|
||||
// Do not manipulate the hp settings
|
||||
const viewportOptions = cloneDeep(options);
|
||||
Object.entries(viewportOptions).forEach(([key, value]) => {
|
||||
initialViewport[key] = value;
|
||||
});
|
||||
});
|
||||
currentViewportData.initialViewport = initialViewport;
|
||||
}
|
||||
|
||||
this.matchDetails[viewportIndex] = currentViewportData;
|
||||
});
|
||||
}
|
||||
|
||||
// Match images given a list of Studies and a Viewport's image matching reqs
|
||||
_matchImages(viewport) {
|
||||
console.log('ProtocolEngine::matchImages');
|
||||
|
||||
// TODO: matching is applied on study and series level, instance
|
||||
// level matching needs to be added in future
|
||||
|
||||
const { studyMatchingRules, seriesMatchingRules } = viewport;
|
||||
|
||||
const matchingScores = [];
|
||||
let highestStudyMatchingScore = 0;
|
||||
let highestSeriesMatchingScore = 0;
|
||||
|
||||
this.studies.forEach(study => {
|
||||
const studyMatchDetails = this.ProtocolEngine.findMatch(
|
||||
study,
|
||||
studyMatchingRules
|
||||
);
|
||||
|
||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
if (
|
||||
studyMatchDetails.requiredFailed === true ||
|
||||
studyMatchDetails.score < highestStudyMatchingScore
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
highestStudyMatchingScore = studyMatchDetails.score;
|
||||
|
||||
study.series.forEach(aSeries => {
|
||||
const seriesMatchDetails = this.ProtocolEngine.findMatch(
|
||||
aSeries,
|
||||
seriesMatchingRules
|
||||
);
|
||||
|
||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
if (
|
||||
seriesMatchDetails.requiredFailed === true ||
|
||||
seriesMatchDetails.score < highestSeriesMatchingScore
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
highestSeriesMatchingScore = seriesMatchDetails.score;
|
||||
|
||||
const matchDetails = {
|
||||
passed: [],
|
||||
failed: [],
|
||||
};
|
||||
|
||||
matchDetails.passed = matchDetails.passed.concat(
|
||||
seriesMatchDetails.details.passed
|
||||
);
|
||||
matchDetails.passed = matchDetails.passed.concat(
|
||||
studyMatchDetails.details.passed
|
||||
);
|
||||
|
||||
matchDetails.failed = matchDetails.failed.concat(
|
||||
seriesMatchDetails.details.failed
|
||||
);
|
||||
matchDetails.failed = matchDetails.failed.concat(
|
||||
studyMatchDetails.details.failed
|
||||
);
|
||||
|
||||
const totalMatchScore =
|
||||
seriesMatchDetails.score + studyMatchDetails.score;
|
||||
|
||||
const imageDetails = {
|
||||
StudyInstanceUID: study.StudyInstanceUID,
|
||||
SeriesInstanceUID: aSeries.SeriesInstanceUID,
|
||||
matchingScore: totalMatchScore,
|
||||
matchDetails: matchDetails,
|
||||
sortingInfo: {
|
||||
score: totalMatchScore,
|
||||
study: study.StudyInstanceUID,
|
||||
series: parseInt(aSeries.SeriesNumber),
|
||||
},
|
||||
};
|
||||
|
||||
matchingScores.push(imageDetails);
|
||||
});
|
||||
});
|
||||
|
||||
// Sort the matchingScores
|
||||
const sortingFunction = sortBy(
|
||||
{
|
||||
name: 'score',
|
||||
reverse: true,
|
||||
},
|
||||
{
|
||||
name: 'study',
|
||||
reverse: true,
|
||||
},
|
||||
{
|
||||
name: 'series',
|
||||
}
|
||||
);
|
||||
matchingScores.sort((a, b) =>
|
||||
sortingFunction(a.sortingInfo, b.sortingInfo)
|
||||
);
|
||||
|
||||
const bestMatch = matchingScores[0];
|
||||
|
||||
console.log('ProtocolEngine::matchImages bestMatch', bestMatch);
|
||||
|
||||
return {
|
||||
bestMatch,
|
||||
matchingScores,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the next stage is available
|
||||
* @return {Boolean} True if next stage is available or false otherwise
|
||||
*/
|
||||
_isNextStageAvailable() {
|
||||
const numberOfStages = this._getNumProtocolStages();
|
||||
|
||||
return this.stage + 1 < numberOfStages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the previous stage is available
|
||||
* @return {Boolean} True if previous stage is available or false otherwise
|
||||
*/
|
||||
_isPreviousStageAvailable() {
|
||||
return this.stage - 1 >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the current stage to a new stage index in the display set sequence.
|
||||
* It checks if the next stage exists.
|
||||
*
|
||||
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
|
||||
* @return {Boolean} True if new stage has set or false, otherwise
|
||||
*/
|
||||
_setCurrentProtocolStage(stageAction) {
|
||||
//reseting the applied protocols
|
||||
this.hpAlreadyApplied = [];
|
||||
// Check if previous or next stage is available
|
||||
if (stageAction === -1 && !this._isPreviousStageAvailable()) {
|
||||
return false;
|
||||
} else if (stageAction === 1 && !this._isNextStageAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sets the new stage
|
||||
this.stage += stageAction;
|
||||
|
||||
// Log the new stage
|
||||
console.log(
|
||||
`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`
|
||||
);
|
||||
|
||||
// Since stage has changed, we need to update the viewports
|
||||
// and redo matchings
|
||||
this._updateViewports();
|
||||
|
||||
// Everything went well
|
||||
this._broadcastChange(this.EVENTS.STAGE_CHANGE, {
|
||||
matchDetails: this.matchDetails,
|
||||
hpAlreadyApplied: this.hpAlreadyApplied,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Broadcasts hanging protocols changes.
|
||||
*
|
||||
* @param {string} eventName The event name.add
|
||||
* @param {object} eventData.source The measurement source.
|
||||
* @param {object} eventData.measurement The measurement.
|
||||
* @param {boolean} eventData.notYetUpdatedAtSource True if the measurement was edited
|
||||
* within the measurement service and the source needs to update.
|
||||
* @return void
|
||||
*/
|
||||
_broadcastChange(eventName, eventData) {
|
||||
const hasListeners = Object.keys(this.listeners).length > 0;
|
||||
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
||||
|
||||
if (hasListeners && hasCallbacks) {
|
||||
this.listeners[eventName].forEach(listener => {
|
||||
listener.callback(eventData);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
export default HangingProtocolService;
|
||||
export { EVENTS };
|
||||
@@ -0,0 +1,789 @@
|
||||
import { HPMatcher } from './HPMatcher.js';
|
||||
import { sortByScore } from './lib/sortByScore';
|
||||
|
||||
const deafultProtocol = {
|
||||
id: 'defaultProtocol',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
modifiedDate: '2021-02-23T19:22:08.894Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
protocolMatchingRules: [],
|
||||
stages: [
|
||||
{
|
||||
id: 'nwzau7jDkEkL8djfr',
|
||||
name: 'oneByOne',
|
||||
viewportStructure: {
|
||||
type: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 1,
|
||||
},
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
viewportSettings: [],
|
||||
imageMatchingRules: [],
|
||||
seriesMatchingRules: [],
|
||||
studyMatchingRules: [],
|
||||
},
|
||||
],
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
},
|
||||
],
|
||||
numberOfPriorsReferenced: -1,
|
||||
};
|
||||
|
||||
export default class ProtocolEngine {
|
||||
constructor(protocols, customAttributeRetrievalCallbacks) {
|
||||
this.protocols = protocols;
|
||||
this.customAttributeRetrievalCallbacks = customAttributeRetrievalCallbacks;
|
||||
this.matchedProtocols = new Map();
|
||||
this.matchedProtocolScores = {};
|
||||
this.study = undefined;
|
||||
}
|
||||
|
||||
run(studyMetaData) {
|
||||
this.study = studyMetaData;
|
||||
return this.getBestProtocolMatch();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Resets the ProtocolEngine to the best match
|
||||
// */
|
||||
// reset() {
|
||||
// const protocol = this.getBestProtocolMatch();
|
||||
|
||||
// this.setHangingProtocol(protocol);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Return the best matched Protocol to the current study or set of studies
|
||||
* @returns {*}
|
||||
*/
|
||||
getBestProtocolMatch() {
|
||||
// Run the matching to populate matchedProtocols Set and Map
|
||||
this.updateProtocolMatches();
|
||||
|
||||
// Retrieve the highest scoring Protocol
|
||||
const bestMatch = this._getHighestScoringProtocol();
|
||||
|
||||
console.log('ProtocolEngine::getBestProtocolMatch bestMatch', bestMatch);
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the MatchedProtocols Collection by running the matching procedure
|
||||
*/
|
||||
updateProtocolMatches() {
|
||||
console.log('ProtocolEngine::updateProtocolMatches');
|
||||
|
||||
// Clear all data currently in matchedProtocols
|
||||
this._clearMatchedProtocols();
|
||||
|
||||
// TODO: handle more than one study
|
||||
const study = this.study;
|
||||
const matched = this.findMatchByStudy(study);
|
||||
|
||||
// For each matched protocol, check if it is already in MatchedProtocols
|
||||
matched.forEach(matchedDetail => {
|
||||
const protocol = matchedDetail.protocol;
|
||||
if (!protocol) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it is not already in the MatchedProtocols Collection, insert it with its score
|
||||
if (!this.matchedProtocols.has(protocol.id)) {
|
||||
console.log(
|
||||
'ProtocolEngine::updateProtocolMatches inserting protocol match',
|
||||
matchedDetail
|
||||
);
|
||||
this.matchedProtocols.set(protocol.id, protocol);
|
||||
this.matchedProtocolScores[protocol.id] = matchedDetail.score;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
findMatch(metaData, rules) {
|
||||
return HPMatcher.match(
|
||||
metaData,
|
||||
rules,
|
||||
this.customAttributeRetrievalCallbacks
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the best protocols from Protocol Store, matching each protocol matching rules
|
||||
* with the given study. The best protocol are orded by score and returned in an array
|
||||
* @param {Object} study StudyMetadata instance object
|
||||
* @return {Array} Array of match objects or an empty array if no match was found
|
||||
* Each match object has the score of the matching and the matched
|
||||
* protocol
|
||||
*/
|
||||
findMatchByStudy(study) {
|
||||
const matched = [];
|
||||
|
||||
this.protocols.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();
|
||||
if (!rules) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run the matcher and get matching details
|
||||
const matchedDetails = this.findMatch(study, rules);
|
||||
const score = matchedDetails.score;
|
||||
|
||||
// The protocol matched some rule, add it to the matched list
|
||||
if (score > 0) {
|
||||
matched.push({
|
||||
score,
|
||||
protocol,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If no matches were found, select the default protocol
|
||||
if (!matched.length) {
|
||||
return [
|
||||
{
|
||||
score: 1,
|
||||
protocol: deafultProtocol,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Sort the matched list by score
|
||||
sortByScore(matched);
|
||||
|
||||
console.log('ProtocolEngine::findMatchByStudy matched', matched);
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
_clearMatchedProtocols() {
|
||||
this.matchedProtocols.clear();
|
||||
this.matchedProtocolScores = {};
|
||||
}
|
||||
|
||||
_largestKeyByValue(obj) {
|
||||
return Object.keys(obj).reduce((a, b) => (obj[a] > obj[b] ? a : b));
|
||||
}
|
||||
|
||||
_getHighestScoringProtocol() {
|
||||
if (!Object.keys(this.matchedProtocolScores).length) {
|
||||
return;
|
||||
}
|
||||
const highestScoringProtocolId = this._largestKeyByValue(
|
||||
this.matchedProtocolScores
|
||||
);
|
||||
return this.matchedProtocols.get(highestScoringProtocolId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the ProtocolEngine to the best match
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieves the current Stage from the current Protocol and stage index
|
||||
*
|
||||
* @returns {*} The Stage model for the currently displayed Stage
|
||||
*/
|
||||
// getCurrentStageModel() {
|
||||
// return this.protocol.stages[this.stage];
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Get the number of prior studies supplied in the priorStudies map property.
|
||||
// *
|
||||
// * @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
|
||||
// */
|
||||
// getNumberOfAvailablePriors(studyObjectID) {
|
||||
// return this.getAvailableStudyPriors(studyObjectID).length;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Get the array of prior studies from a specific study.
|
||||
// *
|
||||
// * @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
|
||||
// */
|
||||
// getAvailableStudyPriors(studyObjectID) {
|
||||
// const priors = this.priorStudies.get(studyObjectID);
|
||||
|
||||
// return priors instanceof Array ? priors : [];
|
||||
// }
|
||||
|
||||
// // Match images given a list of Studies and a Viewport's image matching reqs
|
||||
// matchImages(viewport, viewportIndex) {
|
||||
// log.trace('ProtocolEngine::matchImages');
|
||||
|
||||
// const {
|
||||
// studyMatchingRules,
|
||||
// seriesMatchingRules,
|
||||
// imageMatchingRules: instanceMatchingRules,
|
||||
// } = viewport;
|
||||
|
||||
// const matchingScores = [];
|
||||
// const currentStudy = this.studies[0]; // @TODO: Should this be: this.studies[this.currentStudy] ???
|
||||
// const firstInstance = currentStudy.getFirstInstance();
|
||||
|
||||
// let highestStudyMatchingScore = 0;
|
||||
// let highestSeriesMatchingScore = 0;
|
||||
|
||||
// // Set custom attribute for study metadata and it's first instance
|
||||
// currentStudy.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
|
||||
// if (firstInstance instanceof InstanceMetadata) {
|
||||
// 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];
|
||||
// const validator = Object.keys(rule.constraint[validatorType])[0];
|
||||
|
||||
// let abstractPriorValue = rule.constraint[validatorType][validator];
|
||||
// abstractPriorValue = parseInt(abstractPriorValue, 10);
|
||||
// // TODO: Restrict or clarify validators for abstractPriorValue?
|
||||
|
||||
// // 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 = priorStudies[priorStudies.length - 1];
|
||||
// } else {
|
||||
// const studyIndex = Math.max(abstractPriorValue - 1, 0);
|
||||
// priorStudy = priorStudies[studyIndex];
|
||||
// }
|
||||
|
||||
// // Invalid data
|
||||
// if (!priorStudy instanceof StudyMetadata) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const priorStudyObjectID = priorStudy.getObjectID();
|
||||
|
||||
// // Check if study metadata is already in studies list
|
||||
// if (
|
||||
// this.studies.find(study => study.getObjectID() === priorStudyObjectID)
|
||||
// ) {
|
||||
// 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(
|
||||
// ABSTRACT_PRIOR_VALUE,
|
||||
// abstractPriorValue
|
||||
// );
|
||||
|
||||
// // Also add custom attribute
|
||||
// const firstInstance = studyMetadata.getFirstInstance();
|
||||
// if (firstInstance instanceof InstanceMetadata) {
|
||||
// firstInstance.setCustomAttribute(
|
||||
// ABSTRACT_PRIOR_VALUE,
|
||||
// abstractPriorValue
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Insert the new study metadata
|
||||
// this.studies.push(studyMetadata);
|
||||
|
||||
// // Update the viewport to refresh layout manager with new study
|
||||
// this.updateViewports(viewportIndex);
|
||||
// },
|
||||
// error => {
|
||||
// log.warn(error);
|
||||
// throw new OHIFError(
|
||||
// `ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`
|
||||
// );
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// // TODO: Add relative Date / time
|
||||
// });
|
||||
|
||||
// this.studies.forEach(study => {
|
||||
// const studyMatchDetails = HPMatcher.match(
|
||||
// study.getFirstInstance(),
|
||||
// studyMatchingRules
|
||||
// );
|
||||
|
||||
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
// if (
|
||||
// studyMatchDetails.requiredFailed === true ||
|
||||
// studyMatchDetails.score < highestStudyMatchingScore
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// highestStudyMatchingScore = studyMatchDetails.score;
|
||||
|
||||
// study.forEachSeries(series => {
|
||||
// const seriesMatchDetails = HPMatcher.match(
|
||||
// series.getFirstInstance(),
|
||||
// seriesMatchingRules
|
||||
// );
|
||||
|
||||
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
// if (
|
||||
// seriesMatchDetails.requiredFailed === true ||
|
||||
// seriesMatchDetails.score < highestSeriesMatchingScore
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// highestSeriesMatchingScore = seriesMatchDetails.score;
|
||||
|
||||
// series.forEachInstance((instance, index) => {
|
||||
// // This tests to make sure there is actually image data in this instance
|
||||
// // TODO: Change this when we add PDF and MPEG support
|
||||
// // See https://ohiforg.atlassian.net/browse/LT-227
|
||||
// if (
|
||||
// !isImage(instance.getTagValue('SOPClassUID')) &&
|
||||
// !instance.getTagValue('Rows')
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const instanceMatchDetails = HPMatcher.match(
|
||||
// instance,
|
||||
// instanceMatchingRules
|
||||
// );
|
||||
|
||||
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
// if (instanceMatchDetails.requiredFailed === true) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const matchDetails = {
|
||||
// passed: [],
|
||||
// failed: [],
|
||||
// };
|
||||
|
||||
// matchDetails.passed = matchDetails.passed.concat(
|
||||
// instanceMatchDetails.details.passed
|
||||
// );
|
||||
// matchDetails.passed = matchDetails.passed.concat(
|
||||
// seriesMatchDetails.details.passed
|
||||
// );
|
||||
// matchDetails.passed = matchDetails.passed.concat(
|
||||
// studyMatchDetails.details.passed
|
||||
// );
|
||||
|
||||
// matchDetails.failed = matchDetails.failed.concat(
|
||||
// instanceMatchDetails.details.failed
|
||||
// );
|
||||
// matchDetails.failed = matchDetails.failed.concat(
|
||||
// seriesMatchDetails.details.failed
|
||||
// );
|
||||
// matchDetails.failed = matchDetails.failed.concat(
|
||||
// studyMatchDetails.details.failed
|
||||
// );
|
||||
|
||||
// const totalMatchScore =
|
||||
// instanceMatchDetails.score +
|
||||
// seriesMatchDetails.score +
|
||||
// studyMatchDetails.score;
|
||||
// const currentSOPInstanceUID = instance.getSOPInstanceUID();
|
||||
|
||||
// const imageDetails = {
|
||||
// StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
// SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
// SOPInstanceUID: currentSOPInstanceUID,
|
||||
// currentImageIdIndex: index,
|
||||
// matchingScore: totalMatchScore,
|
||||
// matchDetails: matchDetails,
|
||||
// sortingInfo: {
|
||||
// score: totalMatchScore,
|
||||
// study:
|
||||
// instance.getTagValue('StudyDate') +
|
||||
// instance.getTagValue('StudyTime'),
|
||||
// series: parseInt(instance.getTagValue('SeriesNumber')), // TODO: change for seriesDateTime
|
||||
// instance: parseInt(instance.getTagValue('InstanceNumber')), // TODO: change for acquisitionTime
|
||||
// },
|
||||
// };
|
||||
|
||||
// // Find the displaySet
|
||||
// const displaySet = study.findDisplaySet(displaySet =>
|
||||
// displaySet.images.find(
|
||||
// image => image.getSOPInstanceUID() === currentSOPInstanceUID
|
||||
// )
|
||||
// );
|
||||
|
||||
// // If the instance was found, set the displaySet ID
|
||||
// if (displaySet) {
|
||||
// imageDetails.displaySetInstanceUID = displaySet.getUID();
|
||||
// imageDetails.imageId = instance.getImageId();
|
||||
// }
|
||||
|
||||
// matchingScores.push(imageDetails);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// // Sort the matchingScores
|
||||
// const sortingFunction = sortBy(
|
||||
// {
|
||||
// name: 'score',
|
||||
// reverse: true,
|
||||
// },
|
||||
// {
|
||||
// name: 'study',
|
||||
// reverse: true,
|
||||
// },
|
||||
// {
|
||||
// name: 'instance',
|
||||
// },
|
||||
// {
|
||||
// name: 'series',
|
||||
// }
|
||||
// );
|
||||
// matchingScores.sort((a, b) =>
|
||||
// sortingFunction(a.sortingInfo, b.sortingInfo)
|
||||
// );
|
||||
|
||||
// const bestMatch = matchingScores[0];
|
||||
|
||||
// log.trace('ProtocolEngine::matchImages bestMatch', bestMatch);
|
||||
|
||||
// return {
|
||||
// bestMatch,
|
||||
// matchingScores,
|
||||
// };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Sets the current layout
|
||||
*
|
||||
* @param {number} numRows
|
||||
* @param {number} numColumns
|
||||
*/
|
||||
// setLayout(numRows, numColumns) {
|
||||
// if (numRows < 1 && numColumns < 1) {
|
||||
// log.error(`Invalid layout ${numRows} x ${numColumns}`);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (typeof this.options.setLayout !== 'function') {
|
||||
// log.error('Hanging Protocol Engine setLayout callback is not defined');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let viewports = [];
|
||||
// const numViewports = numRows * numColumns;
|
||||
|
||||
// for (let i = 0; i < numViewports; i++) {
|
||||
// viewports.push({});
|
||||
// }
|
||||
|
||||
// this.options.setLayout({ numRows, numColumns, viewports });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Rerenders viewports that are part of the current layout manager
|
||||
// * using the matching rules internal to each viewport.
|
||||
// *
|
||||
// * If this function is provided the index of a viewport, only the specified viewport
|
||||
// * is rerendered.
|
||||
// *
|
||||
// * @param viewportIndex
|
||||
// */
|
||||
// updateViewports(viewportIndex) {
|
||||
// log.trace(
|
||||
// `ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}`
|
||||
// );
|
||||
|
||||
// // Make sure we have an active protocol with a non-empty array of display sets
|
||||
// if (!this.getNumProtocolStages()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Retrieve the current stage
|
||||
// const stageModel = this.getCurrentStageModel();
|
||||
|
||||
// // If the current stage does not fulfill the requirements to be displayed,
|
||||
// // stop here.
|
||||
// if (
|
||||
// !stageModel ||
|
||||
// !stageModel.viewportStructure ||
|
||||
// !stageModel.viewports ||
|
||||
// !stageModel.viewports.length
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Retrieve the layoutTemplate associated with the current display set's viewport structure
|
||||
// // If no such template name exists, stop here.
|
||||
// const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
|
||||
// if (!layoutTemplateName) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Retrieve the properties associated with the current display set's viewport structure template
|
||||
// // If no such layout properties exist, stop here.
|
||||
// const layoutProps = stageModel.viewportStructure.properties;
|
||||
// if (!layoutProps) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Create an empty array to store the output viewportData
|
||||
// const viewportData = [];
|
||||
|
||||
// // Empty the matchDetails associated with the ProtocolEngine.
|
||||
// // This will be used to store the pass/fail details and score
|
||||
// // for each of the viewport matching procedures
|
||||
// this.matchDetails = [];
|
||||
|
||||
// // Loop through each viewport
|
||||
// stageModel.viewports.forEach((viewport, viewportIndex) => {
|
||||
// const details = this.matchImages(viewport, viewportIndex);
|
||||
|
||||
// this.matchDetails[viewportIndex] = details;
|
||||
|
||||
// // Convert any YES/NO values into true/false for Cornerstone
|
||||
// const cornerstoneViewportParams = {};
|
||||
|
||||
// // Cache viewportSettings keys
|
||||
// const viewportSettingsKeys = Object.keys(viewport.viewportSettings);
|
||||
|
||||
// viewportSettingsKeys.forEach(key => {
|
||||
// let value = viewport.viewportSettings[key];
|
||||
// if (value === 'YES') {
|
||||
// value = true;
|
||||
// } else if (value === 'NO') {
|
||||
// value = false;
|
||||
// }
|
||||
|
||||
// cornerstoneViewportParams[key] = value;
|
||||
// });
|
||||
|
||||
// // imageViewerViewports occasionally needs relevant layout data in order to set
|
||||
// // the element style of the viewport in question
|
||||
// const currentViewportData = {
|
||||
// viewportIndex,
|
||||
// viewport: cornerstoneViewportParams,
|
||||
// ...layoutProps,
|
||||
// };
|
||||
|
||||
// const customSettings = [];
|
||||
// viewportSettingsKeys.forEach(id => {
|
||||
// const setting = CustomViewportSettings[id];
|
||||
// if (!setting) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// customSettings.push({
|
||||
// id: id,
|
||||
// value: viewport.viewportSettings[id],
|
||||
// });
|
||||
// });
|
||||
|
||||
// currentViewportData.renderedCallback = element => {
|
||||
// //console.log('renderedCallback for ' + element.id);
|
||||
// customSettings.forEach(customSetting => {
|
||||
// log.trace(
|
||||
// `ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
|
||||
// );
|
||||
// log.trace(
|
||||
// `ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
|
||||
// );
|
||||
|
||||
// const setting = CustomViewportSettings[customSetting.id];
|
||||
// setting.callback(element, customSetting.value);
|
||||
// });
|
||||
// };
|
||||
|
||||
// let currentMatch = details.bestMatch;
|
||||
// let currentPosition = 1;
|
||||
// const scoresLength = details.matchingScores.length;
|
||||
// while (
|
||||
// currentPosition < scoresLength &&
|
||||
// viewportData.find(a => a.imageId === currentMatch.imageId)
|
||||
// ) {
|
||||
// currentMatch = details.matchingScores[currentPosition];
|
||||
// currentPosition++;
|
||||
// }
|
||||
|
||||
// if (currentMatch && currentMatch.imageId) {
|
||||
// currentViewportData.StudyInstanceUID = currentMatch.StudyInstanceUID;
|
||||
// currentViewportData.SeriesInstanceUID = currentMatch.SeriesInstanceUID;
|
||||
// currentViewportData.SOPInstanceUID = currentMatch.SOPInstanceUID;
|
||||
// currentViewportData.currentImageIdIndex =
|
||||
// currentMatch.currentImageIdIndex;
|
||||
// currentViewportData.displaySetInstanceUID =
|
||||
// currentMatch.displaySetInstanceUID;
|
||||
// currentViewportData.imageId = currentMatch.imageId;
|
||||
// }
|
||||
|
||||
// // @TODO Why should we throw an exception when a best match is not found? This was aborting the whole process.
|
||||
// // if (!currentViewportData.displaySetInstanceUID) {
|
||||
// // throw new OHIFError('ProtocolEngine::updateViewports No matching display set found?');
|
||||
// // }
|
||||
|
||||
// viewportData.push(currentViewportData);
|
||||
// });
|
||||
|
||||
// this.setLayout(layoutProps.Rows, layoutProps.Columns);
|
||||
|
||||
// if (typeof this.options.setViewportSpecificData !== 'function') {
|
||||
// log.error(
|
||||
// 'Hanging Protocol Engine setViewportSpecificData callback is not defined'
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // If viewportIndex is defined, then update only that viewport
|
||||
// if (viewportIndex !== undefined && viewportData[viewportIndex]) {
|
||||
// this.options.setViewportSpecificData(
|
||||
// viewportIndex,
|
||||
// viewportData[viewportIndex]
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Update all viewports
|
||||
// viewportData.forEach(viewportSpecificData => {
|
||||
// this.options.setViewportSpecificData(
|
||||
// viewportSpecificData.viewportIndex,
|
||||
// viewportSpecificData
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Sets the current Hanging Protocol to the specified Protocol
|
||||
// * An optional argument can also be used to prevent the updating of the Viewports
|
||||
// *
|
||||
// * @param newProtocol
|
||||
// * @param updateViewports
|
||||
// */
|
||||
// setHangingProtocol(newProtocol, updateViewports = true) {
|
||||
// log.trace('ProtocolEngine::setHangingProtocol newProtocol', newProtocol);
|
||||
// log.trace(
|
||||
// `ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`
|
||||
// );
|
||||
|
||||
// // Reset the array of newStageIds
|
||||
// this.newStageIds = [];
|
||||
|
||||
// if (Protocol.prototype.isPrototypeOf(newProtocol)) {
|
||||
// this.protocol = newProtocol;
|
||||
// } else {
|
||||
// this.protocol = new Protocol();
|
||||
// this.protocol.fromObject(newProtocol);
|
||||
// }
|
||||
|
||||
// this.stage = 0;
|
||||
|
||||
// // Update viewports by default
|
||||
// if (updateViewports) {
|
||||
// this.updateViewports();
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Check if the next stage is available
|
||||
// * @return {Boolean} True if next stage is available or false otherwise
|
||||
// */
|
||||
// isNextStageAvailable() {
|
||||
// const numberOfStages = this.getNumProtocolStages();
|
||||
|
||||
// return this.stage + 1 < numberOfStages;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Check if the previous stage is available
|
||||
// * @return {Boolean} True if previous stage is available or false otherwise
|
||||
// */
|
||||
// isPreviousStageAvailable() {
|
||||
// return this.stage - 1 >= 0;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Changes the current stage to a new stage index in the display set sequence.
|
||||
// * It checks if the next stage exists.
|
||||
// *
|
||||
// * @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
|
||||
// * @return {Boolean} True if new stage has set or false, otherwise
|
||||
// */
|
||||
// setCurrentProtocolStage(stageAction) {
|
||||
// // Check if previous or next stage is available
|
||||
// if (stageAction === -1 && !this.isPreviousStageAvailable()) {
|
||||
// return false;
|
||||
// } else if (stageAction === 1 && !this.isNextStageAvailable()) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// // Sets the new stage
|
||||
// this.stage += stageAction;
|
||||
|
||||
// // Log the new stage
|
||||
// log.trace(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
|
||||
|
||||
// // Since stage has changed, we need to update the viewports
|
||||
// // and redo matchings
|
||||
// this.updateViewports();
|
||||
|
||||
// // Everything went well
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Retrieves the number of Stages in the current Protocol or
|
||||
// * undefined if no protocol or stages are set
|
||||
// */
|
||||
// getNumProtocolStages() {
|
||||
// if (
|
||||
// !this.protocol ||
|
||||
// !this.protocol.stages ||
|
||||
// !this.protocol.stages.length
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// return this.protocol.stages.length;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Switches to the next protocol stage in the display set sequence
|
||||
// */
|
||||
// nextProtocolStage() {
|
||||
// log.trace('ProtocolEngine::nextProtocolStage');
|
||||
|
||||
// if (!this.setCurrentProtocolStage(1)) {
|
||||
// log.trace('ProtocolEngine::nextProtocolStage failed');
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Switches to the previous protocol stage in the display set sequence
|
||||
// */
|
||||
// previousProtocolStage() {
|
||||
// log.trace('ProtocolEngine::previousProtocolStage');
|
||||
|
||||
// if (!this.setCurrentProtocolStage(-1)) {
|
||||
// log.trace('ProtocolEngine::previousProtocolStage failed');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,3 +1,8 @@
|
||||
import HangingProtocolService from './HangingProtocolService';
|
||||
|
||||
export default HangingProtocolService;
|
||||
export default {
|
||||
name: 'HangingProtocolService',
|
||||
create: ({ configuration = {}, commandsManager }) => {
|
||||
return new HangingProtocolService(commandsManager);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
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: 'startsWith',
|
||||
name: 'Starts with',
|
||||
validator: 'startsWith',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must start with this value.',
|
||||
},
|
||||
{
|
||||
id: 'endsWith',
|
||||
name: 'Ends with',
|
||||
validator: 'endsWith',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must end with 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 };
|
||||
@@ -0,0 +1,71 @@
|
||||
const attributeCache = Object.create(null);
|
||||
const REGEXP = /^\([x0-9a-f]+\)/;
|
||||
|
||||
const humanize = text => {
|
||||
let humanized = text.replace(/([A-Z])/g, ' $1'); // insert a space before all caps
|
||||
|
||||
humanized = humanized.replace(/^./, str => {
|
||||
// uppercase the first character
|
||||
return str.toUpperCase();
|
||||
});
|
||||
|
||||
return humanized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the text of an attribute for a given attribute
|
||||
* @param {String} attributeId The attribute ID
|
||||
* @param {Array} attributes Array of attributes objects with id and text properties
|
||||
* @return {String} If found return the attribute text or an empty string otherwise
|
||||
*/
|
||||
const getAttributeText = (attributeId, attributes) => {
|
||||
// If the attribute is already in the cache, return it
|
||||
if (attributeId in attributeCache) {
|
||||
return attributeCache[attributeId];
|
||||
}
|
||||
|
||||
// Find the attribute with given attributeId
|
||||
const attribute = attributes.find(attribute => attribute.id === attributeId);
|
||||
|
||||
let attributeText;
|
||||
|
||||
// If attribute was found get its text and save it on the cache
|
||||
if (attribute) {
|
||||
attributeText = attribute.text.replace(REGEXP, '');
|
||||
attributeCache[attributeId] = attributeText;
|
||||
}
|
||||
|
||||
return attributeText || '';
|
||||
};
|
||||
|
||||
function displayConstraint(attributeId, constraint, attributes) {
|
||||
if (!constraint || !attributeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validatorType = Object.keys(constraint)[0];
|
||||
if (!validatorType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validator = Object.keys(constraint[validatorType])[0];
|
||||
if (!validator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = constraint[validatorType][validator];
|
||||
if (value === void 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let comparator = validator;
|
||||
if (validator === 'value') {
|
||||
comparator = validatorType;
|
||||
}
|
||||
|
||||
const attributeText = getAttributeText(attributeId, attributes);
|
||||
const constraintText =
|
||||
attributeText + ' ' + humanize(comparator).toLowerCase() + ' ' + value;
|
||||
|
||||
return constraintText;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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) => {
|
||||
// TODO: Double check whether or not this deep equality check is necessary
|
||||
//if (_.isEqual(value, input)) {
|
||||
if (value === input) {
|
||||
indexToRemove = index;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (indexToRemove === void 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
array.splice(indexToRemove, 1);
|
||||
return true;
|
||||
};
|
||||
|
||||
export { removeFromArray };
|
||||
@@ -0,0 +1,8 @@
|
||||
// Sorts an array by score
|
||||
const sortByScore = arr => {
|
||||
arr.sort((a, b) => {
|
||||
return b.score - a.score;
|
||||
});
|
||||
};
|
||||
|
||||
export { sortByScore };
|
||||
@@ -0,0 +1,39 @@
|
||||
import validate from 'validate.js';
|
||||
|
||||
validate.validators.equals = function (value, options, key, attributes) {
|
||||
if (options && value !== options.value) {
|
||||
return key + 'must equal ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.doesNotEqual = function (value, options, key) {
|
||||
if (options && value === options.value) {
|
||||
return key + 'cannot equal ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.contains = function (value, options, key) {
|
||||
if (options && value.indexOf && value.indexOf(options.value) === -1) {
|
||||
return key + 'must contain ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.doesNotContain = function (value, options, key) {
|
||||
if (options && value.indexOf && value.indexOf(options.value) !== -1) {
|
||||
return key + 'cannot contain ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.startsWith = function (value, options, key) {
|
||||
if (options && value.startsWith && !value.startsWith(options.value)) {
|
||||
return key + 'must start with ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.endsWith = function (value, options, key) {
|
||||
if (options && value.endsWith && !value.endsWith(options.value)) {
|
||||
return key + 'must end with ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
export default validate;
|
||||
@@ -54,6 +54,7 @@ const EVENTS = {
|
||||
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
||||
INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
|
||||
MEASUREMENT_ADDED: 'event::measurement_added',
|
||||
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
|
||||
MEASUREMENT_REMOVED: 'event::measurement_removed',
|
||||
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
|
||||
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
|
||||
@@ -314,11 +315,17 @@ class MeasurementService {
|
||||
* Converted to/from annotation in the same way. E.g. import serialized data
|
||||
* Of the same form as the measurement source.
|
||||
* @param {MeasurementSource} source The measurement source instance.
|
||||
* @param {string} definition The source definition you want to add the measuremnet to.
|
||||
* @param {string} definition The source definition you want to add the measurement to.
|
||||
* @param {object} data The data you wish to add to the source.
|
||||
* @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source definition.
|
||||
*/
|
||||
addRawMeasurement(source, definition, data, toMeasurementSchema) {
|
||||
addRawMeasurement(
|
||||
source,
|
||||
definition,
|
||||
data,
|
||||
toMeasurementSchema,
|
||||
dataSource = {}
|
||||
) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
@@ -342,7 +349,6 @@ class MeasurementService {
|
||||
try {
|
||||
/* Convert measurement */
|
||||
measurement = toMeasurementSchema(data);
|
||||
|
||||
/* Assign measurement source instance */
|
||||
measurement.source = source;
|
||||
} catch (error) {
|
||||
@@ -385,9 +391,11 @@ class MeasurementService {
|
||||
} else {
|
||||
log.info(`Measurement added.`, newMeasurement);
|
||||
this.measurements[internalId] = newMeasurement;
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
|
||||
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||
source,
|
||||
measurement: newMeasurement,
|
||||
data,
|
||||
dataSource,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -501,6 +509,7 @@ class MeasurementService {
|
||||
|
||||
clearMeasurements() {
|
||||
this.measurements = {};
|
||||
this._jumpToMeasurementCache = {};
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export default class ToolBarService {
|
||||
this.EVENTS = EVENTS;
|
||||
this.listeners = {};
|
||||
this.buttons = {};
|
||||
this.unsubscriptions = []; // if tools need to unsubscribe from events
|
||||
this.buttonSections = {
|
||||
/**
|
||||
* primary: ['Zoom', 'Wwwc'],
|
||||
@@ -39,6 +40,16 @@ export default class ToolBarService {
|
||||
this.extensionManager = extensionManager;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.unsubscriptions.forEach(unsub => unsub());
|
||||
this.state = {
|
||||
primaryToolId: 'Wwwc',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
};
|
||||
this.unsubscriptions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} interaction
|
||||
@@ -63,6 +74,7 @@ export default class ToolBarService {
|
||||
this.state.toggles[itemId] === undefined
|
||||
? true
|
||||
: !this.state.toggles[itemId];
|
||||
interaction.commandOptions.toggledState = this.state.toggles[itemId];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -73,13 +85,21 @@ export default class ToolBarService {
|
||||
//
|
||||
// NOTE: Should probably just do this for tools as well?
|
||||
// But would be nice if we could enforce at least the command name?
|
||||
let unsubscribe;
|
||||
if (interaction.commandName) {
|
||||
commandsManager.runCommand(
|
||||
unsubscribe = commandsManager.runCommand(
|
||||
interaction.commandName,
|
||||
interaction.commandOptions
|
||||
);
|
||||
}
|
||||
|
||||
// Storing the unsubscribe for later reseting
|
||||
if (unsubscribe && typeof unsubscribe === 'function') {
|
||||
if (this.unsubscriptions.indexOf(unsubscribe) === -1) {
|
||||
this.unsubscriptions.push(unsubscribe);
|
||||
}
|
||||
}
|
||||
|
||||
// Track last touched id for each group
|
||||
if (groupId) {
|
||||
this.state.groups[groupId] = itemId;
|
||||
|
||||
@@ -10,6 +10,7 @@ import ToolBarService from './ToolBarService';
|
||||
import ViewportGridService from './ViewportGridService';
|
||||
import CineService from './CineService';
|
||||
import HangingProtocolService from './HangingProtocolService';
|
||||
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
|
||||
|
||||
export {
|
||||
MeasurementService,
|
||||
@@ -23,5 +24,6 @@ export {
|
||||
ToolBarService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
CineService
|
||||
CineService,
|
||||
pubSubServiceInterface,
|
||||
};
|
||||
@@ -22,6 +22,7 @@ function createAndAddStack(
|
||||
StudyInstanceUID,
|
||||
frameRate,
|
||||
isClip,
|
||||
initialImageIdIndex,
|
||||
} = displaySet;
|
||||
if (!images) {
|
||||
return;
|
||||
@@ -35,6 +36,7 @@ function createAndAddStack(
|
||||
imageIds,
|
||||
frameRate,
|
||||
isClip,
|
||||
initialImageIdIndex,
|
||||
};
|
||||
|
||||
stackMap[displaySetInstanceUID] = stack;
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
* Sorting function
|
||||
* Sorts an array by seriesDate and seriesNumber if equal
|
||||
*/
|
||||
const sortBySeriesDate = array => array.sort((a, b) => {
|
||||
if (a.seriesNumber !== b.seriesNumber) {
|
||||
return a.seriesNumber - b.seriesNumber;
|
||||
}
|
||||
const sortBySeriesDate = array =>
|
||||
array.sort((a, b) => {
|
||||
if (a.seriesNumber !== b.seriesNumber) {
|
||||
return a.seriesNumber - b.seriesNumber;
|
||||
}
|
||||
|
||||
const seriesDateA = Date.parse(a.seriesDate);
|
||||
const seriesDateB = Date.parse(b.seriesDate);
|
||||
const seriesDateA = Date.parse(a.seriesDate);
|
||||
const seriesDateB = Date.parse(b.seriesDate);
|
||||
|
||||
return seriesDateA - seriesDateB;
|
||||
});
|
||||
return seriesDateA - seriesDateB;
|
||||
});
|
||||
|
||||
export default sortBySeriesDate;
|
||||
Reference in new issue
Block a user