feat(synchronizers): Add registration for synchronizers (#2861)
doc(hp):Update the sync group/hanging protocol documentation
This commit is contained in:
parent
f9449e97df
commit
dc04296cfd
@ -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(
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user