From 8adf663248e9f0456a9373961b928ee069ee1d0e Mon Sep 17 00:00:00 2001 From: Alireza Date: Wed, 9 Aug 2023 08:08:23 -0400 Subject: [PATCH] fix(state synchronization): SEG and RT viewports should keep their before hydration voi (#3560) --- extensions/cornerstone-dicom-rt/src/index.tsx | 2 ++ .../src/utils/promptHydrateRT.ts | 5 +++ .../viewports/OHIFCornerstoneRTViewport.tsx | 17 +++++++++ .../cornerstone-dicom-seg/src/index.tsx | 3 +- .../src/utils/promptHydrateSEG.ts | 11 +++++- .../viewports/OHIFCornerstoneSEGViewport.tsx | 20 +++++++++-- .../src/Viewport/OHIFCornerstoneViewport.tsx | 34 +++--------------- extensions/cornerstone/src/commandsModule.ts | 35 +++++++++++++++++++ .../ViewportGridService/getPresentationIds.ts | 34 ++++++++++++++++-- .../docs/configuration/configurationFiles.md | 10 +++--- platform/docs/docs/migration-guide.md | 11 ++++++ .../components/ContextMenu/ContextMenu.tsx | 1 - 12 files changed, 140 insertions(+), 43 deletions(-) diff --git a/extensions/cornerstone-dicom-rt/src/index.tsx b/extensions/cornerstone-dicom-rt/src/index.tsx index 7d552977674..5cc8478f88e 100644 --- a/extensions/cornerstone-dicom-rt/src/index.tsx +++ b/extensions/cornerstone-dicom-rt/src/index.tsx @@ -36,12 +36,14 @@ const extension: Types.Extensions.Extension = { getViewportModule({ servicesManager, extensionManager, + commandsManager, }: Types.Extensions.ExtensionParams) { const ExtendedOHIFCornerstoneRTViewport = props => { return ( ); diff --git a/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts index 29e99bd044b..55c01107468 100644 --- a/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts +++ b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts @@ -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, diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx index 58d190f5a86..d203c676923 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -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, diff --git a/extensions/cornerstone-dicom-seg/src/index.tsx b/extensions/cornerstone-dicom-seg/src/index.tsx index 04ce5f98cb6..10a19c42dbf 100644 --- a/extensions/cornerstone-dicom-seg/src/index.tsx +++ b/extensions/cornerstone-dicom-seg/src/index.tsx @@ -69,6 +69,7 @@ const extension = { ); @@ -88,4 +89,4 @@ const extension = { getHangingProtocolModule, }; -export default extension; \ No newline at end of file +export default extension; diff --git a/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts index 7d6ee24523a..76b5ab53174 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts @@ -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, diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index 81813bd2c76..c5fb133de6d 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -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, diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index 3290279599f..abfde7f6372 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -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 (
diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index b87fbaf605e..cf1b219a32f 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -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 { diff --git a/platform/core/src/services/ViewportGridService/getPresentationIds.ts b/platform/core/src/services/ViewportGridService/getPresentationIds.ts index 4299175115a..decbb01167d 100644 --- a/platform/core/src/services/ViewportGridService/getPresentationIds.ts +++ b/platform/core/src/services/ViewportGridService/getPresentationIds.ts @@ -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); diff --git a/platform/docs/docs/configuration/configurationFiles.md b/platform/docs/docs/configuration/configurationFiles.md index 85e6344df4e..40a2d84d363 100644 --- a/platform/docs/docs/configuration/configurationFiles.md +++ b/platform/docs/docs/configuration/configurationFiles.md @@ -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.
Points to consider while using `dangerouslyUseDynamicConfig`:
- User have to enable this feature by setting `dangerouslyUseDynamicConfig.enabled:true`. By default it is `false`. diff --git a/platform/docs/docs/migration-guide.md b/platform/docs/docs/migration-guide.md index ae44d979086..af87a0296ee 100644 --- a/platform/docs/docs/migration-guide.md +++ b/platform/docs/docs/migration-guide.md @@ -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 diff --git a/platform/ui/src/components/ContextMenu/ContextMenu.tsx b/platform/ui/src/components/ContextMenu/ContextMenu.tsx index ff743200961..0c894956cc3 100644 --- a/platform/ui/src/components/ContextMenu/ContextMenu.tsx +++ b/platform/ui/src/components/ContextMenu/ContextMenu.tsx @@ -5,7 +5,6 @@ import Icon from '../Icon'; const ContextMenu = ({ items, ...props }) => { if (!items) { - console.warn('No items for context menu'); return null; } return (