diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index 7c5120452..24fa1e45f 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -22,6 +22,20 @@ function areEqual(prevProps, nextProps) { return false; } + if ( + prevProps.viewportOptions.orientation !== + nextProps.viewportOptions.orientation + ) { + return false; + } + + if ( + prevProps.viewportOptions.viewportType !== + nextProps.viewportOptions.viewportType + ) { + return false; + } + const prevDisplaySets = prevProps.displaySets[0]; const nextDisplaySets = nextProps.displaySets[0]; diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx index 29d8b863d..7c5a071c7 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportOverlay.tsx @@ -258,8 +258,14 @@ function _getInstanceNumberFromVolume( const isAcquisitionPlane = vec3.length(cross) < EPSILON; if (isAcquisitionPlane) { + const imageId = imageIds[imageIndex]; + + if (!imageId) { + return {}; + } + const { instanceNumber } = - metaData.get('generalImageModule', imageIds[imageIndex]) || {}; + metaData.get('generalImageModule', imageId) || {}; return parseInt(instanceNumber); } } diff --git a/extensions/cornerstone/src/commandsModule.js b/extensions/cornerstone/src/commandsModule.ts similarity index 97% rename from extensions/cornerstone/src/commandsModule.js rename to extensions/cornerstone/src/commandsModule.ts index 46af1fac6..2e0fd8a47 100644 --- a/extensions/cornerstone/src/commandsModule.js +++ b/extensions/cornerstone/src/commandsModule.ts @@ -11,6 +11,8 @@ import { segmentation, utilities as csToolsUtils, } from '@cornerstonejs/tools'; + +import { Types } from '@ohif/core'; import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm'; import { getEnabledElement as OHIFgetEnabledElement } from './state'; @@ -26,6 +28,9 @@ const commandsModule = ({ servicesManager }) => { UIDialogService, CornerstoneViewportService, SegmentationService, + DisplaySetService, + HangingProtocolService, + UINotificationService, } = servicesManager.services; function _getActiveViewportEnabledElement() { @@ -415,6 +420,9 @@ const commandsModule = ({ servicesManager }) => { (activeViewportIndex - 1 + viewports.length) % viewports.length; ViewportGridService.setActiveViewportIndex(nextViewportIndex); }, + setHangingProtocol: ({ protocolId }) => { + HangingProtocolService.setProtocol(protocolId); + }, }; const definitions = { @@ -537,6 +545,11 @@ const commandsModule = ({ servicesManager }) => { storeContexts: [], options: {}, }, + setHangingProtocol: { + commandFn: actions.setHangingProtocol, + storeContexts: [], + options: {}, + }, }; return { diff --git a/extensions/cornerstone/src/getHangingProtocolModule.ts b/extensions/cornerstone/src/getHangingProtocolModule.ts new file mode 100644 index 000000000..e7a2b2cd5 --- /dev/null +++ b/extensions/cornerstone/src/getHangingProtocolModule.ts @@ -0,0 +1,146 @@ +import { Types } from '@ohif/core'; + +const MPRHangingProtocolGenerator: Types.HangingProtocol.ProtocolGenerator = ({ + servicesManager, + commandsManager, +}) => { + const { + ViewportGridService, + UINotificationService, + DisplaySetService, + } = servicesManager.services; + + const { activeViewportIndex, viewports } = ViewportGridService.getState(); + const viewportDisplaySetInstanceUIDs = + viewports[activeViewportIndex].displaySetInstanceUIDs; + + if ( + !viewportDisplaySetInstanceUIDs || + !viewportDisplaySetInstanceUIDs.length + ) { + return; + } + + const displaySetsToHang = viewportDisplaySetInstanceUIDs.map( + displaySetInstanceUID => { + const displaySet = DisplaySetService.getDisplaySetByUID( + displaySetInstanceUID + ); + + return displaySet; + } + ); + + if (displaySetsToHang.some(ds => !ds.isReconstructable)) { + UINotificationService.show({ + title: 'Multiplanar reconstruction (MPR) ', + message: + 'Cannot create MPR for this series since it is not reconstructable.', + type: 'warning', + displayTime: 3000, + }); + + return; + } + + const matchingDisplaySets = {}; + + displaySetsToHang.forEach(displaySet => { + const { + displaySetInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } = displaySet; + + matchingDisplaySets[displaySetInstanceUID] = { + displaySetInstanceUID, + SeriesInstanceUID, + StudyInstanceUID, + } as Types.HangingProtocol.DisplaySetMatchDetails; + }); + + const hpViewports: Types.HangingProtocol.Viewport[] = [ + 'axial', + 'sagittal', + 'coronal', + ].map(viewportOrientation => { + return { + viewportOptions: { + toolGroupId: 'mpr', + viewportType: 'volume', + orientation: viewportOrientation, + initialImageOptions: { + preset: 'middle', + }, + syncGroups: [ + { + type: 'voi', + id: 'mpr', + source: true, + target: true, + }, + ], + }, + displaySets: viewportDisplaySetInstanceUIDs.map(displaySetInstanceUID => { + return { + id: displaySetInstanceUID, + }; + }), + }; + }); + + const protocol = { + id: 'mpr', + stages: [ + { + id: 'mprStage', + name: 'mpr', + viewportStructure: { + layoutType: 'grid', + properties: { + rows: 1, + columns: 3, + layoutOptions: [ + { + x: 0, + y: 0, + width: 1 / 3, + height: 1, + }, + { + x: 1 / 3, + y: 0, + width: 1 / 3, + height: 1, + }, + { + x: 2 / 3, + y: 0, + width: 1 / 3, + height: 1, + }, + ], + }, + }, + displaySets: [], + viewports: hpViewports, + }, + ], + }; + + return { + protocol, + matchingDisplaySets, + }; +}; + +function getHangingProtocolModule() { + return [ + { + id: 'mpr', + protocol: MPRHangingProtocolGenerator, + }, + ]; +} + +export default getHangingProtocolModule; diff --git a/extensions/cornerstone/src/index.tsx b/extensions/cornerstone/src/index.tsx index 44e12fc95..470e6c46a 100644 --- a/extensions/cornerstone/src/index.tsx +++ b/extensions/cornerstone/src/index.tsx @@ -9,6 +9,7 @@ import { import { Enums as cs3DToolsEnums } from '@cornerstonejs/tools'; import init from './init.js'; import commandsModule from './commandsModule'; +import getHangingProtocolModule from './getHangingProtocolModule'; import ToolGroupService from './services/ToolGroupService'; import SyncGroupService from './services/SyncGroupService'; import { toolNames } from './initCornerstoneTools'; @@ -70,6 +71,7 @@ const cornerstoneExtension = { servicesManager.registerService(SyncGroupService(servicesManager)); await init({ servicesManager, commandsManager, configuration, appConfig }); }, + getHangingProtocolModule, getViewportModule({ servicesManager, commandsManager }) { const ExtendedOHIFCornerstoneViewport = props => { // const onNewImageHandler = jumpData => { diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js index 0d3d6b1a8..162cba0cf 100644 --- a/extensions/default/src/getHangingProtocolModule.js +++ b/extensions/default/src/getHangingProtocolModule.js @@ -14,7 +14,7 @@ const defaultProtocol = { id: 'hYbmMy3b7pz7GLiaT', name: 'default', viewportStructure: { - type: 'grid', + layoutType: 'grid', properties: { rows: 1, columns: 1, @@ -56,8 +56,8 @@ const defaultProtocol = { function getHangingProtocolModule() { return [ { - name: 'default', - protocols: [defaultProtocol], + id: defaultProtocol.id, + protocol: defaultProtocol, }, ]; } diff --git a/extensions/default/src/index.js b/extensions/default/src/index.js index 0cf1773a8..ecbce1582 100644 --- a/extensions/default/src/index.js +++ b/extensions/default/src/index.js @@ -2,9 +2,9 @@ import getDataSourcesModule from './getDataSourcesModule.js'; import getLayoutTemplateModule from './getLayoutTemplateModule.js'; import getPanelModule from './getPanelModule'; import getSopClassHandlerModule from './getSopClassHandlerModule.js'; -import getHangingProtocolModule from './getHangingProtocolModule.js'; import getToolbarModule from './getToolbarModule'; import commandsModule from './commandsModule'; +import getHangingProtocolModule from './getHangingProtocolModule'; import getStudiesForPatientByStudyInstanceUID from './Panels/getStudiesForPatientByStudyInstanceUID'; import { id } from './id.js'; import init from './init'; @@ -18,9 +18,9 @@ const defaultExtension = { init({ servicesManager, configuration }); }, getDataSourcesModule, - getHangingProtocolModule, getLayoutTemplateModule, getPanelModule, + getHangingProtocolModule, getSopClassHandlerModule, getToolbarModule, getCommandsModule({ servicesManager, commandsManager }) { diff --git a/extensions/tmtv/src/getHangingProtocolModule.js b/extensions/tmtv/src/getHangingProtocolModule.js index d92a5fd08..541055e5c 100644 --- a/extensions/tmtv/src/getHangingProtocolModule.js +++ b/extensions/tmtv/src/getHangingProtocolModule.js @@ -1,5 +1,5 @@ const ptCT = { - id: 'tmtv-pt-ct', + id: 'ptCT', locked: true, hasUpdatedPriorsInformation: false, name: 'Default', @@ -15,6 +15,15 @@ const ptCT = { ], imageLoadStrategy: 'interleaveTopToBottom', // "default" , "interleaveTopToBottom", "interleaveCenter" protocolMatchingRules: [ + { + id: 'wauZK2QNEfDPwcAQo', + weight: 1, + attribute: 'ModalitiesInStudy', + constraint: { + contains: ['CT', 'PT'], + }, + required: false, + }, { id: 'wauZK2QNEfDPwcAQo', weight: 1, @@ -632,8 +641,8 @@ const ptCT = { function getHangingProtocolModule() { return [ { - name: 'ptCT', - protocols: [ptCT], + id: ptCT.id, + protocol: ptCT, }, ]; } diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index f3072cf2e..35c7773c2 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -10,7 +10,6 @@ const NON_IMAGE_MODALITIES = ['SM', 'ECG', 'SR', 'SEG']; const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', - hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default', }; const tracked = { @@ -103,6 +102,7 @@ function modeFactory() { 'Pan', 'Capture', 'Layout', + 'MPR', 'MoreTools', ]); }, @@ -169,7 +169,8 @@ function modeFactory() { }, ], extensions: extensionDependencies, - hangingProtocols: [ohif.hangingProtocols], + // Default protocol gets self-registered by default in the init + hangingProtocol: 'default', // Order is important in sop class handlers when two handlers both use // the same sop class under different situations. In that case, the more // general handler needs to come last. For this case, the dicomvideo must diff --git a/modes/longitudinal/src/initToolGroups.js b/modes/longitudinal/src/initToolGroups.js index bd31ea75d..3b3a916e3 100644 --- a/modes/longitudinal/src/initToolGroups.js +++ b/modes/longitudinal/src/initToolGroups.js @@ -1,7 +1,8 @@ function initDefaultToolGroup( extensionManager, ToolGroupService, - commandsManager + commandsManager, + toolGroupId ) { const utilityModule = extensionManager.getModuleEntry( '@ohif/extension-cornerstone.utilityModule.tools' @@ -57,7 +58,6 @@ function initDefaultToolGroup( }, }; - const toolGroupId = 'default'; ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, toolsConfig); } @@ -140,8 +140,19 @@ function initSRToolGroup(extensionManager, ToolGroupService, commandsManager) { } function initToolGroups(extensionManager, ToolGroupService, commandsManager) { - initDefaultToolGroup(extensionManager, ToolGroupService, commandsManager); + initDefaultToolGroup( + extensionManager, + ToolGroupService, + commandsManager, + 'default' + ); initSRToolGroup(extensionManager, ToolGroupService, commandsManager); + initDefaultToolGroup( + extensionManager, + ToolGroupService, + commandsManager, + 'mpr' + ); } export default initToolGroups; diff --git a/modes/longitudinal/src/toolbarButtons.js b/modes/longitudinal/src/toolbarButtons.js index 3e2ecd012..41520edbd 100644 --- a/modes/longitudinal/src/toolbarButtons.js +++ b/modes/longitudinal/src/toolbarButtons.js @@ -290,6 +290,23 @@ const toolbarButtons = [ columns: 3, }, }, + // Todo: MPR not ready yet for SEG support, not activating it now + { + id: 'MPR', + type: 'ohif.action', + props: { + icon: 'old-play', + label: 'MPR', + type: 'action', + commands: [ + { + commandName: 'setHangingProtocol', + commandOptions: { protocolId: 'mpr' }, + context: 'CORNERSTONE', + }, + ], + }, + }, // More... { id: 'MoreTools', diff --git a/modes/tmtv/src/index.js b/modes/tmtv/src/index.js index f77422e94..4018dd538 100644 --- a/modes/tmtv/src/index.js +++ b/modes/tmtv/src/index.js @@ -8,7 +8,6 @@ import setFusionActiveVolume from './utils/setFusionActiveVolume.js'; const ohif = { layout: '@ohif/extension-default.layoutTemplateModule.viewerLayout', sopClassHandler: '@ohif/extension-default.sopClassHandlerModule.stack', - hangingProtocols: '@ohif/extension-default.hangingProtocolModule.default', measurements: '@ohif/extension-default.panelModule.measure', thumbnailList: '@ohif/extension-default.panelModule.seriesList', }; @@ -188,7 +187,7 @@ function modeFactory({ modeConfiguration }) { }, ], extensions: extensionDependencies, - hangingProtocols: [tmtv.hangingProtocols], + hangingProtocol: 'ptCT', sopClassHandlers: [ohif.sopClassHandler], hotkeys: [...hotkeys.defaults.hotkeyBindings], }; diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index 38583497d..7da7a9d92 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -43,12 +43,10 @@ export default class ExtensionManager { const { MeasurementService, ViewportGridService, - HangingProtocolService, } = _servicesManager.services; MeasurementService.clearMeasurements(); ViewportGridService.reset(); - HangingProtocolService.reset(); registeredExtensionIds.forEach(extensionId => { const onModeEnter = _extensionLifeCycleHooks.onModeEnter[extensionId]; @@ -74,12 +72,10 @@ export default class ExtensionManager { const { MeasurementService, ViewportGridService, - HangingProtocolService, } = _servicesManager.services; MeasurementService.clearMeasurements(); ViewportGridService.reset(); - HangingProtocolService.reset(); registeredExtensionIds.forEach(extensionId => { const onModeExit = _extensionLifeCycleHooks.onModeExit[extensionId]; @@ -196,6 +192,8 @@ export default class ExtensionManager { dataSources ); break; + case MODULE_TYPES.HANGING_PROTOCOL: + this._initHangingProtocolsModule(extensionModule, extensionId); case MODULE_TYPES.TOOLBAR: case MODULE_TYPES.VIEWPORT: case MODULE_TYPES.PANEL: @@ -203,7 +201,6 @@ export default class ExtensionManager { case MODULE_TYPES.CONTEXT: case MODULE_TYPES.LAYOUT_TEMPLATE: case MODULE_TYPES.UTILITY: - case MODULE_TYPES.HANGING_PROTOCOL: // Default for most extension points, // Just adds each entry ready for consumption by mode. extensionModule.forEach(element => { @@ -287,6 +284,13 @@ export default class ExtensionManager { } }; + _initHangingProtocolsModule = (extensionModule, extensionId) => { + const { HangingProtocolService } = this._servicesManager.services; + extensionModule.forEach(({ id, protocol }) => { + HangingProtocolService.addProtocol(id, protocol); + }); + }; + _initDataSourcesModule(extensionModule, extensionId, dataSources = []) { const { UserAuthenticationService } = this._servicesManager.services; diff --git a/platform/core/src/extensions/ExtensionManager.test.js b/platform/core/src/extensions/ExtensionManager.test.js index 8bc41e755..1ecf778ad 100644 --- a/platform/core/src/extensions/ExtensionManager.test.js +++ b/platform/core/src/extensions/ExtensionManager.test.js @@ -18,6 +18,9 @@ describe('ExtensionManager.js', () => { services: { // Required for DataSource Module initiation UserAuthenticationService: jest.fn(), + HangingProtocolService: { + addProtocol: jest.fn(), + }, }, }; appConfig = { diff --git a/platform/core/src/services/DisplaySetService/IDisplaySet.ts b/platform/core/src/services/DisplaySetService/IDisplaySet.ts index 3d7dc2252..9b81d6f89 100644 --- a/platform/core/src/services/DisplaySetService/IDisplaySet.ts +++ b/platform/core/src/services/DisplaySetService/IDisplaySet.ts @@ -2,6 +2,7 @@ interface IDisplaySet { displaySetInstanceUID: string; StudyInstanceUID: string; SeriesInstanceUID?: string; + SeriesNumber?: string; } export default IDisplaySet; diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js index d39610f24..4942aa3fd 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js @@ -127,7 +127,7 @@ describe('HangingProtocolService', () => { let initialScaling; beforeAll(() => { - hps.addProtocols([testProtocol]); + hps.addProtocol(testProtocol.id, testProtocol); }); it('has one protocol', () => { @@ -137,11 +137,7 @@ describe('HangingProtocolService', () => { describe('run', () => { it('matches best image match', () => { hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets }); - const { - hpAlreadyApplied, - viewportMatchDetails, - displaySetMatchDetails, - } = hps.getMatchDetails(); + const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails(); expect(hpAlreadyApplied).toMatchObject([false]); expect(viewportMatchDetails.length).toBe(1); expect(viewportMatchDetails[0]).toMatchObject({ diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index 0b1ebcf4b..a4914d003 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -3,6 +3,7 @@ import sortBy from '../../utils/sortBy'; import ProtocolEngine from './ProtocolEngine'; import StudyMetadata from '../../types/StudyMetadata'; import IDisplaySet from '../DisplaySetService/IDisplaySet'; +import { HangingProtocol } from '../../types'; const EVENTS = { STAGE_CHANGE: 'event::hanging_protocol_stage_change', @@ -12,51 +13,24 @@ const EVENTS = { 'event::hanging_protocol_custom_image_load_performed', }; -type ViewportOptions = { - orientation: string; - toolGroupId: string; - viewportId: string; - viewportType: string; - initialImageOptions: Record; - syncGroups: Record; -}; - -type ViewportMatchDetails = { - viewportOptions: ViewportOptions; - displaySetsInfo: { - SeriesInstanceUID: string; - displaySetInstanceUID: string; - displaySetOptions: Record; - }; -}; - -type DisplaySetMatchDetails = { - SeriesInstanceUID: string; - StudyInstanceUID: string; - displaySetInstanceUID: string; - matchDetails: any; - matchingScore: number; - sortingInfo: any; -}; - -type HangingProtocolMatchDetails = { - displaySetMatchDetails: Map; - viewportMatchDetails: ViewportMatchDetails[]; - hpAlreadyApplied: boolean[]; -}; +type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator; class HangingProtocolService { studies: StudyMetadata[]; - protocols: Record[]; - protocol: Record; + // stores all the protocols (object or function that returns an object) in a map + protocols: Map; + // the current protocol that is being applied to the viewports in object format + protocol: HangingProtocol.Protocol; stage: number; _commandsManager: Record; + _servicesManager: Record; protocolEngine: ProtocolEngine; hpAlreadyApplied: boolean[] = []; customViewportSettings = []; displaySets: IDisplaySet[] = []; activeStudy: Record; debugLogging: false; + EVENTS: { [key: string]: string }; customAttributeRetrievalCallbacks = { NumberOfStudyRelatedSeries: { @@ -89,17 +63,21 @@ class HangingProtocolService { * DisplaySetId is the id defined in the hangingProtocol object itself * and match is an object that contains information about */ - displaySetMatchDetails: Map = new Map(); + displaySetMatchDetails: Map< + string, + HangingProtocol.DisplaySetMatchDetails + > = new Map(); /** * An array that contains for each viewport (viewportIndex) specified in the * hanging protocol, an object of the form */ - viewportMatchDetails = [] as ViewportMatchDetails[]; + viewportMatchDetails = [] as HangingProtocol.ViewportMatchDetails[]; - constructor(commandsManager) { + constructor(commandsManager, servicesManager) { this._commandsManager = commandsManager; - this.protocols = []; + this._servicesManager = servicesManager; + this.protocols = new Map(); this.protocolEngine = undefined; this.protocol = undefined; this.stage = undefined; @@ -116,13 +94,17 @@ class HangingProtocolService { public reset() { this.studies = []; - this.protocols = []; + this.protocols = new Map(); this.hpAlreadyApplied = []; this.viewportMatchDetails = []; // this.ProtocolEngine.reset() } - public getMatchDetails(): HangingProtocolMatchDetails { + public getDefaultProtocol(): HangingProtocol.Protocol { + return this.getProtocolById('default'); + } + + public getMatchDetails(): HangingProtocol.HangingProtocolMatchDetails { return { viewportMatchDetails: this.viewportMatchDetails, displaySetMatchDetails: this.displaySetMatchDetails, @@ -130,16 +112,76 @@ class HangingProtocolService { }; } - public getProtocols() { - return this.protocols; + /** + * It loops over the protocols map object, and checks whether the protocol + * is a function, if so, it executes it and returns the result as a protocol object + * otherwise it returns the protocol object itself + * + * @returns all the hanging protocol registered in the HangingProtocolService + */ + public getProtocols(): HangingProtocol.Protocol[] { + // this.protocols is a map of protocols with the protocol id as the key + // and the protocol or a function that returns a protocol as the value + const protocols = []; + // @ts-ignore + for (const protocolId of this.protocols.keys()) { + const protocol = this.getProtocolById(protocolId); + if (protocol) { + protocols.push(protocol); + } + } + + return protocols; } - public addProtocols(protocols) { - protocols.forEach(protocol => { - if (this.protocols.indexOf(protocol) === -1) { - this.protocols.push(this._validateProtocol(protocol)); + /** + * Returns the protocol with the given id, it will get the protocol from the + * protocols map object and if it is a function, it will execute it and return + * the result as a protocol object + * + * @param protocolId - the id of the protocol + * @returns protocol - the protocol with the given id + */ + public getProtocolById(id: string): HangingProtocol.Protocol { + const protocol = this.protocols.get(id); + + if (protocol instanceof Function) { + try { + const { protocol: generatedProtocol } = this._getProtocolFromGenerator( + protocol + ); + + return generatedProtocol; + } catch (error) { + console.warn( + `Error while executing protocol generator for protocol ${id}: ${error}` + ); } - }); + } else { + return protocol; + } + } + + /** + * It adds a protocol to the protocols map object. If a protocol with the given + * id already exists, warn the user and overwrite it. + * + * @param {string} protocolId - The id of the protocol. + * @param {Protocol} protocol - Protocol - This is the protocol that you want to + * add to the protocol manager. + */ + public addProtocol(protocolId: string, protocol: Protocol): void { + if (this.protocols.has(protocolId)) { + console.warn( + `A protocol with id ${protocolId} already exists. It will be overwritten.` + ); + } + + if (!(protocol instanceof Function)) { + protocol = this._validateProtocol(protocol as HangingProtocol.Protocol); + } + + this.protocols.set(protocolId, protocol); } /** @@ -156,28 +198,28 @@ class HangingProtocolService { * @param protocol is a specific protocol to apply. * @returns */ - public run({ studies, displaySets, activeStudy }, protocol) { + public run({ studies, displaySets, activeStudy }, protocolId) { this.studies = [...studies]; this.displaySets = displaySets; this.activeStudy = activeStudy || studies[0]; this.protocolEngine = new ProtocolEngine( - this.protocols, + this.getProtocols(), this.customAttributeRetrievalCallbacks ); - // if there is no pre-defined protocol - if (!protocol || protocol.id === undefined) { - const matchedProtocol = this.protocolEngine.run({ - studies: this.studies, - activeStudy, - displaySets, - }); - this._setProtocol(matchedProtocol); + if (protocolId) { + const protocol = this.getProtocolById(protocolId); + this._setProtocol(protocol); return; } - this._setProtocol(protocol); + const matchedProtocol = this.protocolEngine.run({ + studies: this.studies, + activeStudy, + displaySets, + }); + this._setProtocol(matchedProtocol); } /** @@ -185,7 +227,7 @@ class HangingProtocolService { * and its callback has been added to the HangingProtocolService * @returns {boolean} true */ - public hasCustomImageLoadStrategy() { + public hasCustomImageLoadStrategy(): boolean { return ( this.activeImageLoadStrategyName !== null && this.registeredImageLoadStrategies[ @@ -194,7 +236,7 @@ class HangingProtocolService { ); } - public getCustomImageLoadPerformed() { + public getCustomImageLoadPerformed(): boolean { return this.customImageLoadPerformed; } @@ -203,13 +245,13 @@ class HangingProtocolService { * @param {string} name strategy name * @param {Function} callback image loader callback */ - public registerImageLoadStrategy(name, callback) { + public registerImageLoadStrategy(name, callback): void { if (callback instanceof Function && name) { this.registeredImageLoadStrategies[name] = callback; } } - public setHangingProtocolAppliedForViewport(i) { + public setHangingProtocolAppliedForViewport(i): void { this.hpAlreadyApplied[i] = true; } @@ -222,7 +264,12 @@ class HangingProtocolService { * @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image) * @param options to add to the "this" object for the custom attribute retriever */ - public addCustomAttribute(attributeId, attributeName, callback, options) { + public addCustomAttribute( + attributeId: string, + attributeName: string, + callback: (metadata: any) => any, + options: Record = {} + ): void { this.customAttributeRetrievalCallbacks[attributeId] = { ...options, id: attributeId, @@ -234,7 +281,7 @@ class HangingProtocolService { /** * Switches to the next protocol stage in the display set sequence */ - public nextProtocolStage() { + public nextProtocolStage(): void { console.log('ProtocolEngine::nextProtocolStage'); if (!this._setCurrentProtocolStage(1)) { @@ -245,7 +292,7 @@ class HangingProtocolService { /** * Switches to the previous protocol stage in the display set sequence */ - public previousProtocolStage() { + public previousProtocolStage(): void { console.log('ProtocolEngine::previousProtocolStage'); if (!this._setCurrentProtocolStage(-1)) { @@ -257,7 +304,7 @@ class HangingProtocolService { * Executes the callback function for the custom loading strategy for the images * if no strategy is set, the default strategy is used */ - runImageLoadStrategy(data) { + runImageLoadStrategy(data): void { const loader = this.registeredImageLoadStrategies[ this.activeImageLoadStrategyName ]; @@ -277,8 +324,14 @@ class HangingProtocolService { this._broadcastChange(this.EVENTS.CUSTOM_IMAGE_LOAD_PERFORMED, loadedData); } - _validateProtocol(protocol) { + _validateProtocol( + protocol: HangingProtocol.Protocol + ): HangingProtocol.Protocol { protocol.id = protocol.id || protocol.name; + const defaultViewportOptions = { + toolGroupId: 'default', + viewportType: 'stack', + }; // Automatically compute some number of attributes if they // aren't present. Makes defining new HPs easier. protocol.name = protocol.name || protocol.id; @@ -292,13 +345,14 @@ class HangingProtocolService { for (let i = 0; i < rows * columns; i++) { stage.viewports.push({ - viewportOptions: {}, + viewportOptions: defaultViewportOptions, displaySets: [], }); } } else { stage.viewports.forEach(viewport => { - viewport.viewportOptions = viewport.viewportOptions || {}; + viewport.viewportOptions = + viewport.viewportOptions || defaultViewportOptions; if (!viewport.displaySets) { viewport.displaySets = []; } else { @@ -313,9 +367,53 @@ class HangingProtocolService { return protocol; } - _setProtocol(protocol) { - // TODO: Add proper Protocol class to validate the protocols - // which are entered manually + /** + * It applied the protocol to the current studies and display sets based on the + * protocolId that is provided. + * @param protocolId - name of the protocol to be set + * @param protocol - protocol object (optional), if not provided, the protocol + * will be retrieved from the list of protocols by its name + * @param matchingDisplaySets - predefined display sets to be used for the protocol + */ + public setProtocol( + protocolId: string, + protocol?: HangingProtocol.Protocol, + matchingDisplaySets?: Record + ): void { + if (!protocol) { + const foundProtocol = this.protocols.get(protocolId); + + if (!foundProtocol) { + console.warn( + `HangingProtocolService::setProtocol - protocol ${protocolId} not found` + ); + return; + } + + if (foundProtocol instanceof Function) { + try { + ({ protocol, matchingDisplaySets } = this._getProtocolFromGenerator( + foundProtocol + )); + } catch (error) { + console.warn( + `HangingProtocolService::setProtocol - protocol ${protocolId} failed to execute`, + error + ); + return; + } + } else { + protocol = foundProtocol; + } + } + + this._setProtocol(protocol, matchingDisplaySets); + } + + private _setProtocol( + protocol: HangingProtocol.Protocol, + matchingDisplaySets?: Record + ): void { this.stage = 0; this.protocol = protocol; const { imageLoadStrategy } = protocol; @@ -328,7 +426,7 @@ class HangingProtocolService { this.activeImageLoadStrategyName = imageLoadStrategy; } } - this._updateViewports(); + this._updateViewports(matchingDisplaySets); this._broadcastChange(this.EVENTS.PROTOCOL_CHANGED, { viewportMatchDetails: this.viewportMatchDetails, @@ -362,19 +460,50 @@ class HangingProtocolService { return this.protocol.stages[this.stage]; } + private _getProtocolFromGenerator( + protocolGenerator: HangingProtocol.ProtocolGenerator + ): { + protocol: HangingProtocol.Protocol; + matchingDisplaySets: Record; + } { + const { protocol, matchingDisplaySets } = protocolGenerator({ + servicesManager: this._servicesManager, + commandsManager: this._commandsManager, + }); + + const validatedProtocol = this._validateProtocol(protocol); + + return { + protocol: validatedProtocol, + matchingDisplaySets, + }; + } + /** * Updates the viewports with the selected protocol stage. */ - _updateViewports() { + _updateViewports( + matchingDisplaySets?: Record + ): void { // Make sure we have an active protocol with a non-empty array of display sets if (!this._getNumProtocolStages()) { console.log('No protocol stages - nothing to display'); return; } + // each time we are updating the viewports, we need to reset the + // matching applied + this.hpAlreadyApplied = []; + // reset displaySetMatchDetails this.displaySetMatchDetails = new Map(); + if (matchingDisplaySets) { + this.displaySetMatchDetails = new Map( + Object.entries(matchingDisplaySets) + ); + } + // Retrieve the current stage const stageModel = this._getCurrentStageModel(); @@ -392,7 +521,7 @@ class HangingProtocolService { } this.customImageLoadPerformed = false; - const { type: layoutType } = stageModel.viewportStructure; + const { layoutType } = stageModel.viewportStructure; // Retrieve the properties associated with the current display set's viewport structure template // If no such layout properties exist, stop here. @@ -412,14 +541,19 @@ class HangingProtocolService { }); // Matching the displaySets + for (const displaySet of stageModel.displaySets) { + // skip matching if already matched + if (this.displaySetMatchDetails.has(displaySet.id)) { + continue; + } - stageModel.displaySets.forEach(displaySet => { const { bestMatch, matchingScores } = this._matchImages(displaySet); this.displaySetMatchDetails.set(displaySet.id, bestMatch); + if (bestMatch) { bestMatch.matchingScores = matchingScores; } - }); + } // Loop through each viewport stageModel.viewports.forEach((viewport, viewportIndex) => { @@ -430,6 +564,8 @@ class HangingProtocolService { // but it is a info to locate the displaySet from the displaySetService const displaySetsInfo = []; viewport.displaySets.forEach( + // Todo: why do we have displaySetIndex here? It is not used in the protocol + // definition ({ id, displaySetIndex = 0, options: displaySetOptions }) => { const viewportDisplaySetMain = this.displaySetMatchDetails.get(id); // Use the display set index to allow getting the "next" match, eg @@ -445,7 +581,7 @@ class HangingProtocolService { displaySetInstanceUID, } = viewportDisplaySet; - const displaySetInfo = { + const displaySetInfo: HangingProtocol.DisplaySetInfo = { SeriesInstanceUID, displaySetInstanceUID, displaySetOptions, @@ -524,6 +660,7 @@ class HangingProtocolService { const seriesMatchDetails = this.protocolEngine.findMatch( displaySet, seriesMatchingRules, + // Todo: why we have images here since the matching type does not have it { studies: this.studies, instance: displaySet.images?.[0] } ); diff --git a/platform/core/src/services/HangingProtocolService/index.js b/platform/core/src/services/HangingProtocolService/index.js index db50eaee3..92e288c4e 100644 --- a/platform/core/src/services/HangingProtocolService/index.js +++ b/platform/core/src/services/HangingProtocolService/index.js @@ -2,7 +2,7 @@ import HangingProtocolService from './HangingProtocolService'; export default { name: 'HangingProtocolService', - create: ({ configuration = {}, commandsManager }) => { - return new HangingProtocolService(commandsManager); + create: ({ configuration = {}, commandsManager, servicesManager }) => { + return new HangingProtocolService(commandsManager, servicesManager); }, }; diff --git a/platform/core/src/services/ServicesManager.js b/platform/core/src/services/ServicesManager.js index ad36592ef..ef3b04a46 100644 --- a/platform/core/src/services/ServicesManager.js +++ b/platform/core/src/services/ServicesManager.js @@ -37,6 +37,7 @@ export default class ServicesManager { this.services[service.name] = service.create({ configuration, commandsManager: this._commandsManager, + servicesManager: this, }); } else { log.warn(`Service create factory function not defined. Exiting early.`); diff --git a/platform/core/src/services/ServicesManager.test.js b/platform/core/src/services/ServicesManager.test.js index e1c35d120..dd6fd478c 100644 --- a/platform/core/src/services/ServicesManager.test.js +++ b/platform/core/src/services/ServicesManager.test.js @@ -4,10 +4,15 @@ import log from '../log.js'; jest.mock('./../log.js'); describe('ServicesManager.js', () => { - let servicesManager; + let servicesManager, commandsManager; beforeEach(() => { - servicesManager = new ServicesManager(); + commandsManager = { + createContext: jest.fn(), + getContext: jest.fn(), + registerCommand: jest.fn(), + }; + servicesManager = new ServicesManager(commandsManager); log.warn.mockClear(); jest.clearAllMocks(); }); @@ -90,9 +95,9 @@ describe('ServicesManager.js', () => { servicesManager.registerService(fakeService, configuration); - expect(fakeService.create.mock.calls[0][0]).toEqual({ - configuration, - }); + expect(fakeService.create.mock.calls[0][0].configuration.config).toBe( + configuration.config + ); }); }); }); diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts new file mode 100644 index 000000000..8d299ed09 --- /dev/null +++ b/platform/core/src/types/HangingProtocol.ts @@ -0,0 +1,140 @@ +type DisplaySetInfo = { + SeriesInstanceUID: string; + displaySetInstanceUID: string; + displaySetOptions: Record; +}; + +type ViewportMatchDetails = { + viewportOptions: ViewportOptions; + displaySetsInfo: DisplaySetInfo[]; +}; + +type DisplaySetMatchDetails = { + SeriesInstanceUID: string; + StudyInstanceUID: string; + displaySetInstanceUID: string; + matchDetails?: any; + matchingScores?: any[]; + sortingInfo?: any; +}; + +type HangingProtocolMatchDetails = { + displaySetMatchDetails: Map; + viewportMatchDetails: ViewportMatchDetails[]; + hpAlreadyApplied: boolean[]; +}; + +type MatchingRule = { + id: string; + weight: number; + attribute: string; + constraint: Record; + required: boolean; +}; + +type ViewportLayoutOptions = { + x: number; + y: number; + width: number; + height: number; +}; + +type ViewportStructure = { + layoutType: string; + properties: { + rows: number; + columns: number; + layoutOptions: ViewportLayoutOptions[]; + }; +}; + +type DisplaySet = { + id: string; + imageMatchingRules: MatchingRule[]; + seriesMatchingRules: MatchingRule[]; + studyMatchingRules: MatchingRule[]; +}; + +type SyncGroup = { + type: string; + id: string; + source?: boolean + target?: boolean +} + +type initialImageOptions = { + index?: number; + preset? : string; // todo: type more +} + +type ViewportOptions = { + toolGroupId: string; + viewportType: string; + id?: string; + orientation?: string; + viewportId?: string; + initialImageOptions?: initialImageOptions; + syncGroups?: SyncGroup[]; + customViewportProps? : Record; +}; + +type DisplaySetOptions = { + id: string; + options?: Record; +}; + +type Viewport = { + viewportOptions: ViewportOptions; + displaySets: DisplaySetOptions[]; +}; + +type ProtocolStage = { + id: string; + name: string; + viewportStructure: ViewportStructure; + displaySets: DisplaySet[]; + viewports: Viewport[]; + createdDate?: string; +}; + +type Protocol = { + // Mandatory + id: string; + stages: ProtocolStage[]; + // Optional + locked?: boolean; + hasUpdatedPriorsInformation?: boolean; + name?: string; + createdDate?: string; + modifiedDate?: string; + availableTo?: Record; + editableBy?: Record; + toolGroupIds?: string[]; + imageLoadStrategy?: string; // Todo: this should be types specifically + protocolMatchingRules?: MatchingRule[]; + numberOfPriorsReferenced?: number; +}; + +type ProtocolGenerator = ({servicesManager: any, commandsManager: any}) => { + protocol: Protocol; + matchingDisplaySets: any; +}; + +export type { + ProtocolGenerator, + ViewportOptions, + ViewportMatchDetails, + DisplaySetMatchDetails, + HangingProtocolMatchDetails, + Protocol, + ProtocolStage, + Viewport, + DisplaySet, + ViewportStructure, + ViewportLayoutOptions, + DisplaySetOptions, + MatchingRule, + SyncGroup, + initialImageOptions, + DisplaySetInfo +}; diff --git a/platform/core/src/types/index.ts b/platform/core/src/types/index.ts index 896e1ff05..80351c953 100644 --- a/platform/core/src/types/index.ts +++ b/platform/core/src/types/index.ts @@ -6,4 +6,12 @@ import { import Consumer from './Consumer'; -export { StudyMetadata, SeriesMetadata, InstanceMetadata, Consumer }; +import * as HangingProtocol from './HangingProtocol'; + +export type { + HangingProtocol, + StudyMetadata, + SeriesMetadata, + InstanceMetadata, + Consumer, +}; diff --git a/platform/docs/docs/platform/extensions/modules/hpModule.md b/platform/docs/docs/platform/extensions/modules/hpModule.md index ce6880838..d7778b7b0 100644 --- a/platform/docs/docs/platform/extensions/modules/hpModule.md +++ b/platform/docs/docs/platform/extensions/modules/hpModule.md @@ -28,7 +28,7 @@ const deafultProtocol = { id: 'hYbmMy3b7pz7GLiaT', name: 'default', viewportStructure: { - type: 'grid', + layoutType: 'grid', properties: { rows: 1, columns: 2, @@ -64,8 +64,8 @@ const deafultProtocol = { function getHangingProtocolModule() { return [ { - name: hangingProtocolName, - protocols: [defaultProtocol], + id: 'default', + protocol: deafultProtocol, }, ]; } diff --git a/platform/docs/docs/platform/modes/routes.md b/platform/docs/docs/platform/modes/routes.md index 2204bd77a..6e8647309 100644 --- a/platform/docs/docs/platform/modes/routes.md +++ b/platform/docs/docs/platform/modes/routes.md @@ -149,8 +149,8 @@ async function defaultRouteInit({ }); DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => { - const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID); - HangingProtocolService.run(studyMetadata); + const studyMetadata = // get study metadata and displaySets + HangingProtocolService.run({studies, displaySets, activeStudy}); }); return unsubscriptions; diff --git a/platform/docs/docs/platform/services/data/HangingProtocolService.md b/platform/docs/docs/platform/services/data/HangingProtocolService.md index 02dfe63a9..ded2252cd 100644 --- a/platform/docs/docs/platform/services/data/HangingProtocolService.md +++ b/platform/docs/docs/platform/services/data/HangingProtocolService.md @@ -8,20 +8,32 @@ sidebar_label: Hanging Protocol Service ## Overview `HangingProtocolService` is a migration of the `OHIF-v1` hanging protocol -engine. This service handles the arrangement of the images in the viewport. In -short, the registered protocols will get matched with the Series that are -available for the series. Each protocol gets a point, and they are ranked. The +engine along with various improvements and fixes. +This service handles the arrangement of the images in the viewport. In +short, the registered protocols will get matched with the DisplaySets that are +available. Each protocol gets a score, and they are ranked. The winning protocol gets applied and its settings run for the viewports. You can read more about hanging protocols [here](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf). -In short with `OHIF-v3` hanging protocols you can: +In `OHIF-v3` hanging protocols you can: - Define what layout of the viewport should the viewer starts with (2x2 layout) -- Define which series gets displayed in which position of the layout -- Apply certain initial viewport settings; e.g., inverting the contrast -- Enable certain tools based on what series are displayed: link prostate T2 and - ADC MRI. +- Specify the type of the viewport and its orientation +- Define which displaySets gets displayed in which viewport of the layout +- Apply certain initial viewport settings; e.g., inverting the contrast, jumping to a specific slice, etc. +- Add specific synchronization rules for the viewports + +## Protocols + +A protocol can be an object or a function that returns an object (protocol generator). +Each protocol can get added to the `HangingProtocolService` by using the `addProtocol` method given +an `id` and the protocol itself. As an example, the `default` protocol is an object, while +the 'mpr' protocol is a function that returns the protocol (the reason for this is that the +`mpr` protocol needs to be generated based on the active viewport). + +All protocols are stored in the `HangingProtocolService` using their `id` as the key, and +the protocol itself as the value. ## Events @@ -31,20 +43,21 @@ There are two events that get publish in `HangingProtocolService`: | ------------ | -------------------------------------------------------------------- | | NEW_LAYOUT | Fires when a new layout is requested by the `HangingProtocolService` | | STAGE_CHANGE | Fires when the the stage is changed in the hanging protocols | +| PROTOCOL_CHANGED | Fires when the the protocol is changed in the hanging protocols | + + ## API -- `getState`: returns an array: `[matchDetails, hpAlreadyApplied]`: +- `getMatchDetails`: returns an object which contains the details of the + matching for the viewports, displaysets and whether the protocol is + applied to the viewport or not - - `matchDetails`: matching details for the series - - `hpAlreadyApplied`: An array which tracks whether HPServices has been - applied on each viewport. - -- `addProtocols`: adds provided protocols to the list of registered protocols +- `addProtocol`: adds provided protocol to the list of registered protocols for matching -- `run(studyMetaData, protocol)`: runs the HPService with the provided - studyMetaData and optional protocol. If protocol is not given, HP Matching +- `run({studies, activeStudy, displaySets }, protocolId)`: runs the HPService with the provided + studyMetaData and optional protocolId. If protocol is not given, HP Matching engine will search all the registered protocols for the best matching one based on the constraints. @@ -52,6 +65,8 @@ There are two events that get publish in `HangingProtocolService`: Default initialization of the modes handles running the `HangingProtocolService` + + ## Custom Attribute In some situations, you might want to match based on a custom attribute and not the DICOM tags. For instance, if you have assigned a `timepointId` to each study, and you want to match based on it. @@ -129,50 +144,3 @@ function modeFactory() { }; } ``` - -## Viewport Settings - -You can define custom settings to be applied to each viewport. There are two -types of settings: - -- `viewport settings`: Currently we support two viewport settings - - - `voi`: applying an initial `voi` by setting the windowWidth and windowCenter - - `inverted`: inverting the viewport color (e.g., for PET images) - -- `props settings`: Running commands after the first render; e.g., enabling the - link tool - -Examples of each settings are : - -```js -viewportSettings: [ - { - options: { - itemId: 'SyncScroll', - interactionType: 'toggle', - commandName: 'toggleSynchronizer', - commandOptions: { toggledState: true }, - }, - commandName: 'setToolActive', - type: 'props', - }, -]; -``` - -and - -```js -viewportSettings: [ - { - options: { - voi: { - windowWidth: 400, - windowCenter: 40, - }, - }, - commandName: '', - type: 'viewport', - }, -]; -``` diff --git a/platform/docs/docs/platform/services/pubsub.md b/platform/docs/docs/platform/services/pubsub.md index 4fdc8748c..f7c9c4cce 100644 --- a/platform/docs/docs/platform/services/pubsub.md +++ b/platform/docs/docs/platform/services/pubsub.md @@ -58,8 +58,7 @@ async function defaultRouteInit({ const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe( DicomMetadataStore.EVENTS.SERIES_ADDED, ({ StudyInstanceUID }) => { - const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID); - HangingProtocolService.run(studyMetadata); + HangingProtocolService.run({studies, displaySets, activeStudy}); } ); unsubscriptions.push(seriesAddedUnsubscribe); @@ -89,11 +88,6 @@ export default function ModeRoute(/**..**/) { extensionManager.onModeEnter(); mode?.onModeEnter({ servicesManager, extensionManager }); - hangingProtocols.forEach(extentionProtocols => { - const { protocols } = extensionManager.getModuleEntry(extentionProtocols); - HangingProtocolService.addProtocols(protocols); - }); - const setupRouteInit = async () => { if (route.init) { return await route.init(/**...**/); diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.tsx b/platform/ui/src/contextProviders/ViewportGridProvider.tsx index 9f8ce3a00..f4a8db299 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.tsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.tsx @@ -49,11 +49,18 @@ export function ViewportGridProvider({ children, service }) { case 'SET_DISPLAYSET_FOR_VIEWPORT': { const payload = action.payload; const { viewportIndex, displaySetInstanceUIDs } = payload; - const viewport = state.viewports[viewportIndex]; - const viewportOptions = - payload.viewportOptions || viewport.viewportOptions || {}; - const displaySetOptions = payload.displaySetOptions || - viewport.displaySetOptions || [{}]; + + // Note: there should be no inheritance happening at this level, + // we can't assume the new displaySet can inherit the previous + // displaySet's or viewportOptions at all. For instance, dragging + // and dropping a SEG/RT displaySet without any viewportOptions + // or displaySetOptions should not inherit the previous displaySet's + // which might have been a PDF Viewport. The viewport itself + // will deal with inheritance if required. Here is just a simple + // provider. + const viewportOptions = payload.viewportOptions || {}; + const displaySetOptions = payload.displaySetOptions || [{}]; + const viewports = state.viewports.slice(); // merge the displaySetOptions and viewportOptions and displaySetInstanceUIDs diff --git a/platform/viewer/src/routes/Mode/Mode.tsx b/platform/viewer/src/routes/Mode/Mode.tsx index d49606849..628e33088 100644 --- a/platform/viewer/src/routes/Mode/Mode.tsx +++ b/platform/viewer/src/routes/Mode/Mode.tsx @@ -17,7 +17,10 @@ import Compose from './Compose'; * @param props.dataSource to read the data from * @returns array of subscriptions to cancel */ -function defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource }) { +function defaultRouteInit( + { servicesManager, studyInstanceUIDs, dataSource }, + hangingProtocol +) { const { DisplaySetService, HangingProtocolService, @@ -75,8 +78,12 @@ function defaultRouteInit({ servicesManager, studyInstanceUIDs, dataSource }) { // study being displayed, and is thus the "active" study. const activeStudy = studies[0]; - // run the hanging protocol matching service on the displaySets - HangingProtocolService.run({ studies, activeStudy, displaySets }); + // run the hanging protocol matching on the displaySets with the predefined + // hanging protocol in the mode configuration + HangingProtocolService.run( + { studies, activeStudy, displaySets }, + hangingProtocol + ); }); return unsubscriptions; @@ -107,12 +114,9 @@ export default function ModeRoute({ locationRef.current = location; } - const { - DisplaySetService, - HangingProtocolService, - } = servicesManager.services; + const { DisplaySetService } = servicesManager.services; - const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode; + const { extensions, sopClassHandlers, hotkeys, hangingProtocol } = mode; if (dataSourceName === undefined) { dataSourceName = extensionManager.defaultDataSourceName; @@ -239,34 +243,28 @@ export default function ModeRoute({ }); mode?.onModeEnter({ servicesManager, extensionManager, commandsManager }); - // Adding hanging protocols of extensions after onModeEnter since - // it will reset the protocols - hangingProtocols.forEach(extensionProtocols => { - const hangingProtocolModule = extensionManager.getModuleEntry( - extensionProtocols - ); - - if (hangingProtocolModule?.protocols) { - HangingProtocolService.addProtocols(hangingProtocolModule.protocols); - } - }); - const setupRouteInit = async () => { if (route.init) { - return await route.init({ - servicesManager, - extensionManager, - hotkeysManager, - studyInstanceUIDs, - dataSource, - }); + return await route.init( + { + servicesManager, + extensionManager, + hotkeysManager, + studyInstanceUIDs, + dataSource, + }, + hangingProtocol + ); } - return defaultRouteInit({ - servicesManager, - studyInstanceUIDs, - dataSource, - }); + return defaultRouteInit( + { + servicesManager, + studyInstanceUIDs, + dataSource, + }, + hangingProtocol + ); }; let unsubscriptions; @@ -291,7 +289,6 @@ export default function ModeRoute({ hotkeysManager, studyInstanceUIDs, refresh, - hangingProtocols, ]); const renderLayoutData = props => {