feat(hp callback): Add viewport ready callback (#3772)

Co-authored-by: Ouwen Huang <ouwen.huang@duke.edu>
This commit is contained in:
Alireza 2023-11-08 09:19:49 -05:00 committed by GitHub
parent 1a68c06c3b
commit bf252bcec2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 143 additions and 44 deletions

View File

@ -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",

View File

@ -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 {

View File

@ -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<void> {
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<void> {
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,

View File

@ -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);

View File

@ -141,7 +141,9 @@ function checkHpsBestMatch(hps) {
describe('HangingProtocolService', () => {
const mockedFunction = jest.fn();
const commandsManager = {};
const commandsManager = {
run: mockedFunction,
};
const servicesManager = {
services: {
TestService: {

View File

@ -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,

View File

@ -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;

View File

@ -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[];
};
/**

View File

@ -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
};

View File

@ -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,
},
...
```