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",
|
"@cornerstonejs/dicom-image-loader": "^1.27.3",
|
||||||
"@ohif/core": "3.8.0-beta.10",
|
"@ohif/core": "3.8.0-beta.10",
|
||||||
"@ohif/ui": "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",
|
"dicom-parser": "^1.8.21",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
|
|||||||
@ -580,6 +580,23 @@ function commandsModule({
|
|||||||
storePresentation: ({ viewportId }) => {
|
storePresentation: ({ viewportId }) => {
|
||||||
cornerstoneViewportService.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 = {
|
const definitions = {
|
||||||
@ -606,7 +623,6 @@ function commandsModule({
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteMeasurement: {
|
deleteMeasurement: {
|
||||||
commandFn: actions.deleteMeasurement,
|
commandFn: actions.deleteMeasurement,
|
||||||
},
|
},
|
||||||
@ -714,6 +730,9 @@ function commandsModule({
|
|||||||
cleanUpCrosshairs: {
|
cleanUpCrosshairs: {
|
||||||
commandFn: actions.cleanUpCrosshairs,
|
commandFn: actions.cleanUpCrosshairs,
|
||||||
},
|
},
|
||||||
|
attachProtocolViewportDataListener: {
|
||||||
|
commandFn: actions.attachProtocolViewportDataListener,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -283,15 +283,22 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
this.viewportsById.set(viewportId, viewportInfo);
|
this.viewportsById.set(viewportId, viewportInfo);
|
||||||
|
|
||||||
const viewport = renderingEngine.getViewport(viewportId);
|
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
|
// 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
|
// viewport to access. Doing it too early can result in exceptions or
|
||||||
// invalid data.
|
// invalid data.
|
||||||
|
displaySetPromise.then(() => {
|
||||||
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
||||||
viewportData,
|
viewportData,
|
||||||
viewportId,
|
viewportId,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCornerstoneViewport(
|
public getCornerstoneViewport(
|
||||||
@ -312,12 +319,12 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
return this.viewportsById.get(viewportId);
|
return this.viewportsById.get(viewportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setStackViewport(
|
private async _setStackViewport(
|
||||||
viewport: Types.IStackViewport,
|
viewport: Types.IStackViewport,
|
||||||
viewportData: StackViewportData,
|
viewportData: StackViewportData,
|
||||||
viewportInfo: ViewportInfo,
|
viewportInfo: ViewportInfo,
|
||||||
presentations: Presentations
|
presentations: Presentations
|
||||||
): void {
|
): Promise<void> {
|
||||||
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
const displaySetOptions = viewportInfo.getDisplaySetOptions();
|
||||||
|
|
||||||
const { imageIds, initialImageIndex, displaySetInstanceUID } = viewportData.data;
|
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 });
|
viewport.setProperties({ ...properties });
|
||||||
const camera = presentations.positionPresentation?.camera;
|
const camera = presentations.positionPresentation?.camera;
|
||||||
if (camera) {
|
if (camera) {
|
||||||
@ -654,21 +661,27 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
const viewport = this.getCornerstoneViewport(viewportId);
|
const viewport = this.getCornerstoneViewport(viewportId);
|
||||||
const viewportCamera = viewport.getCamera();
|
const viewportCamera = viewport.getCamera();
|
||||||
|
|
||||||
|
let displaySetPromise;
|
||||||
|
|
||||||
if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) {
|
if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) {
|
||||||
this._setVolumeViewport(viewport, viewportData, viewportInfo).then(() => {
|
displaySetPromise = this._setVolumeViewport(viewport, viewportData, viewportInfo).then(() => {
|
||||||
if (keepCamera) {
|
if (keepCamera) {
|
||||||
viewport.setCamera(viewportCamera);
|
viewport.setCamera(viewportCamera);
|
||||||
viewport.render();
|
viewport.render();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewport instanceof StackViewport) {
|
if (viewport instanceof StackViewport) {
|
||||||
this._setStackViewport(viewport, viewportData, viewportInfo);
|
displaySetPromise = this._setStackViewport(viewport, viewportData, viewportInfo);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displaySetPromise.then(() => {
|
||||||
|
this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, {
|
||||||
|
viewportData,
|
||||||
|
viewportId,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_setDisplaySets(
|
_setDisplaySets(
|
||||||
@ -676,16 +689,16 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi
|
|||||||
viewportData: StackViewportData | VolumeViewportData,
|
viewportData: StackViewportData | VolumeViewportData,
|
||||||
viewportInfo: ViewportInfo,
|
viewportInfo: ViewportInfo,
|
||||||
presentations: Presentations = {}
|
presentations: Presentations = {}
|
||||||
): void {
|
): Promise<void> {
|
||||||
if (viewport instanceof StackViewport) {
|
if (viewport instanceof StackViewport) {
|
||||||
this._setStackViewport(
|
return this._setStackViewport(
|
||||||
viewport,
|
viewport,
|
||||||
viewportData as StackViewportData,
|
viewportData as StackViewportData,
|
||||||
viewportInfo,
|
viewportInfo,
|
||||||
presentations
|
presentations
|
||||||
);
|
);
|
||||||
} else if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) {
|
} else if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) {
|
||||||
this._setVolumeViewport(
|
return this._setVolumeViewport(
|
||||||
viewport,
|
viewport,
|
||||||
viewportData as VolumeViewportData,
|
viewportData as VolumeViewportData,
|
||||||
viewportInfo,
|
viewportInfo,
|
||||||
|
|||||||
@ -241,7 +241,6 @@ const commandsModule = ({
|
|||||||
];
|
];
|
||||||
stateSyncService.store(stateSyncReduce);
|
stateSyncService.store(stateSyncReduce);
|
||||||
// This is a default action applied
|
// This is a default action applied
|
||||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
|
||||||
actions.toggleHpTools();
|
actions.toggleHpTools();
|
||||||
|
|
||||||
// try to use the same tool in the new hanging protocol stage
|
// 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;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@ -141,7 +141,9 @@ function checkHpsBestMatch(hps) {
|
|||||||
|
|
||||||
describe('HangingProtocolService', () => {
|
describe('HangingProtocolService', () => {
|
||||||
const mockedFunction = jest.fn();
|
const mockedFunction = jest.fn();
|
||||||
const commandsManager = {};
|
const commandsManager = {
|
||||||
|
run: mockedFunction,
|
||||||
|
};
|
||||||
const servicesManager = {
|
const servicesManager = {
|
||||||
services: {
|
services: {
|
||||||
TestService: {
|
TestService: {
|
||||||
|
|||||||
@ -849,6 +849,8 @@ export default class HangingProtocolService extends PubSubService {
|
|||||||
|
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._commandsManager.run(this.protocol?.callbacks?.onProtocolEnter);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected matchActivation(
|
protected matchActivation(
|
||||||
@ -956,6 +958,12 @@ export default class HangingProtocolService extends PubSubService {
|
|||||||
if (!this.protocol || this.protocol.id !== protocol.id) {
|
if (!this.protocol || this.protocol.id !== protocol.id) {
|
||||||
this.stageIndex = options?.stageIndex || 0;
|
this.stageIndex = options?.stageIndex || 0;
|
||||||
this._originalProtocol = this._copyProtocol(protocol);
|
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;
|
this.protocol = protocol;
|
||||||
|
|
||||||
const { imageLoadStrategy } = protocol;
|
const { imageLoadStrategy } = protocol;
|
||||||
@ -1120,6 +1128,13 @@ export default class HangingProtocolService extends PubSubService {
|
|||||||
|
|
||||||
const { columns: numCols, rows: numRows, layoutOptions = [] } = layoutProps;
|
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, {
|
this._broadcastEvent(this.EVENTS.NEW_LAYOUT, {
|
||||||
layoutType,
|
layoutType,
|
||||||
numRows,
|
numRows,
|
||||||
@ -1143,9 +1158,6 @@ export default class HangingProtocolService extends PubSubService {
|
|||||||
} {
|
} {
|
||||||
let matchedViewports = 0;
|
let matchedViewports = 0;
|
||||||
stageModel.viewports.forEach(viewport => {
|
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 viewportId = viewport.viewportOptions.viewportId;
|
||||||
const matchDetails = this._matchViewport(
|
const matchDetails = this._matchViewport(
|
||||||
viewport,
|
viewport,
|
||||||
|
|||||||
@ -90,7 +90,7 @@ function _broadcastEvent(eventName, callbackProps) {
|
|||||||
/** Export a PubSubService class to be used instead of the individual items */
|
/** Export a PubSubService class to be used instead of the individual items */
|
||||||
export class PubSubService {
|
export class PubSubService {
|
||||||
EVENTS: any;
|
EVENTS: any;
|
||||||
subscribe: (eventName: string, callback: Function) => { unsubscribe: () => any; };
|
subscribe: (eventName: string, callback: Function) => { unsubscribe: () => any };
|
||||||
_broadcastEvent: (eventName: string, callbackProps: any) => void;
|
_broadcastEvent: (eventName: string, callbackProps: any) => void;
|
||||||
_unsubscribe: (eventName: string, listenerId: string) => void;
|
_unsubscribe: (eventName: string, listenerId: string) => void;
|
||||||
_isValidEvent: (eventName: string) => boolean;
|
_isValidEvent: (eventName: string) => boolean;
|
||||||
|
|||||||
@ -241,15 +241,17 @@ export type ProtocolStage = {
|
|||||||
export type ProtocolNotifications = {
|
export type ProtocolNotifications = {
|
||||||
// This set of commands is executed after the protocol is exited and the new one applied
|
// This set of commands is executed after the protocol is exited and the new one applied
|
||||||
onProtocolExit?: Command[];
|
onProtocolExit?: Command[];
|
||||||
|
|
||||||
// This set of commands is executed after the protocol is entered and applied
|
// This set of commands is executed after the protocol is entered and applied
|
||||||
onProtocolEnter?: Command[];
|
onProtocolEnter?: Command[];
|
||||||
|
|
||||||
// This set of commands is executed before the layout change is started.
|
// This set of commands is executed before the layout change is started.
|
||||||
// If it returns false, the layout change will be aborted.
|
// If it returns false, the layout change will be aborted.
|
||||||
// The numRows and numCols is included in the command params, so it is possible
|
// The numRows and numCols is included in the command params, so it is possible
|
||||||
// to apply a specific hanging protocol
|
// to apply a specific hanging protocol
|
||||||
onLayoutChange?: Command[];
|
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
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user