Skip to content

Commit

Permalink
feat(hp callbacks: add on viewport initialized hook
Browse files Browse the repository at this point in the history
  • Loading branch information
sedghi committed Nov 7, 2023
1 parent ddd80b0 commit 1d8af04
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 44 deletions.
21 changes: 20 additions & 1 deletion extensions/cornerstone/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
let numPanesWithData = 0;
cornerstoneViewportService.subscribe(EVENT, evt => {
numPanesWithData++;

if (numPanesWithData === numPanes) {
commandsManager.run(...command);

// Unsubscribe from the event
cornerstoneViewportService.unsubscribe(EVENT);
}
});
},
};

const definitions = {
Expand All @@ -606,7 +623,6 @@ function commandsModule({
storeContexts: [],
options: {},
},

deleteMeasurement: {
commandFn: actions.deleteMeasurement,
},
Expand Down Expand Up @@ -714,6 +730,9 @@ function commandsModule({
cleanUpCrosshairs: {
commandFn: actions.cleanUpCrosshairs,
},
attachProtocolViewportDataListener: {
commandFn: actions.attachProtocolViewportDataListener,
},
};

return {
Expand Down
4 changes: 2 additions & 2 deletions extensions/cornerstone/src/init.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ export default async function init({
viewportGridService.subscribe(
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
activeViewportEventListener
);
}
);
}

function CPUModal() {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});
}

Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -654,38 +661,44 @@ 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(
viewport: StackViewport | VolumeViewport,
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,
Expand Down
11 changes: 0 additions & 11 deletions extensions/default/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,8 @@ export default class HangingProtocolService extends PubSubService {

throw new Error(error);
}

this._commandsManager.run(this.protocol.callbacks?.onProtocolEnter);
}

protected matchActivation(
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions platform/core/src/types/HangingProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 stages initial layout.
onViewportDataInitialized?: Command[];
};

/**
Expand Down
61 changes: 61 additions & 0 deletions platform/docs/docs/platform/extensions/modules/hpModule.md
Original file line number Diff line number Diff line change
Expand Up @@ -559,3 +559,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
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
...
```

0 comments on commit 1d8af04

Please sign in to comment.