feat(hp callback): Add viewport ready callback (#3772)
Co-authored-by: Ouwen Huang <ouwen.huang@duke.edu>
This commit is contained in:
parent
1a68c06c3b
commit
bf252bcec2
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -141,7 +141,9 @@ function checkHpsBestMatch(hps) {
|
||||
|
||||
describe('HangingProtocolService', () => {
|
||||
const mockedFunction = jest.fn();
|
||||
const commandsManager = {};
|
||||
const commandsManager = {
|
||||
run: mockedFunction,
|
||||
};
|
||||
const servicesManager = {
|
||||
services: {
|
||||
TestService: {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
Loading…
Reference in New Issue
Block a user