616 lines
22 KiB
TypeScript
616 lines
22 KiB
TypeScript
import { Types } from '@ohif/core';
|
|
|
|
import { ContextMenuController, defaultContextMenu } from './CustomizableContextMenu';
|
|
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
|
import reuseCachedLayouts from './utils/reuseCachedLayouts';
|
|
import findViewportsByPosition, {
|
|
findOrCreateViewport as layoutFindOrCreate,
|
|
} from './findViewportsByPosition';
|
|
|
|
import { ContextMenuProps } from './CustomizableContextMenu/types';
|
|
import { NavigateHistory } from './types/commandModuleTypes';
|
|
import { history } from '@ohif/app';
|
|
import { useViewportGridStore } from './stores/useViewportGridStore';
|
|
import { useDisplaySetSelectorStore } from './stores/useDisplaySetSelectorStore';
|
|
import { useHangingProtocolStageIndexStore } from './stores/useHangingProtocolStageIndexStore';
|
|
import { useToggleHangingProtocolStore } from './stores/useToggleHangingProtocolStore';
|
|
import { useViewportsByPositionStore } from './stores/useViewportsByPositionStore';
|
|
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
|
|
|
export type HangingProtocolParams = {
|
|
protocolId?: string;
|
|
stageIndex?: number;
|
|
activeStudyUID?: string;
|
|
stageId?: string;
|
|
reset?: false;
|
|
};
|
|
|
|
export type UpdateViewportDisplaySetParams = {
|
|
direction: number;
|
|
excludeNonImageModalities?: boolean;
|
|
};
|
|
|
|
const commandsModule = ({
|
|
servicesManager,
|
|
commandsManager,
|
|
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
|
const {
|
|
customizationService,
|
|
measurementService,
|
|
hangingProtocolService,
|
|
uiNotificationService,
|
|
viewportGridService,
|
|
displaySetService,
|
|
} = servicesManager.services;
|
|
|
|
// Define a context menu controller for use with any context menus
|
|
const contextMenuController = new ContextMenuController(servicesManager, commandsManager);
|
|
|
|
const actions = {
|
|
/**
|
|
* Show the context menu.
|
|
* @param options.menuId defines the menu name to lookup, from customizationService
|
|
* @param options.defaultMenu contains the default menu set to use
|
|
* @param options.element is the element to show the menu within
|
|
* @param options.event is the event that caused the context menu
|
|
* @param options.selectorProps is the set of selection properties to use
|
|
*/
|
|
showContextMenu: (options: ContextMenuProps) => {
|
|
const {
|
|
menuCustomizationId,
|
|
element,
|
|
event,
|
|
selectorProps,
|
|
defaultPointsPosition = [],
|
|
} = options;
|
|
|
|
const optionsToUse = { ...options };
|
|
|
|
if (menuCustomizationId) {
|
|
Object.assign(
|
|
optionsToUse,
|
|
customizationService.get(menuCustomizationId, defaultContextMenu)
|
|
);
|
|
}
|
|
|
|
// TODO - make the selectorProps richer by including the study metadata and display set.
|
|
const { protocol, stage } = hangingProtocolService.getActiveProtocol();
|
|
optionsToUse.selectorProps = {
|
|
event,
|
|
protocol,
|
|
stage,
|
|
...selectorProps,
|
|
};
|
|
|
|
contextMenuController.showContextMenu(optionsToUse, element, defaultPointsPosition);
|
|
},
|
|
|
|
/** Close a context menu currently displayed */
|
|
closeContextMenu: () => {
|
|
contextMenuController.closeContextMenu();
|
|
},
|
|
|
|
displayNotification: ({ text, title, type }) => {
|
|
uiNotificationService.show({
|
|
title: title,
|
|
message: text,
|
|
type: type,
|
|
});
|
|
},
|
|
clearMeasurements: () => {
|
|
measurementService.clearMeasurements();
|
|
},
|
|
|
|
/**
|
|
* Sets the specified protocol
|
|
* 1. Records any existing state using the viewport grid service
|
|
* 2. Finds the destination state - this can be one of:
|
|
* a. The specified protocol stage
|
|
* b. An alternate (toggled or restored) protocol stage
|
|
* c. A restored custom layout
|
|
* 3. Finds the parameters for the specified state
|
|
* a. Gets the displaySetSelectorMap
|
|
* b. Gets the map by position
|
|
* c. Gets any toggle mapping to map position to/from current view
|
|
* 4. If restore, then sets layout
|
|
* a. Maps viewport position by currently displayed viewport map id
|
|
* b. Uses toggle information to map display set id
|
|
* 5. Else applies the hanging protocol
|
|
* a. HP Service is provided displaySetSelectorMap
|
|
* b. HP Service will throw an exception if it isn't applicable
|
|
* @param options - contains information on the HP to apply
|
|
* @param options.activeStudyUID - the updated study to apply the HP to
|
|
* @param options.protocolId - the protocol ID to change to
|
|
* @param options.stageId - the stageId to apply
|
|
* @param options.stageIndex - the index of the stage to go to.
|
|
* @param options.reset - flag to indicate if the HP should be reset to its original and not restored to a previous state
|
|
*
|
|
* commandsManager.run('setHangingProtocol', {
|
|
* activeStudyUID: '1.2.3',
|
|
* protocolId: 'myProtocol',
|
|
* stageId: 'myStage',
|
|
* stageIndex: 0,
|
|
* reset: false,
|
|
* });
|
|
*/
|
|
setHangingProtocol: ({
|
|
activeStudyUID = '',
|
|
protocolId,
|
|
stageId,
|
|
stageIndex,
|
|
reset = false,
|
|
}: HangingProtocolParams): boolean => {
|
|
try {
|
|
// Stores in the state the display set selector id to displaySetUID mapping
|
|
// Pass in viewportId for the active viewport. This item will get set as
|
|
// the activeViewportId
|
|
const state = viewportGridService.getState();
|
|
const hpInfo = hangingProtocolService.getState();
|
|
reuseCachedLayouts(state, hangingProtocolService);
|
|
const { hangingProtocolStageIndexMap } = useHangingProtocolStageIndexStore.getState();
|
|
const { displaySetSelectorMap } = useDisplaySetSelectorStore.getState();
|
|
|
|
if (!protocolId) {
|
|
// Reuse the previous protocol id, and optionally stage
|
|
protocolId = hpInfo.protocolId;
|
|
if (stageId === undefined && stageIndex === undefined) {
|
|
stageIndex = hpInfo.stageIndex;
|
|
}
|
|
} else if (stageIndex === undefined && stageId === undefined) {
|
|
// Re-set the same stage as was previously used
|
|
const hangingId = `${activeStudyUID || hpInfo.activeStudyUID}:${protocolId}`;
|
|
stageIndex = hangingProtocolStageIndexMap[hangingId]?.stageIndex;
|
|
}
|
|
|
|
const useStageIdx =
|
|
stageIndex ??
|
|
hangingProtocolService.getStageIndex(protocolId, {
|
|
stageId,
|
|
stageIndex,
|
|
});
|
|
|
|
if (activeStudyUID) {
|
|
hangingProtocolService.setActiveStudyUID(activeStudyUID);
|
|
}
|
|
|
|
const storedHanging = `${hangingProtocolService.getState().activeStudyUID}:${protocolId}:${
|
|
useStageIdx || 0
|
|
}`;
|
|
|
|
const { viewportGridState } = useViewportGridStore.getState();
|
|
const restoreProtocol = !reset && viewportGridState[storedHanging];
|
|
|
|
if (
|
|
protocolId === hpInfo.protocolId &&
|
|
useStageIdx === hpInfo.stageIndex &&
|
|
!activeStudyUID
|
|
) {
|
|
// Clear the HP setting to reset them
|
|
hangingProtocolService.setProtocol(protocolId, {
|
|
stageId,
|
|
stageIndex: useStageIdx,
|
|
});
|
|
} else {
|
|
hangingProtocolService.setProtocol(protocolId, {
|
|
displaySetSelectorMap,
|
|
stageId,
|
|
stageIndex: useStageIdx,
|
|
restoreProtocol,
|
|
});
|
|
if (restoreProtocol) {
|
|
viewportGridService.set(viewportGridState[storedHanging]);
|
|
}
|
|
}
|
|
// Do this after successfully applying the update
|
|
const { setDisplaySetSelector } = useDisplaySetSelectorStore.getState();
|
|
setDisplaySetSelector(
|
|
`${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`,
|
|
null
|
|
);
|
|
return true;
|
|
} catch (e) {
|
|
console.error(e);
|
|
uiNotificationService.show({
|
|
title: 'Apply Hanging Protocol',
|
|
message: 'The hanging protocol could not be applied.',
|
|
type: 'error',
|
|
duration: 3000,
|
|
});
|
|
return false;
|
|
}
|
|
},
|
|
|
|
toggleHangingProtocol: ({ protocolId, stageIndex }: HangingProtocolParams): boolean => {
|
|
const {
|
|
protocol,
|
|
stageIndex: desiredStageIndex,
|
|
activeStudy,
|
|
} = hangingProtocolService.getActiveProtocol();
|
|
const { toggleHangingProtocol, setToggleHangingProtocol } =
|
|
useToggleHangingProtocolStore.getState();
|
|
const storedHanging = `${activeStudy.StudyInstanceUID}:${protocolId}:${stageIndex | 0}`;
|
|
if (
|
|
protocol.id === protocolId &&
|
|
(stageIndex === undefined || stageIndex === desiredStageIndex)
|
|
) {
|
|
// Toggling off - restore to previous state
|
|
const previousState = toggleHangingProtocol[storedHanging] || {
|
|
protocolId: 'default',
|
|
};
|
|
return actions.setHangingProtocol(previousState);
|
|
} else {
|
|
setToggleHangingProtocol(storedHanging, {
|
|
protocolId: protocol.id,
|
|
stageIndex: desiredStageIndex,
|
|
});
|
|
return actions.setHangingProtocol({
|
|
protocolId,
|
|
stageIndex,
|
|
reset: true,
|
|
});
|
|
}
|
|
},
|
|
|
|
deltaStage: ({ direction }) => {
|
|
const { protocolId, stageIndex: oldStageIndex } = hangingProtocolService.getState();
|
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
|
for (
|
|
let stageIndex = oldStageIndex + direction;
|
|
stageIndex >= 0 && stageIndex < protocol.stages.length;
|
|
stageIndex += direction
|
|
) {
|
|
if (protocol.stages[stageIndex].status !== 'disabled') {
|
|
return actions.setHangingProtocol({
|
|
protocolId,
|
|
stageIndex,
|
|
});
|
|
}
|
|
}
|
|
uiNotificationService.show({
|
|
title: 'Change Stage',
|
|
message: 'The hanging protocol has no more applicable stages',
|
|
type: 'info',
|
|
duration: 3000,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Changes the viewport grid layout in terms of the MxN layout.
|
|
*/
|
|
setViewportGridLayout: ({ numRows, numCols, isHangingProtocolLayout = false }) => {
|
|
const { protocol } = hangingProtocolService.getActiveProtocol();
|
|
const onLayoutChange = protocol.callbacks?.onLayoutChange;
|
|
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
|
|
console.log('setViewportGridLayout running', onLayoutChange, numRows, numCols);
|
|
// Don't apply the layout if the run command returns false
|
|
return;
|
|
}
|
|
|
|
const completeLayout = () => {
|
|
const state = viewportGridService.getState();
|
|
findViewportsByPosition(state, { numRows, numCols });
|
|
|
|
const { viewportsByPosition, initialInDisplay } = useViewportsByPositionStore.getState();
|
|
|
|
const findOrCreateViewport = layoutFindOrCreate.bind(
|
|
null,
|
|
hangingProtocolService,
|
|
isHangingProtocolLayout,
|
|
{ ...viewportsByPosition, initialInDisplay }
|
|
);
|
|
|
|
viewportGridService.setLayout({
|
|
numRows,
|
|
numCols,
|
|
findOrCreateViewport,
|
|
isHangingProtocolLayout,
|
|
});
|
|
};
|
|
// Need to finish any work in the callback
|
|
window.setTimeout(completeLayout, 0);
|
|
},
|
|
|
|
toggleOneUp() {
|
|
const viewportGridState = viewportGridService.getState();
|
|
const { activeViewportId, viewports, layout, isHangingProtocolLayout } = viewportGridState;
|
|
const { displaySetInstanceUIDs, displaySetOptions, viewportOptions } =
|
|
viewports.get(activeViewportId);
|
|
|
|
if (layout.numCols === 1 && layout.numRows === 1) {
|
|
// The viewer is in one-up. Check if there is a state to restore/toggle back to.
|
|
const { toggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState();
|
|
|
|
if (!toggleOneUpViewportGridStore) {
|
|
return;
|
|
}
|
|
// There is a state to toggle back to. The viewport that was
|
|
// originally toggled to one up was the former active viewport.
|
|
const viewportIdToUpdate = toggleOneUpViewportGridStore.activeViewportId;
|
|
|
|
// We are restoring the previous layout but taking into the account that
|
|
// the current one up viewport might have a new displaySet dragged and dropped on it.
|
|
// updatedViewportsViaHP below contains the viewports applicable to the HP that existed
|
|
// prior to the toggle to one-up - including the updated viewports if a display
|
|
// set swap were to have occurred.
|
|
const updatedViewportsViaHP =
|
|
displaySetInstanceUIDs.length > 1
|
|
? []
|
|
: displaySetInstanceUIDs
|
|
.map(displaySetInstanceUID =>
|
|
hangingProtocolService.getViewportsRequireUpdate(
|
|
viewportIdToUpdate,
|
|
displaySetInstanceUID,
|
|
isHangingProtocolLayout
|
|
)
|
|
)
|
|
.flat();
|
|
|
|
// findOrCreateViewport returns either one of the updatedViewportsViaHP
|
|
// returned from the HP service OR if there is not one from the HP service then
|
|
// simply returns what was in the previous state for a given position in the layout.
|
|
const findOrCreateViewport = (position: number, positionId: string) => {
|
|
// Find the viewport for the given position prior to the toggle to one-up.
|
|
const preOneUpViewport = Array.from(toggleOneUpViewportGridStore.viewports.values()).find(
|
|
viewport => viewport.positionId === positionId
|
|
);
|
|
|
|
// Use the viewport id from before the toggle to one-up to find any updates to the viewport.
|
|
const viewport = updatedViewportsViaHP.find(
|
|
viewport => viewport.viewportId === preOneUpViewport.viewportId
|
|
);
|
|
|
|
return viewport
|
|
? // Use the applicable viewport from the HP updated viewports
|
|
{ viewportOptions, displaySetOptions, ...viewport }
|
|
: // Use the previous viewport for the given position
|
|
preOneUpViewport;
|
|
};
|
|
|
|
const layoutOptions = viewportGridService.getLayoutOptionsFromState(
|
|
toggleOneUpViewportGridStore
|
|
);
|
|
|
|
// Restore the previous layout including the active viewport.
|
|
viewportGridService.setLayout({
|
|
numRows: toggleOneUpViewportGridStore.layout.numRows,
|
|
numCols: toggleOneUpViewportGridStore.layout.numCols,
|
|
activeViewportId: viewportIdToUpdate,
|
|
layoutOptions,
|
|
findOrCreateViewport,
|
|
isHangingProtocolLayout: true,
|
|
});
|
|
|
|
// Reset crosshairs after restoring the layout
|
|
setTimeout(() => {
|
|
commandsManager.runCommand('resetCrosshairs');
|
|
}, 0);
|
|
} else {
|
|
// We are not in one-up, so toggle to one up.
|
|
|
|
// Store the current viewport grid state so we can toggle it back later.
|
|
const { setToggleOneUpViewportGridStore } = useToggleOneUpViewportGridStore.getState();
|
|
setToggleOneUpViewportGridStore(viewportGridState);
|
|
|
|
// one being toggled to one up.
|
|
const findOrCreateViewport = () => {
|
|
return {
|
|
displaySetInstanceUIDs,
|
|
displaySetOptions,
|
|
viewportOptions,
|
|
};
|
|
};
|
|
|
|
// Set the layout to be 1x1/one-up.
|
|
viewportGridService.setLayout({
|
|
numRows: 1,
|
|
numCols: 1,
|
|
findOrCreateViewport,
|
|
isHangingProtocolLayout: true,
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Exposes the browser history navigation used by OHIF. This command can be used to either replace or
|
|
* push a new entry into the browser history. For example, the following will replace the current
|
|
* browser history entry with the specified relative URL which changes the study displayed to the
|
|
* study with study instance UID 1.2.3. Note that as a result of using `options.replace = true`, the
|
|
* page prior to invoking this command cannot be returned to via the browser back button.
|
|
*
|
|
* navigateHistory({
|
|
* to: 'viewer?StudyInstanceUIDs=1.2.3',
|
|
* options: { replace: true },
|
|
* });
|
|
*
|
|
* @param historyArgs - arguments for the history function;
|
|
* the `to` property is the URL;
|
|
* the `options.replace` is a boolean indicating if the current browser history entry
|
|
* should be replaced or a new entry pushed onto the history (stack); the default value
|
|
* for `replace` is false
|
|
*/
|
|
navigateHistory(historyArgs: NavigateHistory) {
|
|
history.navigate(historyArgs.to, historyArgs.options);
|
|
},
|
|
|
|
openDICOMTagViewer({ displaySetInstanceUID }: { displaySetInstanceUID?: string }) {
|
|
const { activeViewportId, viewports } = viewportGridService.getState();
|
|
const activeViewportSpecificData = viewports.get(activeViewportId);
|
|
const { displaySetInstanceUIDs } = activeViewportSpecificData;
|
|
|
|
const displaySets = displaySetService.activeDisplaySets;
|
|
const { UIModalService } = servicesManager.services;
|
|
|
|
const defaultDisplaySetInstanceUID = displaySetInstanceUID || displaySetInstanceUIDs[0];
|
|
UIModalService.show({
|
|
content: DicomTagBrowser,
|
|
contentProps: {
|
|
displaySets,
|
|
displaySetInstanceUID: defaultDisplaySetInstanceUID,
|
|
onClose: UIModalService.hide,
|
|
},
|
|
containerDimensions: 'w-[70%] max-w-[900px]',
|
|
title: 'DICOM Tag Browser',
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Toggle viewport overlay (the information panel shown on the four corners
|
|
* of the viewport)
|
|
* @see ViewportOverlay and CustomizableViewportOverlay components
|
|
*/
|
|
toggleOverlays: () => {
|
|
const overlays = document.getElementsByClassName('viewport-overlay');
|
|
for (let i = 0; i < overlays.length; i++) {
|
|
overlays.item(i).classList.toggle('hidden');
|
|
}
|
|
},
|
|
|
|
scrollActiveThumbnailIntoView: () => {
|
|
const { activeViewportId, viewports } = viewportGridService.getState();
|
|
|
|
const activeViewport = viewports.get(activeViewportId);
|
|
const activeDisplaySetInstanceUID = activeViewport.displaySetInstanceUIDs[0];
|
|
|
|
const thumbnailList = document.querySelector('#ohif-thumbnail-list');
|
|
|
|
if (!thumbnailList) {
|
|
return;
|
|
}
|
|
|
|
const thumbnailListBounds = thumbnailList.getBoundingClientRect();
|
|
|
|
const thumbnail = document.querySelector(`#thumbnail-${activeDisplaySetInstanceUID}`);
|
|
|
|
if (!thumbnail) {
|
|
return;
|
|
}
|
|
|
|
const thumbnailBounds = thumbnail.getBoundingClientRect();
|
|
|
|
// This only handles a vertical thumbnail list.
|
|
if (
|
|
thumbnailBounds.top >= thumbnailListBounds.top &&
|
|
thumbnailBounds.top <= thumbnailListBounds.bottom
|
|
) {
|
|
return;
|
|
}
|
|
|
|
thumbnail.scrollIntoView({ behavior: 'smooth' });
|
|
},
|
|
|
|
updateViewportDisplaySet: ({
|
|
direction,
|
|
excludeNonImageModalities,
|
|
}: UpdateViewportDisplaySetParams) => {
|
|
const nonImageModalities = ['SR', 'SEG', 'SM', 'RTSTRUCT', 'RTPLAN', 'RTDOSE'];
|
|
|
|
const currentDisplaySets = [...displaySetService.activeDisplaySets];
|
|
|
|
const { activeViewportId, viewports, isHangingProtocolLayout } =
|
|
viewportGridService.getState();
|
|
|
|
const { displaySetInstanceUIDs } = viewports.get(activeViewportId);
|
|
|
|
const activeDisplaySetIndex = currentDisplaySets.findIndex(displaySet =>
|
|
displaySetInstanceUIDs.includes(displaySet.displaySetInstanceUID)
|
|
);
|
|
|
|
let displaySetIndexToShow: number;
|
|
|
|
for (
|
|
displaySetIndexToShow = activeDisplaySetIndex + direction;
|
|
displaySetIndexToShow > -1 && displaySetIndexToShow < currentDisplaySets.length;
|
|
displaySetIndexToShow += direction
|
|
) {
|
|
if (
|
|
!excludeNonImageModalities ||
|
|
!nonImageModalities.includes(currentDisplaySets[displaySetIndexToShow].Modality)
|
|
) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (displaySetIndexToShow < 0 || displaySetIndexToShow >= currentDisplaySets.length) {
|
|
return;
|
|
}
|
|
|
|
const { displaySetInstanceUID } = currentDisplaySets[displaySetIndexToShow];
|
|
|
|
let updatedViewports = [];
|
|
|
|
try {
|
|
updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
|
activeViewportId,
|
|
displaySetInstanceUID,
|
|
isHangingProtocolLayout
|
|
);
|
|
} catch (error) {
|
|
console.warn(error);
|
|
uiNotificationService.show({
|
|
title: 'Navigate Viewport Display Set',
|
|
message:
|
|
'The requested display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.',
|
|
type: 'info',
|
|
duration: 3000,
|
|
});
|
|
}
|
|
|
|
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
|
|
|
setTimeout(() => actions.scrollActiveThumbnailIntoView(), 0);
|
|
},
|
|
};
|
|
|
|
const definitions = {
|
|
showContextMenu: {
|
|
commandFn: actions.showContextMenu,
|
|
},
|
|
closeContextMenu: {
|
|
commandFn: actions.closeContextMenu,
|
|
},
|
|
clearMeasurements: {
|
|
commandFn: actions.clearMeasurements,
|
|
},
|
|
displayNotification: {
|
|
commandFn: actions.displayNotification,
|
|
},
|
|
setHangingProtocol: {
|
|
commandFn: actions.setHangingProtocol,
|
|
},
|
|
toggleHangingProtocol: {
|
|
commandFn: actions.toggleHangingProtocol,
|
|
},
|
|
navigateHistory: {
|
|
commandFn: actions.navigateHistory,
|
|
},
|
|
nextStage: {
|
|
commandFn: actions.deltaStage,
|
|
options: { direction: 1 },
|
|
},
|
|
previousStage: {
|
|
commandFn: actions.deltaStage,
|
|
options: { direction: -1 },
|
|
},
|
|
setViewportGridLayout: {
|
|
commandFn: actions.setViewportGridLayout,
|
|
},
|
|
toggleOneUp: {
|
|
commandFn: actions.toggleOneUp,
|
|
},
|
|
openDICOMTagViewer: {
|
|
commandFn: actions.openDICOMTagViewer,
|
|
},
|
|
updateViewportDisplaySet: {
|
|
commandFn: actions.updateViewportDisplaySet,
|
|
},
|
|
};
|
|
|
|
return {
|
|
actions,
|
|
definitions,
|
|
defaultContext: 'DEFAULT',
|
|
};
|
|
};
|
|
|
|
export default commandsModule;
|