feat(4D): Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode (#3664)
Co-authored-by: Alireza <ar.sedghi@gmail.com> Co-authored-by: Neil <neil.a.macphee@gmail.com>
This commit is contained in:
200 files changed
+9589
-2178
No files matched your search
@@ -37,7 +37,7 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.68.1",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.70.0",
|
||||
"@ohif/ui": "3.8.0-beta.73",
|
||||
"cornerstone-math": "0.1.9",
|
||||
"dicom-parser": "^1.8.21"
|
||||
|
||||
@@ -241,6 +241,12 @@ class MetadataProvider {
|
||||
sopInstanceUID: instance.SOPInstanceUID,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PET_IMAGE_MODULE:
|
||||
metadata = {
|
||||
frameReferenceTime: instance.FrameReferenceTime,
|
||||
actualFrameDuration: instance.ActualFrameDuration,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PET_ISOTOPE_MODULE:
|
||||
const { RadiopharmaceuticalInformationSequence } = instance;
|
||||
|
||||
@@ -393,6 +399,41 @@ class MetadataProvider {
|
||||
decayCorrection: instance.DecayCorrection,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.CALIBRATION_MODULE:
|
||||
// map the DICOM tags to the cornerstone tags since cornerstone tags
|
||||
// are camelCase and instance tags are all caps
|
||||
metadata = {
|
||||
sequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions?.map(region => {
|
||||
return {
|
||||
regionSpatialFormat: region.RegionSpatialFormat,
|
||||
regionDataType: region.RegionDataType,
|
||||
regionFlags: region.RegionFlags,
|
||||
regionLocationMinX0: region.RegionLocationMinX0,
|
||||
regionLocationMinY0: region.RegionLocationMinY0,
|
||||
regionLocationMaxX1: region.RegionLocationMaxX1,
|
||||
regionLocationMaxY1: region.RegionLocationMaxY1,
|
||||
referencePixelX0: region.ReferencePixelX0,
|
||||
referencePixelY0: region.ReferencePixelY0,
|
||||
referencePixelPhysicalValueX: region.ReferencePixelPhysicalValueX,
|
||||
referencePixelPhysicalValueY: region.ReferencePixelPhysicalValueY,
|
||||
physicalUnitsXDirection: region.PhysicalUnitsXDirection,
|
||||
physicalUnitsYDirection: region.PhysicalUnitsYDirection,
|
||||
physicalDeltaX: region.PhysicalDeltaX,
|
||||
physicalDeltaY: region.PhysicalDeltaY,
|
||||
};
|
||||
}),
|
||||
};
|
||||
break;
|
||||
|
||||
/**
|
||||
* Below are the tags and not the modules since they are not really
|
||||
* consistent with the modules above
|
||||
*/
|
||||
case 'temporalPositionIdentifier':
|
||||
metadata = {
|
||||
temporalPositionIdentifier: instance.TemporalPositionIdentifier,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
@@ -539,6 +580,7 @@ const WADO_IMAGE_LOADER_TAGS = {
|
||||
VOI_LUT_MODULE: 'voiLutModule',
|
||||
MODALITY_LUT_MODULE: 'modalityLutModule',
|
||||
SOP_COMMON_MODULE: 'sopCommonModule',
|
||||
PET_IMAGE_MODULE: 'petImageModule',
|
||||
PET_ISOTOPE_MODULE: 'petIsotopeModule',
|
||||
PER_SERIES_MODULE: 'petSeriesModule',
|
||||
OVERLAY_PLANE_MODULE: 'overlayPlaneModule',
|
||||
@@ -549,6 +591,7 @@ const WADO_IMAGE_LOADER_TAGS = {
|
||||
GENERAL_IMAGE_MODULE: 'generalImageModule',
|
||||
GENERAL_STUDY_MODULE: 'generalStudyModule',
|
||||
CINE_MODULE: 'cineModule',
|
||||
CALIBRATION_MODULE: 'calibrationModule',
|
||||
};
|
||||
|
||||
const INSTANCE = 'instance';
|
||||
@@ -48,6 +48,7 @@ export interface Extension {
|
||||
getCustomizationModule?: (p: ExtensionParams) => unknown;
|
||||
getSopClassHandlerModule?: (p: ExtensionParams) => unknown;
|
||||
getToolbarModule?: (p: ExtensionParams) => unknown;
|
||||
getPanelModule?: (p: ExtensionParams) => unknown;
|
||||
onModeEnter?: () => void;
|
||||
onModeExit?: () => void;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as OHIF from './index';
|
||||
|
||||
describe('Top level exports', () => {
|
||||
test('have not changed', () => {
|
||||
test.only('have not changed', () => {
|
||||
const expectedExports = [
|
||||
'MODULE_TYPES',
|
||||
//
|
||||
@@ -46,6 +46,7 @@ describe('Top level exports', () => {
|
||||
'pubSubServiceInterface',
|
||||
'PubSubService',
|
||||
'PanelService',
|
||||
'WorkflowStepsService',
|
||||
'useToolbar',
|
||||
].sort();
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
CustomizationService,
|
||||
StateSyncService,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
} from './services';
|
||||
|
||||
import { DisplaySetMessage, DisplaySetMessageList } from './services/DisplaySetService';
|
||||
@@ -84,6 +85,7 @@ const OHIF = {
|
||||
PubSubService,
|
||||
PanelService,
|
||||
useToolbar,
|
||||
WorkflowStepsService,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -128,6 +130,7 @@ export {
|
||||
Enums,
|
||||
Types,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
useToolbar,
|
||||
};
|
||||
|
||||
|
||||
@@ -34,29 +34,46 @@ class CineService extends PubSubService {
|
||||
// reducer state does not get updated right away and if we publish the
|
||||
// event and we use the cineService.getState() it will return the old state
|
||||
setTimeout(() => {
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, isCineEnabled);
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isCineEnabled });
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public playClip(element, playClipOptions) {
|
||||
return this.serviceImplementation._playClip(element, playClipOptions);
|
||||
const res = this.serviceImplementation._playClip(element, playClipOptions);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isPlaying: true });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public stopClip(element) {
|
||||
return this.serviceImplementation._stopClip(element);
|
||||
const res = this.serviceImplementation._stopClip(element);
|
||||
|
||||
this._broadcastEvent(this.EVENTS.CINE_STATE_CHANGED, { isPlaying: false });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public _onModeExit() {
|
||||
this.setIsCineEnabled(false);
|
||||
}
|
||||
|
||||
public getSyncedViewports(viewportId) {
|
||||
return this.serviceImplementation._getSyncedViewports(viewportId);
|
||||
}
|
||||
|
||||
public setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setCine: setCineImplementation,
|
||||
setIsCineEnabled: setIsCineEnabledImplementation,
|
||||
playClip: playClipImplementation,
|
||||
stopClip: stopClipImplementation,
|
||||
getSyncedViewports: getSyncedViewportsImplementation,
|
||||
}) {
|
||||
if (getSyncedViewportsImplementation) {
|
||||
this.serviceImplementation._getSyncedViewports = getSyncedViewportsImplementation;
|
||||
}
|
||||
|
||||
if (getStateImplementation) {
|
||||
this.serviceImplementation._getState = getStateImplementation;
|
||||
}
|
||||
|
||||
@@ -1314,18 +1314,19 @@ export default class HangingProtocolService extends PubSubService {
|
||||
);
|
||||
|
||||
// Use the display set provided instead
|
||||
if (reuseDisplaySetUID) {
|
||||
if (viewportOptions.allowUnmatchedView !== true) {
|
||||
this.validateDisplaySetSelectMatch(viewportDisplaySet, id, reuseDisplaySetUID);
|
||||
}
|
||||
const displaySetInfo: HangingProtocol.DisplaySetInfo = {
|
||||
displaySetInstanceUID: reuseDisplaySetUID,
|
||||
displaySetOptions,
|
||||
};
|
||||
// Todo: find out what is wrong here
|
||||
// if (reuseDisplaySetUID) {
|
||||
// if (viewportOptions.allowUnmatchedView !== true) {
|
||||
// this.validateDisplaySetSelectMatch(viewportDisplaySet, id, reuseDisplaySetUID);
|
||||
// }
|
||||
// const displaySetInfo: HangingProtocol.DisplaySetInfo = {
|
||||
// displaySetInstanceUID: reuseDisplaySetUID,
|
||||
// displaySetOptions,
|
||||
// };
|
||||
|
||||
displaySetsInfo.push(displaySetInfo);
|
||||
return;
|
||||
}
|
||||
// displaySetsInfo.push(displaySetInfo);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Use the display set index to allow getting the "next" match, eg
|
||||
// matching all display sets, and get the matchedDisplaySetsIndex'th item
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { ActivatePanelTriggers } from '../../types';
|
||||
import { Subscription } from '../../types/IPubSub';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
|
||||
export const EVENTS = {
|
||||
ACTIVATE_PANEL: 'event::panelService:activatePanel',
|
||||
};
|
||||
|
||||
export default class PanelService extends PubSubService {
|
||||
public static REGISTRATION = {
|
||||
name: 'panelService',
|
||||
create: (): PanelService => {
|
||||
return new PanelService();
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super(EVENTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the panel with the given id. If the forceActive flag is false
|
||||
* then it is up to the component containing the panel whether to activate
|
||||
* it immediately or not. For instance, the panel might not be activated when
|
||||
* the forceActive flag is false in the case where the user might have
|
||||
* activated/displayed and then closed the panel already.
|
||||
* Note that this method simply fires a broadcast event: ActivatePanelEvent.
|
||||
* @param panelId the panel's id
|
||||
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||
*/
|
||||
activatePanel(panelId: string, forceActive = false): void {
|
||||
this._broadcastEvent(EVENTS.ACTIVATE_PANEL, { panelId, forceActive });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mapping of events (activatePanelTriggers.sourceEvents) broadcast by
|
||||
* activatePanelTrigger.sourcePubSubService that
|
||||
* when fired/broadcasted must in turn activate the panel with the given id.
|
||||
* The subscriptions created are returned such that they can be managed and unsubscribed
|
||||
* as appropriate.
|
||||
* @param panelId the id of the panel to activate
|
||||
* @param activatePanelTriggers an array of triggers
|
||||
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||
* @returns an array of the subscriptions subscribed to
|
||||
*/
|
||||
addActivatePanelTriggers(
|
||||
panelId: string,
|
||||
activatePanelTriggers: ActivatePanelTriggers[],
|
||||
forceActive = false
|
||||
): Subscription[] {
|
||||
return activatePanelTriggers
|
||||
.map(trigger =>
|
||||
trigger.sourceEvents.map(eventName =>
|
||||
trigger.sourcePubSubService.subscribe(eventName, () =>
|
||||
this.activatePanel(panelId, forceActive)
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import React from 'react';
|
||||
import { ActivatePanelTriggers } from '../../types';
|
||||
import { Subscription } from '../../types/IPubSub';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
|
||||
export const EVENTS = {
|
||||
PANELS_CHANGED: 'event::panelService:panelsChanged',
|
||||
ACTIVATE_PANEL: 'event::panelService:activatePanel',
|
||||
};
|
||||
|
||||
type PanelData = {
|
||||
id: string;
|
||||
iconName: string;
|
||||
iconLabel: string;
|
||||
label: string;
|
||||
name: string;
|
||||
content: unknown;
|
||||
};
|
||||
|
||||
export enum PanelPosition {
|
||||
Left = 'left',
|
||||
Right = 'right',
|
||||
Bottom = 'bottom',
|
||||
}
|
||||
|
||||
export default class PanelService extends PubSubService {
|
||||
private _extensionManager: ExtensionManager;
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'panelService',
|
||||
create: ({ extensionManager }): PanelService => {
|
||||
return new PanelService(extensionManager);
|
||||
},
|
||||
};
|
||||
|
||||
private _panelsGroups: Map<PanelPosition, PanelData[]> = new Map();
|
||||
|
||||
constructor(extensionManager: ExtensionManager) {
|
||||
super(EVENTS);
|
||||
this._extensionManager = extensionManager;
|
||||
}
|
||||
|
||||
public get PanelPosition(): typeof PanelPosition {
|
||||
return PanelPosition;
|
||||
}
|
||||
|
||||
private _getPanelComponent(panelId: string) {
|
||||
const entry = this._extensionManager.getModuleEntry(panelId);
|
||||
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
`${panelId} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!entry?.component) {
|
||||
throw new Error(
|
||||
`No component found from extension ${panelId}. Check the reference string to the extension in your Mode configuration`
|
||||
);
|
||||
}
|
||||
|
||||
const content = entry.component;
|
||||
|
||||
return { entry, content };
|
||||
}
|
||||
|
||||
public getPanelData(panelId): PanelData {
|
||||
let content, entry;
|
||||
if (Array.isArray(panelId)) {
|
||||
const panelsData = panelId.map(id => this._getPanelComponent(id));
|
||||
|
||||
// use the first panel's entry for the combined panel
|
||||
entry = panelsData[0].entry;
|
||||
|
||||
// stack the content of the panels in one react component
|
||||
content = () => (
|
||||
<>
|
||||
{panelsData.map(({ content: PanelContent }) => (
|
||||
<PanelContent key={entry.id} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
({ content, entry } = this._getPanelComponent(panelId));
|
||||
}
|
||||
|
||||
return {
|
||||
id: entry.id,
|
||||
iconName: entry.iconName,
|
||||
iconLabel: entry.iconLabel,
|
||||
label: entry.label,
|
||||
name: entry.name,
|
||||
content,
|
||||
};
|
||||
}
|
||||
|
||||
public addPanel(position: PanelPosition, panelId: string, options): void {
|
||||
let panels = this._panelsGroups.get(position);
|
||||
|
||||
if (!panels) {
|
||||
panels = [];
|
||||
this._panelsGroups.set(position, panels);
|
||||
}
|
||||
|
||||
const panelComponent = this.getPanelData(panelId);
|
||||
|
||||
panels.push(panelComponent);
|
||||
this._broadcastEvent(EVENTS.PANELS_CHANGED, { position, options });
|
||||
}
|
||||
|
||||
public addPanels(position: PanelPosition, panelsIds: string[], options): void {
|
||||
if (!Array.isArray(panelsIds)) {
|
||||
throw new Error('Invalid "panelsIds" array');
|
||||
}
|
||||
|
||||
panelsIds.forEach(panelId => this.addPanel(position, panelId, options));
|
||||
}
|
||||
|
||||
public setPanels(
|
||||
panels: { [key in PanelPosition]: string[] },
|
||||
options: {
|
||||
rightPanelClosed?: boolean;
|
||||
leftPanelClosed?: boolean;
|
||||
}
|
||||
): void {
|
||||
this.reset();
|
||||
|
||||
Object.keys(panels).forEach((position: PanelPosition) => {
|
||||
this.addPanels(position, panels[position], options);
|
||||
});
|
||||
}
|
||||
|
||||
public getPanels(position: PanelPosition): PanelData[] {
|
||||
const panels = this._panelsGroups.get(position) ?? [];
|
||||
|
||||
// Return a new array to preserve the internal state
|
||||
return [...panels];
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
const affectedPositions = Array.from(this._panelsGroups.keys());
|
||||
|
||||
this._panelsGroups.clear();
|
||||
|
||||
affectedPositions.forEach(position =>
|
||||
this._broadcastEvent(EVENTS.PANELS_CHANGED, { position })
|
||||
);
|
||||
}
|
||||
|
||||
public onModeExit(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
/**5
|
||||
* Activates the panel with the given id. If the forceActive flag is false
|
||||
* then it is up to the component containing the panel whether to activate
|
||||
* it immediately or not. For instance, the panel might not be activated when
|
||||
* the forceActive flag is false in the case where the user might have
|
||||
* activated/displayed and then closed the panel already.
|
||||
* Note that this method simply fires a broadcast event: ActivatePanelEvent.
|
||||
* @param panelId the panel's id
|
||||
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||
*/
|
||||
public activatePanel(panelId: string, forceActive = false): void {
|
||||
this._broadcastEvent(EVENTS.ACTIVATE_PANEL, { panelId, forceActive });
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a mapping of events (activatePanelTriggers.sourceEvents) broadcast by
|
||||
* activatePanelTrigger.sourcePubSubService that
|
||||
* when fired/broadcasted must in turn activate the panel with the given id.
|
||||
* The subscriptions created are returned such that they can be managed and unsubscribed
|
||||
* as appropriate.
|
||||
* @param panelId the id of the panel to activate
|
||||
* @param activatePanelTriggers an array of triggers
|
||||
* @param forceActive optional flag indicating if the panel should be forced to be activated or not
|
||||
* @returns an array of the subscriptions subscribed to
|
||||
*/
|
||||
public addActivatePanelTriggers(
|
||||
panelId: string,
|
||||
activatePanelTriggers: ActivatePanelTriggers[],
|
||||
forceActive = false
|
||||
): Subscription[] {
|
||||
return activatePanelTriggers
|
||||
.map(trigger =>
|
||||
trigger.sourceEvents.map(eventName =>
|
||||
trigger.sourcePubSubService.subscribe(eventName, () =>
|
||||
this.activatePanel(panelId, forceActive)
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export default class ServicesManager {
|
||||
|
||||
constructor(commandsManager: CommandsManager) {
|
||||
this._commandsManager = commandsManager;
|
||||
this._extensionManager = null;
|
||||
this.services = {};
|
||||
this.registeredServiceNames = [];
|
||||
}
|
||||
@@ -46,6 +47,7 @@ export default class ServicesManager {
|
||||
if (service.create) {
|
||||
this.services[service.name] = service.create({
|
||||
configuration,
|
||||
extensionManager: this._extensionManager,
|
||||
commandsManager: this._commandsManager,
|
||||
servicesManager: this,
|
||||
extensionManager: this._extensionManager,
|
||||
|
||||
@@ -68,7 +68,7 @@ export default class ToolbarService extends PubSubService {
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.unsubscriptions.forEach(unsub => unsub());
|
||||
// this.unsubscriptions.forEach(unsub => unsub());
|
||||
this.state = {
|
||||
buttons: {},
|
||||
buttonSections: {},
|
||||
@@ -120,6 +120,10 @@ export default class ToolbarService extends PubSubService {
|
||||
public addButtons(buttons: Button[]): void {
|
||||
buttons.forEach(button => {
|
||||
if (!this.state.buttons[button.id]) {
|
||||
if (!button.props) {
|
||||
button.props = {};
|
||||
}
|
||||
|
||||
this.state.buttons[button.id] = button;
|
||||
}
|
||||
});
|
||||
@@ -350,6 +354,7 @@ export default class ToolbarService extends PubSubService {
|
||||
* @param {Array} buttons - The buttons to be added to the section.
|
||||
*/
|
||||
createButtonSection(key, buttons) {
|
||||
// make sure all buttons have at least an empty props
|
||||
this.state.buttonSections[key] = buttons;
|
||||
this._broadcastEvent(this.EVENTS.TOOL_BAR_MODIFIED, { ...this.state });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
import { CommandsManager } from '../../classes';
|
||||
import { ExtensionManager } from '../../extensions';
|
||||
import { ServicesManager } from '../../services';
|
||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||
|
||||
export const EVENTS = {
|
||||
ACTIVE_STEP_CHANGED: 'event::workflowStepsService:activateStepChanged',
|
||||
STEPS_CHANGED: 'event::workflowStepsService:stepsChanged',
|
||||
};
|
||||
|
||||
/*
|
||||
A mode may define a workflow and each workflow may have one or more steps.
|
||||
Each step may define a different set of tools, hanging protocol and panels
|
||||
layout that will be applied to the viewer once it gets activated making the
|
||||
viewer work in a more dynamic way.
|
||||
|
||||
Example:
|
||||
All keys inside brackets are optionals.
|
||||
|
||||
workflow: {
|
||||
[initialStepId]: 'step1',
|
||||
steps: [
|
||||
{
|
||||
id: 'firstStep',
|
||||
name: 'First Step',
|
||||
[toolbar]: {
|
||||
buttons: firstStepToolbarButtons,
|
||||
sections: [
|
||||
{
|
||||
key: 'primary',
|
||||
buttons: [ 'MeasurementTools', 'Zoom', ... ],
|
||||
},
|
||||
],
|
||||
},
|
||||
[layout]: {
|
||||
[panels]: {
|
||||
left: ['firstLeftPanelId', 'secondLeftPanelId'],
|
||||
right: ['firstRightPanelId'],
|
||||
},
|
||||
},
|
||||
[hangingProtocol]: {
|
||||
protocolId: 'default',
|
||||
[stepId]: 'firstStep',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'secondStep',
|
||||
name: 'Second Step',
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
If workflow steps are defined but `initialStepId` is not set then the first
|
||||
step is set as active during mode initialization.
|
||||
*/
|
||||
|
||||
type CommandCallback = {
|
||||
commandName: string;
|
||||
options: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type WorkflowStep = {
|
||||
id: string;
|
||||
name: string;
|
||||
toolbarButtons?: {
|
||||
buttonSection: string;
|
||||
buttons: string[];
|
||||
}[];
|
||||
hangingProtocol?: {
|
||||
protocolId: string;
|
||||
stageId?: string;
|
||||
};
|
||||
layout?: {
|
||||
panels: {
|
||||
left?: string[];
|
||||
right?: string[];
|
||||
};
|
||||
};
|
||||
onEnter: () => void | CommandCallback[];
|
||||
};
|
||||
|
||||
class WorkflowStepsService extends PubSubService {
|
||||
private _extensionManager: ExtensionManager;
|
||||
private _servicesManager: ServicesManager;
|
||||
private _commandsManager: CommandsManager;
|
||||
private _workflowSteps: WorkflowStep[];
|
||||
private _activeWorkflowStep: WorkflowStep;
|
||||
|
||||
constructor(
|
||||
extensionManager: ExtensionManager,
|
||||
commandsManager: CommandsManager,
|
||||
servicesManager: ServicesManager
|
||||
) {
|
||||
super(EVENTS);
|
||||
this._workflowSteps = [];
|
||||
this._activeWorkflowStep = null;
|
||||
this._extensionManager = extensionManager;
|
||||
this._commandsManager = commandsManager;
|
||||
this._servicesManager = servicesManager;
|
||||
}
|
||||
|
||||
public get workflowSteps(): WorkflowStep[] {
|
||||
return [...this._workflowSteps];
|
||||
}
|
||||
|
||||
public get activeWorkflowStep(): WorkflowStep {
|
||||
return this._activeWorkflowStep;
|
||||
}
|
||||
|
||||
public addWorkflowSteps(workflowSteps: WorkflowStep[]): void {
|
||||
let workflowStepAdded = false;
|
||||
|
||||
workflowSteps.forEach(newWorkflowStep => {
|
||||
const workflowStepExists = this._workflowSteps.some(
|
||||
workflowStep => workflowStep.id === newWorkflowStep.id
|
||||
);
|
||||
|
||||
if (workflowStepExists) {
|
||||
throw new Error(`Duplicated workflow step id (${newWorkflowStep.id})`);
|
||||
}
|
||||
|
||||
this._workflowSteps.push(newWorkflowStep);
|
||||
workflowStepAdded = true;
|
||||
});
|
||||
|
||||
if (workflowStepAdded) {
|
||||
this._broadcastEvent(EVENTS.STEPS_CHANGED, {});
|
||||
}
|
||||
}
|
||||
|
||||
private _updateToolBar(workflowStep: WorkflowStep) {
|
||||
const { toolbarService } = this._servicesManager.services;
|
||||
const { toolbarButtons } = workflowStep;
|
||||
|
||||
const toUse = Array.isArray(toolbarButtons) ? toolbarButtons : [toolbarButtons];
|
||||
|
||||
toUse.forEach(({ buttonSection, buttons }) => {
|
||||
toolbarService.createButtonSection(buttonSection, buttons);
|
||||
});
|
||||
}
|
||||
|
||||
private _updatePanels(workflowStep: WorkflowStep) {
|
||||
const { panelService } = this._servicesManager.services;
|
||||
const panels = workflowStep?.layout?.panels;
|
||||
|
||||
if (!panels) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelService.setPanels(panels, workflowStep?.layout?.options);
|
||||
}
|
||||
|
||||
private _updateHangingProtocol(workflowStep: WorkflowStep) {
|
||||
const { hangingProtocol } = workflowStep;
|
||||
|
||||
if (!hangingProtocol) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._commandsManager.runCommand('setHangingProtocol', {
|
||||
protocolId: hangingProtocol.protocolId,
|
||||
stageId: hangingProtocol.stageId,
|
||||
stageIndex: hangingProtocol.stageIndex,
|
||||
});
|
||||
}
|
||||
|
||||
private _invokeCallbacks(callbacks) {
|
||||
if (!callbacks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commandsManager = this._commandsManager;
|
||||
|
||||
if (!Array.isArray) {
|
||||
callbacks = [callbacks];
|
||||
}
|
||||
|
||||
// Invoke all callbacks which may be a function or an object like
|
||||
// { commandName: string, options?: object }
|
||||
callbacks.forEach(callback => {
|
||||
let fn = callback;
|
||||
|
||||
if (callback?.commandName) {
|
||||
const { commandName, options } = callback;
|
||||
fn = () => commandsManager.runCommand(commandName, options);
|
||||
}
|
||||
|
||||
fn();
|
||||
});
|
||||
}
|
||||
|
||||
public setActiveWorkflowStep(workflowStepId: string): void {
|
||||
const previousWorkflowStep = this._activeWorkflowStep;
|
||||
|
||||
if (workflowStepId === previousWorkflowStep?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newWorkflowStep = this._workflowSteps.find(step => step.id === workflowStepId);
|
||||
|
||||
if (!newWorkflowStep) {
|
||||
throw new Error(`Invalid workflowStepId (${workflowStepId})`);
|
||||
}
|
||||
|
||||
// onEnter needs to be called before updating the Hanging Protocol because
|
||||
// some displaySets need to be created before moving to the next HP stage
|
||||
// (eg: convert segmentations into a chart displaySet). If needed we can
|
||||
// change it to onBeforeEnter and onAfterEnter in the future.
|
||||
this._invokeCallbacks(newWorkflowStep.onEnter);
|
||||
|
||||
this._activeWorkflowStep = newWorkflowStep;
|
||||
this._updateToolBar(newWorkflowStep);
|
||||
this._updatePanels(newWorkflowStep);
|
||||
this._updateHangingProtocol(newWorkflowStep);
|
||||
this._broadcastEvent(EVENTS.ACTIVE_STEP_CHANGED, {
|
||||
activeWorkflowStep: newWorkflowStep,
|
||||
});
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._activeWorkflowStep = null;
|
||||
this._workflowSteps = [];
|
||||
}
|
||||
|
||||
public onModeEnter(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public static REGISTRATION = {
|
||||
name: 'workflowStepsService',
|
||||
create: ({ extensionManager, commandsManager, servicesManager }): WorkflowStepsService => {
|
||||
return new WorkflowStepsService(extensionManager, commandsManager, servicesManager);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { WorkflowStepsService as default, WorkflowStepsService };
|
||||
@@ -0,0 +1,3 @@
|
||||
import WorkflowStepsService from './WorkflowStepsService';
|
||||
|
||||
export default WorkflowStepsService;
|
||||
@@ -16,6 +16,7 @@ import UserAuthenticationService from './UserAuthenticationService';
|
||||
import CustomizationService from './CustomizationService';
|
||||
import StateSyncService from './StateSyncService';
|
||||
import PanelService from './PanelService';
|
||||
import WorkflowStepsService from './WorkflowStepsService';
|
||||
|
||||
import type Services from '../types/Services';
|
||||
|
||||
@@ -40,4 +41,5 @@ export {
|
||||
PubSubService,
|
||||
UserAuthenticationService,
|
||||
PanelService,
|
||||
WorkflowStepsService,
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
StateSyncService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
WorkflowStepsService,
|
||||
} from '../services';
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,7 @@ export default interface Services {
|
||||
uiModalService?: UIModalService;
|
||||
uiNotificationService?: UINotificationService;
|
||||
stateSyncService?: StateSyncService;
|
||||
workflowStepsService: WorkflowStepsService;
|
||||
cineService?: unknown;
|
||||
userAuthenticationService?: unknown;
|
||||
cornerstoneViewportService?: unknown;
|
||||
|
||||
Reference in new issue
Block a user