feat: state sync service and hanging protocol updates to preserve state (#3131)
* feat: Add state sync and use it to remember viewport grid info fix: Version updates Fixes for toggling MPR mode Fix the display when the interleaved load module fails Fix the memory of the state to restore correctly PR fixes for the state sync service PR fixes PR fixes PR fixes Added a hack warning to remove volumeDeactivate Fixes for TMTV colormap setting Fix the casing Missed renames fix: tests not running due to variance in ordering Reverting some fixes to change case PR changes - mostly comments and minor improvements fix: All display sets were being updated on drag and drop PR fixes - mostly renames PR fixes Test support for OHIF, for HP branch test: Add at least a minimal set of automated tests for hanging protocols Docs PR fixes Merge fixes DOCS updates Add an example of the mn hanging protocol PR fixes PR fixes PR fixes * Fix the drag and drop PR fixes * PR changes - update default keys for next/previous stage * fix: Was storing the custom viewport grid too aggressively Caused by a PR change misspelling a variable
This commit is contained in:
1 parent
b7fff77e17
commit
803f638401
87 files changed
+3413
-1919
No files matched your search
@@ -303,6 +303,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
|
||||
seriesDate: ds.SeriesDate,
|
||||
seriesTime: ds.SeriesTime,
|
||||
numInstances: ds.numImageFrames,
|
||||
countIcon: ds.countIcon,
|
||||
StudyInstanceUID: ds.StudyInstanceUID,
|
||||
componentType,
|
||||
imageSrc,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
LayoutSelector as OHIFLayoutSelector,
|
||||
ToolbarButton,
|
||||
useViewportGrid,
|
||||
} from '@ohif/ui';
|
||||
import { LayoutSelector as OHIFLayoutSelector, ToolbarButton } from '@ohif/ui';
|
||||
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
|
||||
@@ -16,8 +12,6 @@ function LayoutSelector({
|
||||
...rest
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [disableSelector, setDisableSelector] = useState(false);
|
||||
const [viewportGridState, viewportGridService] = useViewportGrid();
|
||||
|
||||
const {
|
||||
hangingProtocolService,
|
||||
@@ -50,43 +44,19 @@ function LayoutSelector({
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
/* Reset to default layout when component unmounts */
|
||||
return () => {
|
||||
viewportGridService.setLayout({ numCols: 1, numRows: 1 });
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onInteractionHandler = () => setIsOpen(!isOpen);
|
||||
const DropdownContent = isOpen ? OHIFLayoutSelector : null;
|
||||
|
||||
const onSelectionHandler = ({ numRows, numCols }) => {
|
||||
// TODO Introduce a service to persist the state of the current hanging protocol/app.
|
||||
|
||||
// TODO Here the layout change will amount to a change of hanging protocol as specified by the extension for this layout selector tool
|
||||
// followed by the change of the grid itself.
|
||||
if (hangingProtocolService.getActiveProtocol().protocol.id === 'mpr') {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'MPR',
|
||||
itemId: 'MPR',
|
||||
interactionType: 'toggle',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleMPR',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// When a new layout is selected, keep any extra/offscreen viewports
|
||||
// so that if any of those viewports were populated via the UI then they
|
||||
// will be maintained in case those viewports are redisplayed later.
|
||||
viewportGridService.setLayout({
|
||||
numRows,
|
||||
numCols,
|
||||
keepExtraViewports: true,
|
||||
const onSelectionHandler = props => {
|
||||
toolbarService.recordInteraction({
|
||||
interactionType: 'action',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'setViewportGridLayout',
|
||||
commandOptions: { ...props },
|
||||
context: 'DEFAULT',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -107,7 +77,7 @@ function LayoutSelector({
|
||||
/>
|
||||
)
|
||||
}
|
||||
isActive={disableSelector ? false : isOpen}
|
||||
isActive={isOpen}
|
||||
type="toggle"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,12 @@ import {
|
||||
LoadingIndicatorProgress,
|
||||
} from '@ohif/ui';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { hotkeys } from '@ohif/core';
|
||||
import {
|
||||
ServicesManager,
|
||||
HangingProtocolService,
|
||||
hotkeys,
|
||||
CommandsManager,
|
||||
} from '@ohif/core';
|
||||
import { useAppConfig } from '@state';
|
||||
import Toolbar from '../Toolbar/Toolbar';
|
||||
|
||||
@@ -33,7 +38,7 @@ function ViewerLayout({
|
||||
rightPanels = [],
|
||||
leftPanelDefaultClosed = false,
|
||||
rightPanelDefaultClosed = false,
|
||||
}) {
|
||||
}): React.FunctionComponent {
|
||||
const [appConfig] = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@@ -169,15 +174,13 @@ function ViewerLayout({
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = hangingProtocolService.subscribe(
|
||||
hangingProtocolService.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT,
|
||||
HangingProtocolService.EVENTS.PROTOCOL_CHANGED,
|
||||
|
||||
// Todo: right now to set the loading indicator to false, we need to wait for the
|
||||
// hangingProtocolService to finish applying the viewport matching to each viewport,
|
||||
// however, this might not be the only approach to set the loading indicator to false. we need to explore this further.
|
||||
({ progress }) => {
|
||||
if (progress === 100) {
|
||||
setShowLoadingIndicator(false);
|
||||
}
|
||||
() => {
|
||||
setShowLoadingIndicator(false);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -265,7 +268,8 @@ ViewerLayout.propTypes = {
|
||||
extensionManager: PropTypes.shape({
|
||||
getModuleEntry: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
commandsManager: PropTypes.object,
|
||||
commandsManager: PropTypes.instanceOf(CommandsManager),
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
// From modes
|
||||
leftPanels: PropTypes.array,
|
||||
rightPanels: PropTypes.array,
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||
import React from 'react';
|
||||
|
||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
const {
|
||||
measurementService,
|
||||
hangingProtocolService,
|
||||
uiNotificationService,
|
||||
viewportGridService,
|
||||
displaySetService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const actions = {
|
||||
displayNotification: ({ text, title, type }) => {
|
||||
uiNotificationService.show({
|
||||
title: title,
|
||||
message: text,
|
||||
type: type,
|
||||
});
|
||||
},
|
||||
clearMeasurements: () => {
|
||||
measurementService.clear();
|
||||
},
|
||||
nextStage: () => {
|
||||
// next stage in hanging protocols
|
||||
hangingProtocolService.nextProtocolStage();
|
||||
},
|
||||
previousStage: () => {
|
||||
hangingProtocolService.previousProtocolStage();
|
||||
},
|
||||
openDICOMTagViewer() {
|
||||
const { activeViewportIndex, viewports } = viewportGridService.getState();
|
||||
const activeViewportSpecificData = viewports[activeViewportIndex];
|
||||
const { displaySetInstanceUIDs } = activeViewportSpecificData;
|
||||
|
||||
const displaySets = displaySetService.activeDisplaySets;
|
||||
const { uiModalService } = servicesManager.services;
|
||||
|
||||
const displaySetInstanceUID = displaySetInstanceUIDs[0];
|
||||
uiModalService.show({
|
||||
content: DicomTagBrowser,
|
||||
contentProps: {
|
||||
displaySets,
|
||||
displaySetInstanceUID,
|
||||
onClose: uiModalService.hide,
|
||||
},
|
||||
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');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
clearMeasurements: {
|
||||
commandFn: actions.clearMeasurements,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
displayNotification: {
|
||||
commandFn: actions.displayNotification,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
nextStage: {
|
||||
commandFn: actions.nextStage,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
previousStage: {
|
||||
commandFn: actions.previousStage,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
openDICOMTagViewer: {
|
||||
commandFn: actions.openDICOMTagViewer,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'DEFAULT',
|
||||
};
|
||||
};
|
||||
|
||||
export default commandsModule;
|
||||
@@ -0,0 +1,386 @@
|
||||
import { DicomMetadataStore, ServicesManager } from '@ohif/core';
|
||||
|
||||
import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser';
|
||||
import reuseCachedLayouts from './utils/reuseCachedLayouts';
|
||||
import findViewportsByPosition, {
|
||||
findOrCreateViewport as layoutFindOrCreate,
|
||||
} from './findViewportsByPosition';
|
||||
|
||||
export type HangingProtocolParams = {
|
||||
protocolId?: string;
|
||||
stageIndex?: number;
|
||||
activeStudyUID?: string;
|
||||
stageId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if a command is a hanging protocol one.
|
||||
* For now, just use the two hanging protocol commands that are in this
|
||||
* commands module, but if others get added elsewhere this may need enhancing.
|
||||
*/
|
||||
const isHangingProtocolCommand = command =>
|
||||
command &&
|
||||
(command.commandName === 'setHangingProtocol' ||
|
||||
command.commandName === 'toggleHangingProtocol');
|
||||
|
||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||
const {
|
||||
measurementService,
|
||||
hangingProtocolService,
|
||||
uiNotificationService,
|
||||
viewportGridService,
|
||||
displaySetService,
|
||||
stateSyncService,
|
||||
toolbarService,
|
||||
} = (servicesManager as ServicesManager).services;
|
||||
|
||||
const actions = {
|
||||
displayNotification: ({ text, title, type }) => {
|
||||
uiNotificationService.show({
|
||||
title: title,
|
||||
message: text,
|
||||
type: type,
|
||||
});
|
||||
},
|
||||
clearMeasurements: () => {
|
||||
measurementService.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles off all tools which contain a commandName of setHangingProtocol
|
||||
* or toggleHangingProtocol, and which match/don't match the protocol id/stage
|
||||
*/
|
||||
toggleHpTools: () => {
|
||||
const {
|
||||
protocol,
|
||||
stageIndex: toggleStageIndex,
|
||||
stage,
|
||||
} = hangingProtocolService.getActiveProtocol();
|
||||
const enableListener = button => {
|
||||
if (!button.id) return;
|
||||
const { commands, items } = button.props || button;
|
||||
if (items) {
|
||||
items.forEach(enableListener);
|
||||
}
|
||||
const hpCommand = commands?.find?.(isHangingProtocolCommand);
|
||||
if (!hpCommand) return;
|
||||
const { protocolId, stageIndex, stageId } = hpCommand.commandOptions;
|
||||
const isActive =
|
||||
(!protocolId || protocolId === protocol.id) &&
|
||||
(stageIndex === undefined || stageIndex === toggleStageIndex) &&
|
||||
(!stageId || stageId === stage.id);
|
||||
toolbarService.setActive(button.id, isActive);
|
||||
};
|
||||
Object.values(toolbarService.getButtons()).forEach(enableListener);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
setHangingProtocol: ({
|
||||
activeStudyUID = '',
|
||||
protocolId,
|
||||
stageId,
|
||||
stageIndex,
|
||||
}: HangingProtocolParams): boolean => {
|
||||
try {
|
||||
// Stores in the state the reuseID 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();
|
||||
const {
|
||||
protocol: oldProtocol,
|
||||
} = hangingProtocolService.getActiveProtocol();
|
||||
const stateSyncReduce = reuseCachedLayouts(
|
||||
state,
|
||||
hangingProtocolService,
|
||||
stateSyncService
|
||||
);
|
||||
const {
|
||||
hangingProtocolStageIndexMap,
|
||||
viewportGridStore,
|
||||
displaySetSelectorMap,
|
||||
} = stateSyncReduce;
|
||||
|
||||
if (!protocolId) {
|
||||
// Re-use 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 restoreProtocol = !!viewportGridStore[storedHanging];
|
||||
|
||||
if (
|
||||
protocolId === hpInfo.hangingProtocolId &&
|
||||
useStageIdx === hpInfo.stageIdx &&
|
||||
!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(viewportGridStore[storedHanging]);
|
||||
}
|
||||
}
|
||||
// Do this after successfully applying the update
|
||||
stateSyncService.store(stateSyncReduce);
|
||||
// This is a default action applied
|
||||
actions.toggleHpTools(hangingProtocolService.getActiveProtocol());
|
||||
// Send the notification about updating the state
|
||||
if (protocolId !== hpInfo.protocolId) {
|
||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||
// 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;
|
||||
} catch (e) {
|
||||
actions.toggleHpTools(hangingProtocolService.getActiveProtocol());
|
||||
uiNotificationService.show({
|
||||
title: 'Apply Hanging Protocol',
|
||||
message: `The hanging protocol could not be applied due to ${e}`,
|
||||
type: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
toggleHangingProtocol: ({
|
||||
protocolId,
|
||||
stageIndex,
|
||||
}: HangingProtocolParams): boolean => {
|
||||
const {
|
||||
protocol,
|
||||
stageIndex: desiredStageIndex,
|
||||
activeStudy,
|
||||
} = hangingProtocolService.getActiveProtocol();
|
||||
const { toggleHangingProtocol } = stateSyncService.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 {
|
||||
stateSyncService.store({
|
||||
toggleHangingProtocol: {
|
||||
...toggleHangingProtocol,
|
||||
[storedHanging]: {
|
||||
protocolId: protocol.id,
|
||||
stageIndex: desiredStageIndex,
|
||||
},
|
||||
},
|
||||
});
|
||||
return actions.setHangingProtocol({ protocolId, stageIndex });
|
||||
}
|
||||
},
|
||||
|
||||
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: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the viewport grid layout in terms of the MxN layout.
|
||||
*/
|
||||
setViewportGridLayout: ({ numRows, numCols }) => {
|
||||
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();
|
||||
const stateReduce = findViewportsByPosition(
|
||||
state,
|
||||
{ numRows, numCols },
|
||||
stateSyncService
|
||||
);
|
||||
const findOrCreateViewport = layoutFindOrCreate.bind(
|
||||
null,
|
||||
hangingProtocolService,
|
||||
stateReduce.viewportsByPosition
|
||||
);
|
||||
|
||||
viewportGridService.setLayout({
|
||||
numRows,
|
||||
numCols,
|
||||
findOrCreateViewport,
|
||||
});
|
||||
stateSyncService.store(stateReduce);
|
||||
};
|
||||
// Need to finish any work in the callback
|
||||
window.setTimeout(completeLayout, 0);
|
||||
},
|
||||
|
||||
openDICOMTagViewer() {
|
||||
const { activeViewportIndex, viewports } = viewportGridService.getState();
|
||||
const activeViewportSpecificData = viewports[activeViewportIndex];
|
||||
const { displaySetInstanceUIDs } = activeViewportSpecificData;
|
||||
|
||||
const displaySets = displaySetService.activeDisplaySets;
|
||||
const { UIModalService } = servicesManager.services;
|
||||
|
||||
const displaySetInstanceUID = displaySetInstanceUIDs[0];
|
||||
UIModalService.show({
|
||||
content: DicomTagBrowser,
|
||||
contentProps: {
|
||||
displaySets,
|
||||
displaySetInstanceUID,
|
||||
onClose: UIModalService.hide,
|
||||
},
|
||||
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');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
clearMeasurements: {
|
||||
commandFn: actions.clearMeasurements,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
displayNotification: {
|
||||
commandFn: actions.displayNotification,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setHangingProtocol: {
|
||||
commandFn: actions.setHangingProtocol,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
toggleHangingProtocol: {
|
||||
commandFn: actions.toggleHangingProtocol,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
nextStage: {
|
||||
commandFn: actions.deltaStage,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousStage: {
|
||||
commandFn: actions.deltaStage,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
setViewportGridLayout: {
|
||||
commandFn: actions.setViewportGridLayout,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
openDICOMTagViewer: {
|
||||
commandFn: actions.openDICOMTagViewer,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'DEFAULT',
|
||||
};
|
||||
};
|
||||
|
||||
export default commandsModule;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { StateSyncService, Types } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* This find or create viewport is paired with the reduce results from
|
||||
* below, and the action of this viewport is to look for previously filled
|
||||
* viewports, and to re-use by position id. If there is no filled viewport,
|
||||
* then one can be re-used from the display set if it isn't going to be displayed.
|
||||
* @param hangingProtocolService - bound parameter supplied before using this
|
||||
* @param viewportsByPosition - bound parameter supplied before using this
|
||||
* @param viewportIndex - the index to retrieve
|
||||
* @param positionId - the current position on screen to retrieve
|
||||
* @param options - the set of options used, so that subsequent calls can
|
||||
* store state that is reset by the setLayout.
|
||||
* This class uses the options to store the already viewed
|
||||
* display sets, filling it initially with the pre-existing viewports.
|
||||
*/
|
||||
export const findOrCreateViewport = (
|
||||
hangingProtocolService,
|
||||
viewportsByPosition,
|
||||
viewportIndex: number,
|
||||
positionId: string,
|
||||
options: Record<string, unknown>
|
||||
) => {
|
||||
const byPositionViewport = viewportsByPosition?.[positionId];
|
||||
if (byPositionViewport) return { ...byPositionViewport };
|
||||
const { protocolId, stageIndex } = hangingProtocolService.getState();
|
||||
|
||||
// Setup the initial in display correctly for initial view/select
|
||||
if (!options.inDisplay) {
|
||||
options.inDisplay = [...viewportsByPosition.initialInDisplay];
|
||||
}
|
||||
// See if there is a default viewport for new views.
|
||||
const missing = hangingProtocolService.getMissingViewport(
|
||||
protocolId,
|
||||
stageIndex,
|
||||
options
|
||||
);
|
||||
if (missing) {
|
||||
const displaySetInstanceUIDs = missing.displaySetsInfo.map(
|
||||
it => it.displaySetInstanceUID
|
||||
);
|
||||
options.inDisplay.push(...displaySetInstanceUIDs);
|
||||
return {
|
||||
displaySetInstanceUIDs,
|
||||
displaySetOptions: missing.displaySetsInfo.map(
|
||||
it => it.displaySetOptions
|
||||
),
|
||||
viewportOptions: {
|
||||
...missing.viewportOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Records the information on what viewports are displayed in which position.
|
||||
* Also records what instances from the existing positions are going to be in
|
||||
* view initially.
|
||||
* @param state is the viewport grid state
|
||||
* @param syncService is the state sync service to use for getting existing state
|
||||
* @returns Set of states that can be applied to the state sync to remember
|
||||
* the current view state.
|
||||
*/
|
||||
const findViewportsByPosition = (
|
||||
state,
|
||||
{ numRows, numCols },
|
||||
syncService: StateSyncService
|
||||
): Record<string, Record<string, unknown>> => {
|
||||
const { viewports } = state;
|
||||
const syncState = syncService.getState();
|
||||
const viewportsByPosition = { ...syncState.viewportsByPosition };
|
||||
const initialInDisplay = [];
|
||||
|
||||
for (const viewport of viewports) {
|
||||
if (viewport.positionId) {
|
||||
const storedViewport = {
|
||||
...viewport,
|
||||
viewportOptions: { ...viewport.viewportOptions },
|
||||
};
|
||||
viewportsByPosition[viewport.positionId] = storedViewport;
|
||||
// The cache doesn't store the viewport options - it is only useful
|
||||
// for remembering the type of viewport and UIDs
|
||||
delete storedViewport.viewportId;
|
||||
delete storedViewport.viewportOptions.viewportId;
|
||||
}
|
||||
}
|
||||
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
for (let col = 0; col < numCols; col++) {
|
||||
const pos = col + row * numCols;
|
||||
const positionId = viewports?.[pos]?.positionId || `${col}-${row}`;
|
||||
const viewport = viewportsByPosition[positionId];
|
||||
if (viewport?.displaySetInstanceUIDs) {
|
||||
initialInDisplay.push(...viewport.displaySetInstanceUIDs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store the initially displayed elements
|
||||
viewportsByPosition.initialInDisplay = initialInDisplay;
|
||||
|
||||
return { viewportsByPosition };
|
||||
};
|
||||
|
||||
export default findViewportsByPosition;
|
||||
@@ -1,6 +1,9 @@
|
||||
const defaultProtocol = {
|
||||
id: 'default',
|
||||
locked: true,
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
@@ -9,6 +12,25 @@ const defaultProtocol = {
|
||||
editableBy: {},
|
||||
protocolMatchingRules: [],
|
||||
toolGroupIds: ['default'],
|
||||
// -1 would be used to indicate active only, whereas other values are
|
||||
// the number of required priors referenced - so 0 means active with
|
||||
// 0 or more priors.
|
||||
numberOfPriorsReferenced: 0,
|
||||
// Default viewport is used to define the viewport when
|
||||
// additional viewports are added using the layout tool
|
||||
defaultViewport: {
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
toolGroupId: 'default',
|
||||
allowUnmatchedView: true,
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'defaultDisplaySetId',
|
||||
matchedDisplaySetsIndex: -1,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySetSelectors: {
|
||||
defaultDisplaySetId: {
|
||||
// Unused currently
|
||||
@@ -24,12 +46,12 @@ const defaultProtocol = {
|
||||
},
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
id: 'hYbmMy3b7pz7GLiaT',
|
||||
name: 'default',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
@@ -38,6 +60,119 @@ const defaultProtocol = {
|
||||
columns: 1,
|
||||
},
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
toolGroupId: 'default',
|
||||
// initialImageOptions: {
|
||||
// index: 180,
|
||||
// preset: 'middle', // 'first', 'last', 'middle'
|
||||
// },
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'defaultDisplaySetId',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
createdDate: '2021-02-23T18:32:42.850Z',
|
||||
},
|
||||
|
||||
// This is an example of a 2x2 layout that requires at least 2 viewports
|
||||
// filled to be navigatable to
|
||||
{
|
||||
name: '2x2',
|
||||
// Indicate that the number of viewports needed is 2 filled viewports,
|
||||
// but that 4 viewports are preferred.
|
||||
stageActivation: {
|
||||
enabled: {
|
||||
minViewportsMatched: 4,
|
||||
},
|
||||
passive: {
|
||||
minViewportsMatched: 2,
|
||||
},
|
||||
},
|
||||
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
},
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'defaultDisplaySetId',
|
||||
matchedDisplaySetsIndex: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'defaultDisplaySetId',
|
||||
matchedDisplaySetsIndex: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'defaultDisplaySetId',
|
||||
matchedDisplaySetsIndex: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
// initialImageOptions: {
|
||||
// index: 180,
|
||||
// preset: 'middle', // 'first', 'last', 'middle'
|
||||
// },
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'defaultDisplaySetId',
|
||||
matchedDisplaySetsIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// This is an example of a layout with more than one element in it
|
||||
// It can be navigated to using , and . (prev/next stage)
|
||||
{
|
||||
name: '1x2',
|
||||
// Indicate that the number of viewports needed is 1 filled viewport,
|
||||
// but that 2 viewports are preferred.
|
||||
stageActivation: {
|
||||
enabled: {
|
||||
minViewportsMatched: 3,
|
||||
},
|
||||
},
|
||||
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 2,
|
||||
},
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
viewportOptions: {
|
||||
@@ -53,11 +188,26 @@ const defaultProtocol = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
toolGroupId: 'default',
|
||||
// initialImageOptions: {
|
||||
// index: 180,
|
||||
// preset: 'middle', // 'first', 'last', 'middle'
|
||||
// },
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'defaultDisplaySetId',
|
||||
// Shows the second index of this image set
|
||||
matchedDisplaySetsIndex: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
createdDate: '2021-02-23T18:32:42.850Z',
|
||||
},
|
||||
],
|
||||
numberOfPriorsReferenced: -1,
|
||||
};
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
|
||||
@@ -29,6 +29,7 @@ const makeDisplaySet = instances => {
|
||||
SeriesDescription: instance.SeriesDescription || '',
|
||||
Modality: instance.Modality,
|
||||
isMultiFrame: isMultiFrame(instance),
|
||||
countIcon: displayReconstructableInfo.value ? 'icon-mpr' : undefined,
|
||||
numImageFrames: instances.length,
|
||||
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
||||
isReconstructable: displayReconstructableInfo.value,
|
||||
|
||||
@@ -11,6 +11,7 @@ const metadataProvider = classes.MetadataProvider;
|
||||
* @param {Object} configuration
|
||||
*/
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
const { stateSyncService } = servicesManager.services;
|
||||
// Add
|
||||
DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||
@@ -23,6 +24,34 @@ export default function init({ servicesManager, configuration }) {
|
||||
DicomMetadataStore.EVENTS.SERIES_UPDATED,
|
||||
handlePETImageMetadata
|
||||
);
|
||||
|
||||
// viewportGridStore is a sync state which stores the entire
|
||||
// ViewportGridService getState, by the keys `<activeStudyUID>:<protocolId>:<stageIndex>`
|
||||
// Used to recover manual changes to the layout of a stage.
|
||||
stateSyncService.register('viewportGridStore', { clearOnModeExit: true });
|
||||
|
||||
// displaySetSelectorMap stores a map from
|
||||
// `<activeStudyUID>:<displaySetSelectorId>:<matchOffset>` to
|
||||
// a displaySetInstanceUID, used to display named display sets in
|
||||
// specific spots within a hanging protocol and be able to remember what the
|
||||
// user did with those named spots between stages and protocols.
|
||||
stateSyncService.register('displaySetSelectorMap', { clearOnModeExit: true });
|
||||
|
||||
// Stores a map from `<activeStudyUID>:${protocolId}` to the getHPInfo results
|
||||
// in order to recover the correct stage when returning to a Hanging Protocol.
|
||||
stateSyncService.register('hangingProtocolStageIndexMap', {
|
||||
clearOnModeExit: true,
|
||||
});
|
||||
|
||||
// Stores a map from the to be applied hanging protocols `<activeStudyUID>:<protocolId>`
|
||||
// to the previously applied hanging protolStageIndexMap key, in order to toggle
|
||||
// off the applied protocol and remember the old state.
|
||||
stateSyncService.register('toggleHangingProtocol', { clearOnModeExit: true });
|
||||
|
||||
// Stores the viewports by `rows-cols` position so that when the layout
|
||||
// changes numRows and numCols, the viewports can be remembers and then replaced
|
||||
// afterwards.
|
||||
stateSyncService.register('viewportsByPosition', { clearOnModeExit: true });
|
||||
}
|
||||
|
||||
const handlePETImageMetadata = ({ SeriesInstanceUID, StudyInstanceUID }) => {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { HangingProtocolService, StateSyncService, Types } from '@ohif/core';
|
||||
|
||||
export type ReturnType = {
|
||||
hangingProtocolStageIndexMap: Record<string, Types.HangingProtocol.HPInfo>;
|
||||
viewportGridStore: Record<string, unknown>;
|
||||
displaySetSelectorMap: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates a set of state information for hanging protocols and viewport grid
|
||||
* which defines the currently applied hanging protocol state.
|
||||
* @param state is the viewport grid state
|
||||
* @param syncService is the state sync service to use for getting existing state
|
||||
* @returns Set of states that can be applied to the state sync to remember
|
||||
* the current view state.
|
||||
*/
|
||||
const reuseCachedLayout = (
|
||||
state,
|
||||
hangingProtocolService: HangingProtocolService,
|
||||
syncService: StateSyncService
|
||||
): ReturnType => {
|
||||
const { activeViewportIndex, viewports, layout } = state;
|
||||
const hpInfo = hangingProtocolService.getState();
|
||||
const { protocolId, stageIndex, activeStudyUID } = hpInfo;
|
||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||
const stage = protocol.stages[stageIndex];
|
||||
const storeId = `${activeStudyUID}:${protocolId}:${stageIndex}`;
|
||||
const syncState = syncService.getState();
|
||||
const cacheId = `${activeStudyUID}:${protocolId}`;
|
||||
const viewportGridStore = { ...syncState.viewportGridStore };
|
||||
const hangingProtocolStageIndexMap = {
|
||||
...syncState.hangingProtocolStageIndexMap,
|
||||
};
|
||||
const displaySetSelectorMap = { ...syncState.displaySetSelectorMap };
|
||||
const { rows, columns } = stage.viewportStructure.properties;
|
||||
const custom =
|
||||
stage.viewports.length !== state.viewports.length ||
|
||||
state.layout.numRows !== rows ||
|
||||
state.layout.numCols !== columns;
|
||||
|
||||
hangingProtocolStageIndexMap[cacheId] = hpInfo;
|
||||
|
||||
if (storeId && custom) {
|
||||
viewportGridStore[storeId] = { ...state };
|
||||
}
|
||||
|
||||
for (let idx = 0; idx < state.viewports.length; idx++) {
|
||||
const viewport = state.viewports[idx];
|
||||
const { displaySetOptions, displaySetInstanceUIDs } = viewport;
|
||||
if (!displaySetOptions) continue;
|
||||
for (let i = 0; i < displaySetOptions.length; i++) {
|
||||
const displaySetUID = displaySetInstanceUIDs[i];
|
||||
if (!displaySetUID) continue;
|
||||
if (idx === activeViewportIndex && i === 0) {
|
||||
displaySetSelectorMap[
|
||||
`${activeStudyUID}:activeDisplaySet:0`
|
||||
] = displaySetUID;
|
||||
}
|
||||
if (displaySetOptions[i]?.id) {
|
||||
displaySetSelectorMap[
|
||||
`${activeStudyUID}: ${displaySetOptions[i].id}: ${displaySetOptions[i]
|
||||
.matchedDisplaySetsIndex || 0}`
|
||||
] = displaySetUID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hangingProtocolStageIndexMap,
|
||||
viewportGridStore,
|
||||
displaySetSelectorMap,
|
||||
};
|
||||
};
|
||||
|
||||
export default reuseCachedLayout;
|
||||
Reference in new issue
Block a user