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:
Bill Wallace authored and GitHub committed 2023-03-15 12:41:41 -04:00
1 parent b7fff77e17
commit 803f638401
87 files changed
+3413 -1919

No files matched your search

+386
View File
@@ -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;