diff --git a/extensions/cornerstone/package.json b/extensions/cornerstone/package.json index dedbac20d6d..cc1b3aa1af1 100644 --- a/extensions/cornerstone/package.json +++ b/extensions/cornerstone/package.json @@ -39,7 +39,7 @@ "@cornerstonejs/dicom-image-loader": "^1.27.3", "@ohif/core": "3.8.0-beta.10", "@ohif/ui": "3.8.0-beta.10", - "dcmjs": "^0.29.11", + "dcmjs": "^0.29.12", "dicom-parser": "^1.8.21", "hammerjs": "^2.0.8", "prop-types": "^15.6.2", diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 4c443c243f2..29d24491968 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -580,6 +580,23 @@ function commandsModule({ storePresentation: ({ viewportId }) => { cornerstoneViewportService.storePresentation({ viewportId }); }, + + attachProtocolViewportDataListener: ({ protocol, stageIndex }) => { + const EVENT = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED; + const command = protocol.callbacks.onViewportDataInitialized; + const numPanes = protocol.stages?.[stageIndex]?.viewports.length ?? 1; + let numPanesWithData = 0; + const { unsubscribe } = cornerstoneViewportService.subscribe(EVENT, evt => { + numPanesWithData++; + + if (numPanesWithData === numPanes) { + commandsManager.run(...command); + + // Unsubscribe from the event + unsubscribe(EVENT); + } + }); + }, }; const definitions = { @@ -606,7 +623,6 @@ function commandsModule({ storeContexts: [], options: {}, }, - deleteMeasurement: { commandFn: actions.deleteMeasurement, }, @@ -714,6 +730,9 @@ function commandsModule({ cleanUpCrosshairs: { commandFn: actions.cleanUpCrosshairs, }, + attachProtocolViewportDataListener: { + commandFn: actions.attachProtocolViewportDataListener, + }, }; return { diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 497df2d544d..71b33ffd4fb 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -283,14 +283,21 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi this.viewportsById.set(viewportId, viewportInfo); const viewport = renderingEngine.getViewport(viewportId); - this._setDisplaySets(viewport, viewportData, viewportInfo, presentations); + const displaySetPromise = this._setDisplaySets( + viewport, + viewportData, + viewportInfo, + presentations + ); // The broadcast event here ensures that listeners have a valid, up to date // viewport to access. Doing it too early can result in exceptions or // invalid data. - this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { - viewportData, - viewportId, + displaySetPromise.then(() => { + this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { + viewportData, + viewportId, + }); }); } @@ -312,12 +319,12 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi return this.viewportsById.get(viewportId); } - _setStackViewport( + private async _setStackViewport( viewport: Types.IStackViewport, viewportData: StackViewportData, viewportInfo: ViewportInfo, presentations: Presentations - ): void { + ): Promise { const displaySetOptions = viewportInfo.getDisplaySetOptions(); const { imageIds, initialImageIndex, displaySetInstanceUID } = viewportData.data; @@ -347,7 +354,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi } } - viewport.setStack(imageIds, initialImageIndexToUse).then(() => { + return viewport.setStack(imageIds, initialImageIndexToUse).then(() => { viewport.setProperties({ ...properties }); const camera = presentations.positionPresentation?.camera; if (camera) { @@ -654,21 +661,27 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const viewport = this.getCornerstoneViewport(viewportId); const viewportCamera = viewport.getCamera(); + let displaySetPromise; + if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) { - this._setVolumeViewport(viewport, viewportData, viewportInfo).then(() => { + displaySetPromise = this._setVolumeViewport(viewport, viewportData, viewportInfo).then(() => { if (keepCamera) { viewport.setCamera(viewportCamera); viewport.render(); } }); - - return; } if (viewport instanceof StackViewport) { - this._setStackViewport(viewport, viewportData, viewportInfo); - return; + displaySetPromise = this._setStackViewport(viewport, viewportData, viewportInfo); } + + displaySetPromise.then(() => { + this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { + viewportData, + viewportId, + }); + }); } _setDisplaySets( @@ -676,16 +689,16 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi viewportData: StackViewportData | VolumeViewportData, viewportInfo: ViewportInfo, presentations: Presentations = {} - ): void { + ): Promise { if (viewport instanceof StackViewport) { - this._setStackViewport( + return this._setStackViewport( viewport, viewportData as StackViewportData, viewportInfo, presentations ); } else if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) { - this._setVolumeViewport( + return this._setVolumeViewport( viewport, viewportData as VolumeViewportData, viewportInfo, diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 3f22e35895b..bcc0cab591b 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -241,7 +241,6 @@ const commandsModule = ({ ]; stateSyncService.store(stateSyncReduce); // This is a default action applied - const { protocol } = hangingProtocolService.getActiveProtocol(); actions.toggleHpTools(); // try to use the same tool in the new hanging protocol stage @@ -264,16 +263,6 @@ const commandsModule = ({ }); } } - - // Send the notification about updating the state - if (protocolId !== hpInfo.protocolId) { - // The old protocol callbacks are used for turning off things - // like crosshairs when moving to the new HP - commandsManager.run(oldProtocol.callbacks?.onProtocolExit); - // The new protocol callback is used for things like - // activating modes etc. - } - commandsManager.run(protocol.callbacks?.onProtocolEnter); return true; } catch (e) { console.error(e); diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js index aa9dfa169f0..cbc726ca450 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.test.js @@ -141,7 +141,9 @@ function checkHpsBestMatch(hps) { describe('HangingProtocolService', () => { const mockedFunction = jest.fn(); - const commandsManager = {}; + const commandsManager = { + run: mockedFunction, + }; const servicesManager = { services: { TestService: { diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index 01fdd8f6ef0..104c4be346d 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -849,6 +849,8 @@ export default class HangingProtocolService extends PubSubService { throw new Error(error); } + + this._commandsManager.run(this.protocol?.callbacks?.onProtocolEnter); } protected matchActivation( @@ -956,6 +958,12 @@ export default class HangingProtocolService extends PubSubService { if (!this.protocol || this.protocol.id !== protocol.id) { this.stageIndex = options?.stageIndex || 0; this._originalProtocol = this._copyProtocol(protocol); + + // before reassigning the protocol, we need to check if there is a callback + // on the old protocol that needs to be called + // Send the notification about updating the state + this._commandsManager.run(this.protocol?.callbacks?.onProtocolExit); + this.protocol = protocol; const { imageLoadStrategy } = protocol; @@ -1120,6 +1128,13 @@ export default class HangingProtocolService extends PubSubService { const { columns: numCols, rows: numRows, layoutOptions = [] } = layoutProps; + if (this.protocol?.callbacks?.onViewportDataInitialized) { + this._commandsManager.runCommand('attachProtocolViewportDataListener', { + protocol: this.protocol, + stageIndex: this.stageIndex, + }); + } + this._broadcastEvent(this.EVENTS.NEW_LAYOUT, { layoutType, numRows, @@ -1143,9 +1158,6 @@ export default class HangingProtocolService extends PubSubService { } { let matchedViewports = 0; stageModel.viewports.forEach(viewport => { - // Todo: we should probably assign a random viewportId if not defined - // below, but it feels odd since viewportGrid should handle this kind - // of thing const viewportId = viewport.viewportOptions.viewportId; const matchDetails = this._matchViewport( viewport, diff --git a/platform/core/src/services/_shared/pubSubServiceInterface.ts b/platform/core/src/services/_shared/pubSubServiceInterface.ts index 822f969af59..7b66ceff9f6 100644 --- a/platform/core/src/services/_shared/pubSubServiceInterface.ts +++ b/platform/core/src/services/_shared/pubSubServiceInterface.ts @@ -90,7 +90,7 @@ function _broadcastEvent(eventName, callbackProps) { /** Export a PubSubService class to be used instead of the individual items */ export class PubSubService { EVENTS: any; - subscribe: (eventName: string, callback: Function) => { unsubscribe: () => any; }; + subscribe: (eventName: string, callback: Function) => { unsubscribe: () => any }; _broadcastEvent: (eventName: string, callbackProps: any) => void; _unsubscribe: (eventName: string, listenerId: string) => void; _isValidEvent: (eventName: string) => boolean; diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts index 797028f3815..8059b5675c8 100644 --- a/platform/core/src/types/HangingProtocol.ts +++ b/platform/core/src/types/HangingProtocol.ts @@ -241,15 +241,17 @@ export type ProtocolStage = { export type ProtocolNotifications = { // This set of commands is executed after the protocol is exited and the new one applied onProtocolExit?: Command[]; - // This set of commands is executed after the protocol is entered and applied onProtocolEnter?: Command[]; - // This set of commands is executed before the layout change is started. // If it returns false, the layout change will be aborted. // The numRows and numCols is included in the command params, so it is possible // to apply a specific hanging protocol onLayoutChange?: Command[]; + // This set of commands is executed after the initial viewport grid data is set + // and all viewport data includes a designated display set. This command + // will run on every stage's initial layout. + onViewportDataInitialized?: Command[]; }; /** diff --git a/platform/docs/docs/platform/extensions/modules/hpModule.md b/platform/docs/docs/platform/extensions/modules/hpModule.md index 922d9e04ed1..40d627f2a74 100644 --- a/platform/docs/docs/platform/extensions/modules/hpModule.md +++ b/platform/docs/docs/platform/extensions/modules/hpModule.md @@ -403,6 +403,7 @@ viewportStructure: { ], }, }, + ``` @@ -559,3 +560,64 @@ Additional series level criteria, such as modality rules must be included at the }, ], ``` + + +## Callbacks + + +Hanging protocols in `OHIF-v3` provide the flexibility to define various callbacks that allow you to customize the behavior of your viewer when specific events occur during protocol execution. These callbacks are defined in the `ProtocolNotifications` type and can be added to your hanging protocol configuration. + +Each callback is an array of commands or actions that are executed when the event occurs. + +```js +[ + { + commandName: 'showDownloadViewportModal', + commandOptions: {} + } +] +``` + + +Here, we'll explain the available callbacks and their purposes: + +### `onProtocolExit` + +The `onProtocolExit` callback is executed after the protocol is exited and the new one is applied. This callback is useful for performing actions or executing commands when switching between hanging protocols. + +### `onProtocolEnter` + +The `onProtocolEnter` callback is executed after the protocol is entered and applied. You can use this callback to define actions or commands that should run when entering a specific hanging protocol. + +### `onLayoutChange` + +The `onLayoutChange` callback is executed before the layout change is started. You can use it to apply a specific hanging protocol based on the current layout or other criteria. + +### `onViewportDataInitialized` + +The `onViewportDataInitialized` callback is executed after the initial viewport grid data is set and all viewport data includes a designated display set. This callback runs during the initial layout setup for each stage. You can use it to perform actions or apply settings to the viewports at the start. + +Here is an example of how you can add these callbacks to your hanging protocol configuration: + +```javascript +const protocol = { + id: 'myProtocol', + name: 'My Protocol', + // rest of the protocol configuration + callbacks: { + onProtocolExit: [ + // Array of commands or actions to execute on protocol exit + ], + onProtocolEnter: [ + // Array of commands or actions to execute on protocol enter + ], + onLayoutChange: [ + // Array of commands or actions to execute on layout change + ], + onViewportDataInitialized: [ + // Array of commands or actions to execute on viewport data initialization + ], + }, + // protocolMatchingRules + // the rest +}; diff --git a/platform/docs/docs/platform/services/data/HangingProtocolService.md b/platform/docs/docs/platform/services/data/HangingProtocolService.md index f2e156b6336..58caa82c303 100644 --- a/platform/docs/docs/platform/services/data/HangingProtocolService.md +++ b/platform/docs/docs/platform/services/data/HangingProtocolService.md @@ -350,13 +350,13 @@ The below example modifies the included hanging protocol (extensions/tmtv/src/ge ```javascript ptDisplaySet: { - ... - seriesMatchingRules: [ - { - attribute: 'sameAs', - sameAttribute: 'FrameOfReferenceUID', - sameDisplaySetId: 'ctDisplaySet', - required: true, - }, - ... + ... + seriesMatchingRules: [ + { + attribute: 'sameAs', + sameAttribute: 'FrameOfReferenceUID', + sameDisplaySetId: 'ctDisplaySet', + required: true, + }, + ... ```