fix(state synchronization): SEG and RT viewports should keep their before hydration voi (#3560)

This commit is contained in:
Alireza 2023-08-09 08:08:23 -04:00 committed by GitHub
parent 4f037ae55e
commit 8adf663248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 140 additions and 43 deletions

View File

@ -36,12 +36,14 @@ const extension: Types.Extensions.Extension = {
getViewportModule({
servicesManager,
extensionManager,
commandsManager,
}: Types.Extensions.ExtensionParams) {
const ExtendedOHIFCornerstoneRTViewport = props => {
return (
<OHIFCornerstoneRTViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
{...props}
/>
);

View File

@ -12,6 +12,7 @@ function promptHydrateRT({
rtDisplaySet,
viewportIndex,
toolGroupId = 'default',
preHydrateCallbacks,
}) {
const { uiViewportDialogService } = servicesManager.services;
@ -22,6 +23,10 @@ function promptHydrateRT({
);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,

View File

@ -25,6 +25,7 @@ function OHIFCornerstoneRTViewport(props) {
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;
const {
@ -91,6 +92,14 @@ function OHIFCornerstoneRTViewport(props) {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportIndex }) => {
commandsManager.runCommand('storePresentation', {
viewportIndex,
});
});
}, [viewportGrid]);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
@ -155,6 +164,7 @@ function OHIFCornerstoneRTViewport(props) {
servicesManager,
viewportIndex,
rtDisplaySet,
preHydrateCallbacks: [storePresentationState],
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
@ -303,6 +313,13 @@ function OHIFCornerstoneRTViewport(props) {
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
// Before hydrating a RT and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the RT. This is
// required if the user has changed the viewport (other viewport than RT viewport)
// presentation state (w/l and invert) and then opens the RT. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await _hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,

View File

@ -69,6 +69,7 @@ const extension = {
<OHIFCornerstoneSEGViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
{...props}
/>
);
@ -88,4 +89,4 @@ const extension = {
getHangingProtocolModule,
};
export default extension;
export default extension;

View File

@ -7,7 +7,12 @@ const RESPONSE = {
HYDRATE_SEG: 5,
};
function promptHydrateSEG({ servicesManager, segDisplaySet, viewportIndex }) {
function promptHydrateSEG({
servicesManager,
segDisplaySet,
viewportIndex,
preHydrateCallbacks,
}) {
const { uiViewportDialogService } = servicesManager.services;
return new Promise(async function(resolve, reject) {
@ -17,6 +22,10 @@ function promptHydrateSEG({ servicesManager, segDisplaySet, viewportIndex }) {
);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,

View File

@ -24,6 +24,7 @@ function OHIFCornerstoneSEGViewport(props) {
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;
const { t } = useTranslation('SEGViewport');
@ -48,7 +49,6 @@ function OHIFCornerstoneSEGViewport(props) {
const [viewportGrid, viewportGridService] = useViewportGrid();
// States
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
const [selectedSegment, setSelectedSegment] = useState(1);
// Hydration means that the SEG is opened and segments are loaded into the
@ -93,6 +93,14 @@ function OHIFCornerstoneSEGViewport(props) {
setElement(null);
};
const storePresentationState = useCallback(() => {
viewportGrid?.viewports.forEach(({ viewportIndex }) => {
commandsManager.runCommand('storePresentation', {
viewportIndex,
});
});
}, [viewportGrid]);
const getCornerstoneViewport = useCallback(() => {
const { component: Component } = extensionManager.getModuleEntry(
'@ohif/extension-cornerstone.viewportModule.cornerstone'
@ -159,6 +167,7 @@ function OHIFCornerstoneSEGViewport(props) {
servicesManager,
viewportIndex,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
@ -248,8 +257,6 @@ function OHIFCornerstoneSEGViewport(props) {
toolGroupId
);
setToolGroupCreated(true);
return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
@ -309,6 +316,13 @@ function OHIFCornerstoneSEGViewport(props) {
} = referencedDisplaySetRef.current.metadata;
const onStatusClick = async () => {
// Before hydrating a SEG and make it added to all viewports in the grid
// that share the same frameOfReferenceUID, we need to store the viewport grid
// presentation state, so that we can restore it after hydrating the SEG. This is
// required if the user has changed the viewport (other viewport than SEG viewport)
// presentation state (w/l and invert) and then opens the SEG. If we don't store
// the presentation state, the viewport will be reset to the default presentation
storePresentationState();
const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,

View File

@ -112,6 +112,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
onElementDisabled,
isJumpToMeasurementDisabled,
@ -152,33 +153,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
}
}, [elementRef]);
const storePresentation = () => {
const currentPresentation = cornerstoneViewportService.getPresentation(
viewportIndex
);
if (!currentPresentation || !currentPresentation.presentationIds) return;
const {
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { presentationIds } = currentPresentation;
const { lutPresentationId, positionPresentationId } = presentationIds || {};
const storeState = {};
if (lutPresentationId) {
storeState.lutPresentationStore = {
...lutPresentationStore,
[lutPresentationId]: currentPresentation,
};
}
if (positionPresentationId) {
storeState.positionPresentationStore = {
...positionPresentationStore,
[positionPresentationId]: currentPresentation,
};
}
stateSyncService.store(storeState);
};
const cleanUpServices = useCallback(() => {
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
@ -256,7 +230,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
setImageScrollBarHeight();
return () => {
storePresentation();
commandsManager.runCommand('storePresentation', {
viewportIndex,
});
cleanUpServices();
@ -407,8 +383,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
};
}, [displaySets, elementRef, viewportIndex]);
console.debug('OHIFCornerstoneViewport rendering');
return (
<React.Fragment>
<div className="viewport-wrapper">

View File

@ -28,6 +28,7 @@ function commandsModule({
toolGroupService,
cineService,
toolbarService,
stateSyncService,
uiDialogService,
cornerstoneViewportService,
uiNotificationService,
@ -616,6 +617,35 @@ function commandsModule({
);
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
},
storePresentation: ({ viewportIndex }) => {
const presentation = cornerstoneViewportService.getPresentation(
viewportIndex
);
if (!presentation || !presentation.presentationIds) {
return;
}
const {
lutPresentationStore,
positionPresentationStore,
} = stateSyncService.getState();
const { presentationIds } = presentation;
const { lutPresentationId, positionPresentationId } =
presentationIds || {};
const storeState = {};
if (lutPresentationId) {
storeState.lutPresentationStore = {
...lutPresentationStore,
[lutPresentationId]: presentation,
};
}
if (positionPresentationId) {
storeState.positionPresentationStore = {
...positionPresentationStore,
[positionPresentationId]: presentation,
};
}
stateSyncService.store(storeState);
},
};
const definitions = {
@ -740,6 +770,11 @@ function commandsModule({
toggleReferenceLines: {
commandFn: actions.toggleReferenceLines,
},
storePresentation: {
commandFn: actions.storePresentation,
storeContexts: [],
options: {},
},
};
return {

View File

@ -10,8 +10,14 @@ const DEFAULT = 'default';
// dragged and dropped the view in twice. For example, it allows displaying
// bone, brain and soft tissue views of a single display set, and to still
// remember the specific changes to each viewport.
const addUniqueIndex = (arr, key, viewports) => {
const addUniqueIndex = (arr, key, viewports, isUpdatingSameViewport) => {
arr.push(0);
// If we are updating the viewport, we should not increment the index
if (isUpdatingSameViewport) {
return;
}
// The 128 is just a value that is larger than how many viewports we
// display at once, used as an upper bound on how many unique presentation
// ID's might exist for a single display set at once.
@ -109,8 +115,30 @@ const getPresentationIds = (viewport, viewports): PresentationIds => {
lutPresentationArr.push(uid);
}
addUniqueIndex(positionPresentationArr, 'positionPresentationId', viewports);
addUniqueIndex(lutPresentationArr, 'lutPresentationId', viewports);
// only add unique index if the viewport is getting inserted and not updated
const isUpdatingSameViewport = viewports.some(v => {
return (
v.displaySetInstanceUIDs.toString() ===
viewport.displaySetInstanceUIDs.toString() &&
v.viewportIndex === viewport.viewportIndex
);
});
// if it is updating the viewport we should not increment the index since
// it might be a layer on the fusion or a SEG layer that is added on
// top of the original display set
addUniqueIndex(
positionPresentationArr,
'positionPresentationId',
viewports,
isUpdatingSameViewport
);
addUniqueIndex(
lutPresentationArr,
'lutPresentationId',
viewports,
isUpdatingSameViewport
);
const lutPresentationId = lutPresentationArr.join(JOIN_STR);
const positionPresentationId = positionPresentationArr.join(JOIN_STR);

View File

@ -10,10 +10,11 @@ After following the steps outlined in
OHIF Viewer has data for several studies and their images. You didn't add this
data, so where is it coming from?
By default, the viewer is configured to connect to a remote server hosted by the
nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started,
the time may come when you want to develop using your own data either locally or
remotely.
By default, the viewer is configured to connect to a Amazon S3 bucket that is hosting
a Static WADO server (see [Static WADO DICOMWeb](https://github.com/RadicalImaging/static-dicomweb)).
By default we use `default.js` for the configuration file. You can change this by setting the `APP_CONFIG` environment variable
and select other options such as `config/local_orthanc.js` or `config/google.js`.
## Configuration Files
@ -172,6 +173,7 @@ if auth headers are used, a preflight request is required.
}
```
- `showLoadingIndicator`: (default to true), if set to false, the loading indicator will not be shown when navigating between studies.
- `supportsWildcard`: (default to false), if set to true, the datasource will support wildcard matching for patient name and patient id.
- `dangerouslyUseDynamicConfig`: Dynamic config allows user to pass `configUrl` query string. This allows to load config without recompiling application. If the `configUrl` query string is passed, the worklist and modes will load from the referenced json rather than the default .env config. If there is no `configUrl` path provided, the default behaviour is used and there should not be any deviation from current user experience.<br/>
Points to consider while using `dangerouslyUseDynamicConfig`:<br/>
- User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`.

View File

@ -89,6 +89,16 @@ Since the platform/viewer (@ohif/viewer) is already at v4.12.51, we opted to ren
## Configuration
:::tip
There are various configurations available to customize the viewer. Each configuration is represented by a custom-tailored object that should be used with the viewer to work effectively with a specific server. Here are some examples of configuration files found in the platform/app/public/config directory. Some server-specific configurations that you should be aware are: `supportsWildcard`, `bulkDataURI`, `omitQuotationForMultipartRequest`, `staticWado` (Read more about them [here](./configuration/configurationFiles.md)).
- default.js: This is our default configuration designed for our main server, which uses a Static WADO datasource hosted on Amazon S3.
- local_orthanc.js: Use this configuration when working with our local Orthanc server.
- local_dcm4chee.js: This configuration is intended for our local dcm4chee server.
- netlify.js: This configuration is the same as default.js and is used for deployment on Netlify.
- google.js: Use this configuration to run the viewer against the Google Health API.
:::
OHIF v3 has a new configuration structure. The main difference is that the `servers` is renamed to `dataSources` and the configuration is now asynchronous. Datasources are more abstract and
far more capable than servers. Read more about dataSources [here](./platform/extensions/modules/data-source.md).
@ -98,6 +108,7 @@ far more capable than servers. Read more about dataSources [here](./platform/ext
- The maxConcurrentMetadataRequests property has been removed in favor of `maxNumRequests`
- The hotkeys array has been updated with different command names and options, and some keys have been removed.
- New properties have been added, including `maxNumberOfWebWorkers`, `omitQuotationForMultipartRequest`, `showWarningMessageForCrossOrigin`, `showCPUFallbackMessage`, `showLoadingIndicator`, `strictZSpacingForVolumeViewport`.
- you should see if `supportsWildcard` is supported in your server, some servers don't support it and you need to make it false.
## Modes

View File

@ -5,7 +5,6 @@ import Icon from '../Icon';
const ContextMenu = ({ items, ...props }) => {
if (!items) {
console.warn('No items for context menu');
return null;
}
return (