feat(synchronizers): Add registration for synchronizers (#2861)

doc(hp):Update the sync group/hanging protocol documentation
This commit is contained in:
Bill Wallace 2022-09-15 23:23:17 -04:00 committed by GitHub
parent f9449e97df
commit dc04296cfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 129 additions and 112 deletions

View File

@ -259,6 +259,11 @@ export default async function init({
// }
// };
const newStackCallback = evt => {
const { element } = evt.detail;
utilities.stackPrefetch.enable(element);
};
function elementEnabledHandler(evt) {
const { element } = evt.detail;
@ -267,10 +272,10 @@ export default async function init({
contextMenuHandleClick
);
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
const { element } = evt.detail;
utilities.stackPrefetch.enable(element);
});
eventTarget.addEventListener(
EVENTS.STACK_VIEWPORT_NEW_STACK,
newStackCallback
);
}
function elementDisabledHandler(evt) {
@ -284,10 +289,11 @@ export default async function init({
contextMenuHandleClick
);
eventTarget.removeEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, evt => {
const { element } = evt.detail;
utilities.stackPrefetch.disable(element);
});
// TODO - consider removing the callback when all elements are gone
// eventTarget.removeEventListener(
// EVENTS.STACK_VIEWPORT_NEW_STACK,
// newStackCallback
// );
}
eventTarget.addEventListener(

View File

@ -1,4 +1,8 @@
import { synchronizers, SynchronizerManager } from '@cornerstonejs/tools';
import {
synchronizers,
SynchronizerManager,
Synchronizer,
} from '@cornerstonejs/tools';
import { pubSubServiceInterface } from '@ohif/core';
@ -6,20 +10,40 @@ const EVENTS = {
TOOL_GROUP_CREATED: 'event::cornerstone::syncgroupservice:toolgroupcreated',
};
/**
* @params options - are an optional set of options associated with the first
* sync group declared.
*/
export type SyncCreator = (
type: string,
options?: Record<string, unknown>
) => Synchronizer;
export type SyncGroup = {
type: string;
id: string;
source: boolean;
target: boolean;
id?: string;
// Source and target default to true if not specified
source?: boolean;
target?: boolean;
options?: Record<string, unknown>;
};
const POSITION = 'cameraposition';
const VOI = 'voi';
const ZOOMPAN = 'zoompan';
const asSyncGroup = (syncGroup: string | SyncGroup): SyncGroup =>
typeof syncGroup === 'string' ? { type: syncGroup } : syncGroup;
export default class SyncGroupService {
serviceManager: any;
listeners: { [key: string]: (...args: any[]) => void } = {};
EVENTS: { [key: string]: string };
synchronizerCreators: Record<string, SyncCreator> = {
[POSITION]: synchronizers.createCameraPositionSynchronizer,
[VOI]: synchronizers.createVOISynchronizer,
[ZOOMPAN]: synchronizers.createZoomPanSynchronizer,
};
constructor(serviceManager) {
this.serviceManager = serviceManager;
@ -29,54 +53,71 @@ export default class SyncGroupService {
Object.assign(this, pubSubServiceInterface);
}
private _createSynchronizer(type: string, id: string) {
type = type.toLowerCase();
if (type === POSITION) {
return synchronizers.createCameraPositionSynchronizer(id);
} else if (type === VOI) {
return synchronizers.createVOISynchronizer(id);
private _createSynchronizer(
type: string,
id: string,
options
): Synchronizer | undefined {
const syncCreator = this.synchronizerCreators[type.toLowerCase()];
if (syncCreator) {
return syncCreator(id, options);
} else {
console.warn('Unknown synchronizer type', type, id);
}
}
/**
* Creates a synchronizer type.
* @param type is the type of the synchronizer to create
* @param creator
*/
public setSynchronizer(type: string, creator: SyncCreator): void {
this.synchronizerCreators[type] = creator;
}
protected _getOrCreateSynchronizer(
type: string,
id: string,
options: Record<string, unknown>
): Synchronizer | undefined {
let synchronizer = SynchronizerManager.getSynchronizer(id);
if (!synchronizer) {
synchronizer = this._createSynchronizer(type, id, options);
}
return synchronizer;
}
public addViewportToSyncGroup(
viewportId: string,
renderingEngineId: string,
syncGroups?: SyncGroup[]
syncGroups?: (SyncGroup | string)[]
): void {
if (!syncGroups || !syncGroups.length) {
return;
}
syncGroups.forEach(syncGroup => {
const { type, id, target, source } = syncGroup;
const syncGroupObj = asSyncGroup(syncGroup);
const { type, target = true, source = true, options = {} } = syncGroupObj;
const { id = type } = syncGroupObj;
let synchronizer = SynchronizerManager.getSynchronizer(id);
if (!synchronizer) {
synchronizer = this._createSynchronizer(type, id);
}
const synchronizer = this._getOrCreateSynchronizer(type, id, options);
synchronizer.setOptions(viewportId, options);
const viewportInfo = { viewportId, renderingEngineId };
if (target && source) {
synchronizer.add({
viewportId,
renderingEngineId,
});
synchronizer.add(viewportInfo);
return;
} else if (source) {
synchronizer.addSource({
viewportId,
renderingEngineId,
});
synchronizer.addSource(viewportInfo);
} else if (target) {
synchronizer.addTarget({
viewportId,
renderingEngineId,
});
synchronizer.addTarget(viewportInfo);
}
});
}
public destroy() {
public destroy(): void {
SynchronizerManager.destroy();
}

View File

@ -9,19 +9,23 @@ sidebar_label: Hanging Protocol Service
`HangingProtocolService` is a migration of the `OHIF-v1` hanging protocol
engine. This service handles the arrangement of the images in the viewport. In
short, the registered protocols will get matched with the Series that are
available for the series. Each protocol gets a point, and they are ranked. The
short, the registered protocols will get matched with the DisplaySets that are
available for the study. Each protocol gets a score, and they are ranked. The
winning protocol gets applied and its settings run for the viewports.
You can read more about hanging protocols
[here](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf).
In short with `OHIF-v3` hanging protocols you can:
- Define what layout of the viewport should the viewer starts with (2x2 layout)
- Define what layout of the viewport should the viewer starts with (eg 2x2 layout)
- Define which series gets displayed in which position of the layout
- Apply certain initial viewport settings; e.g., inverting the contrast
- Enable certain tools based on what series are displayed: link prostate T2 and
ADC MRI.
- Apply synchronization settings between different viewports or between setting and viewports
- Register custom synchronization settings for viewports
- Register custom attribute extractors
- Select "next display set" from the matching display sets, both on navigation and initial view
## Skeleton of A Hanging Protocol
@ -127,6 +131,7 @@ const defaultProtocol = {
Let's discuss each property in depth.
- `id`: unique identifier for the protocol
- `name`: Name displayed to the user to select this protocol
- `protocolMatchingRules`: A list of criteria for the protocol along with the
provided points for ranking.
@ -235,17 +240,6 @@ There are two events that get publish in `HangingProtocolService`:
- `addCustomAttribute`: adding a custom attribute for matching. (see below)
- `addCustomViewportSetting`: adding a custom setting to a viewport (initial
`voi`). Below, we explain in detail how to add custom viewport settings via
protocol definitions. `addCustomViewportSetting` is another way to set these
settings which is exposed by API
- `hps.applyCustomViewportSettings(viewportOptions, viewport,...args)` will run
the callback registered with addCustomViewportSetting for all custom settings
whose name matches the id of the custom viewport, with the arguments (id, value, viewport, ...args)
Default initialization of the modes handles running the `HangingProtocolService`
## Custom Attribute
@ -259,30 +253,25 @@ and you want to match based on it. Good news is that, in `OHIF-v3` you can
define you custom attribute and use it for matching.
There are various ways that you can let `HangingProtocolService` know of you
custom attribute. We will show how to add it inside the mode configuration.
custom attribute. We will show how to add it inside the an extension. This extension
also shows how to register a sync group service which can be referenced
in the sync group settings.
```js
const deafultProtocol = {
id: 'defaultProtocol',
const myCustomProtocol = {
id: 'myCustomProtocol',
/** ... **/
protocolMatchingRules: [
{
id: 'vSjk7NCYjtdS3XZAw',
weight: 3,
attribute: 'timepoint',
attribute: 'timepointId',
constraint: {
equals: {
value: 'first',
},
equals: 'first',
},
required: false,
},
],
stages: [
/** ... **/
],
numberOfPriorsReferenced: -1,
};
...
// Custom function for custom attribute
const getTimePointUID = metaData => {
@ -290,45 +279,13 @@ const getTimePointUID = metaData => {
return myBackEndAPI(metaData);
};
function modeFactory() {
return {
id: 'myMode',
/** .. **/
routes: [
{
path: 'myModeRoute',
init: async ({}) => {
const {
DicomMetadataStore,
HangingProtocolService,
} = servicesManager.services;
const onSeriesAdded = ({
StudyInstanceUID,
madeInClient = false,
}) => {
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
// Adding custom attribute to the hangingprotocol
HangingProtocolService.addCustomAttribute(
'timepoint',
'timepoint',
metaData => getFirstMeasurementSeriesInstanceUID(metaData)
);
HangingProtocolService.run(studyMetadata, DisplaySetService.getActiveDisplaySets());
};
DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.SERIES_ADDED,
onSeriesAdded
);
},
},
],
/** ... **/
};
}
preRegistration: ({
servicesManager,
}) => {
const { HangingProtocolService, SyncGroupService } = servicesManager.services;
HangingProtocolService.addCustomAttribute('timepointId', 'TimePoint ID', getTimePointUID);
SyncGroupService.setSynchronizer('initialzoompan', initialZoomPan);
}
```
## Viewport Settings
@ -377,3 +334,15 @@ viewportSettings: [
},
];
```
## Sync Groups
The sync groups are listeners to events that synchronize viewport settings to
some other settings. There are three default/provided sync groups: `zoomPan`,
`cameraPosition` and `voi`. These are defined in the `syncGroups` array.
Additionally, other synchronization types can be created and registered on the
`SyncGroupService.setSynchronizer`, by registering a new id, and a creator method.
The sync group service is specific to the `cornerstone-extension` because the
actual behaviour of the synchronizers is dependent on the specific viewport.
Different viewport types could redifine the same synchronizer names in
different ways appropriate to that viewport.

View File

@ -47,12 +47,13 @@ export function ViewportGridProvider({ children, service }) {
return { ...state, ...{ activeViewportIndex: action.payload } };
}
case 'SET_DISPLAYSET_FOR_VIEWPORT': {
const {
viewportIndex,
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
} = action.payload;
const payload = action.payload;
const { viewportIndex, displaySetInstanceUIDs } = payload;
const viewport = state.viewports[viewportIndex];
const viewportOptions =
payload.viewportOptions || viewport.viewportOptions || {};
const displaySetOptions = payload.displaySetOptions ||
viewport.displaySetOptions || [{}];
const viewports = state.viewports.slice();
// merge the displaySetOptions and viewportOptions and displaySetInstanceUIDs
@ -166,8 +167,8 @@ export function ViewportGridProvider({ children, service }) {
({
viewportIndex,
displaySetInstanceUIDs,
viewportOptions = {},
displaySetOptions = [{}],
viewportOptions,
displaySetOptions,
}) =>
dispatch({
type: 'SET_DISPLAYSET_FOR_VIEWPORT',