Skip to content

Commit

Permalink
fix(state synchronization): SEG and RT viewports should keep their be…
Browse files Browse the repository at this point in the history
…fore hydration voi (#3560)
  • Loading branch information
sedghi authored Aug 9, 2023
1 parent 4f037ae commit 8adf663
Show file tree
Hide file tree
Showing 12 changed files with 140 additions and 43 deletions.
2 changes: 2 additions & 0 deletions extensions/cornerstone-dicom-rt/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
);
Expand Down
5 changes: 5 additions & 0 deletions extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function promptHydrateRT({
rtDisplaySet,
viewportIndex,
toolGroupId = 'default',
preHydrateCallbacks,
}) {
const { uiViewportDialogService } = servicesManager.services;

Expand All @@ -22,6 +23,10 @@ function promptHydrateRT({
);

if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});

const isHydrated = await hydrateRTDisplaySet({
rtDisplaySet,
viewportIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function OHIFCornerstoneRTViewport(props) {
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;

const {
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -155,6 +164,7 @@ function OHIFCornerstoneRTViewport(props) {
servicesManager,
viewportIndex,
rtDisplaySet,
preHydrateCallbacks: [storePresentationState],
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion extensions/cornerstone-dicom-seg/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const extension = {
<OHIFCornerstoneSEGViewport
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
{...props}
/>
);
Expand All @@ -88,4 +89,4 @@ const extension = {
getHangingProtocolModule,
};

export default extension;
export default extension;
11 changes: 10 additions & 1 deletion extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -17,6 +22,10 @@ function promptHydrateSEG({ servicesManager, segDisplaySet, viewportIndex }) {
);

if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
callback();
});

const isHydrated = await hydrateSEGDisplaySet({
segDisplaySet,
viewportIndex,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function OHIFCornerstoneSEGViewport(props) {
viewportLabel,
servicesManager,
extensionManager,
commandsManager,
} = props;

const { t } = useTranslation('SEGViewport');
Expand All @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -159,6 +167,7 @@ function OHIFCornerstoneSEGViewport(props) {
servicesManager,
viewportIndex,
segDisplaySet,
preHydrateCallbacks: [storePresentationState],
}).then(isHydrated => {
if (isHydrated) {
setIsHydrated(true);
Expand Down Expand Up @@ -248,8 +257,6 @@ function OHIFCornerstoneSEGViewport(props) {
toolGroupId
);

setToolGroupCreated(true);

return () => {
// remove the segmentation representations if seg displayset changed
segmentationService.removeSegmentationRepresentationFromToolGroup(
Expand Down Expand Up @@ -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,
Expand Down
34 changes: 4 additions & 30 deletions extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
viewportOptions,
displaySetOptions,
servicesManager,
commandsManager,
onElementEnabled,
onElementDisabled,
isJumpToMeasurementDisabled,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -256,7 +230,9 @@ const OHIFCornerstoneViewport = React.memo(props => {
setImageScrollBarHeight();

return () => {
storePresentation();
commandsManager.runCommand('storePresentation', {
viewportIndex,
});

cleanUpServices();

Expand Down Expand Up @@ -407,8 +383,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
};
}, [displaySets, elementRef, viewportIndex]);

console.debug('OHIFCornerstoneViewport rendering');

return (
<React.Fragment>
<div className="viewport-wrapper">
Expand Down
35 changes: 35 additions & 0 deletions extensions/cornerstone/src/commandsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ function commandsModule({
toolGroupService,
cineService,
toolbarService,
stateSyncService,
uiDialogService,
cornerstoneViewportService,
uiNotificationService,
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -740,6 +770,11 @@ function commandsModule({
toggleReferenceLines: {
commandFn: actions.toggleReferenceLines,
},
storePresentation: {
commandFn: actions.storePresentation,
storeContexts: [],
options: {},
},
};

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 6 additions & 4 deletions platform/docs/docs/configuration/configurationFiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`.
Expand Down
11 changes: 11 additions & 0 deletions platform/docs/docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand All @@ -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

Expand Down
Loading

0 comments on commit 8adf663

Please sign in to comment.