From ee4e8a4105aa7a1c6f2cf661c098ae649376b710 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 20 Mar 2023 15:11:21 -0400 Subject: [PATCH] fix: Hanging protocol state fixes (#3242) * fix: Some residual issues with hanging protocol after state sync fix: Some issues introduced by the state syncing changes * fix: PR fixes, mostly code cleanup * Improve a race condition in an automated test * Remove obsolete code * PR fixes --- .../src/Viewport/OHIFCornerstoneViewport.tsx | 47 +++++-- extensions/cornerstone/src/init.tsx | 12 +- .../SegmentationService.ts | 2 +- .../CornerstoneViewportService.ts | 65 ++++++---- .../src/services/ViewportService/Viewport.ts | 20 ++- .../src/tools/CalibrationLineTool.ts | 5 +- .../cornerstone/src/types/Presentation.ts | 30 ++--- extensions/default/src/commandsModule.ts | 4 +- .../default/src/utils/reuseCachedLayouts.ts | 2 +- extensions/test-extension/src/hp/hpMN.ts | 2 - .../StateSyncService/StateSyncService.ts | 6 + platform/core/src/types/HangingProtocol.ts | 4 +- .../services/data/StateSyncService.md | 15 ++- .../contextProviders/ViewportGridProvider.tsx | 13 +- .../src/contextProviders/getPresentationId.ts | 73 ----------- .../contextProviders/getPresentationIds.ts | 120 ++++++++++++++++++ platform/ui/src/types/index.ts | 3 +- .../OHIFStudyBrowser.spec.js | 2 + .../viewer/src/components/ViewportGrid.tsx | 14 +- 19 files changed, 268 insertions(+), 171 deletions(-) delete mode 100644 platform/ui/src/contextProviders/getPresentationId.ts create mode 100644 platform/ui/src/contextProviders/getPresentationIds.ts diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index 72c41eeae..411e17a10 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -220,15 +220,27 @@ const OHIFCornerstoneViewport = React.memo(props => { const currentPresentation = cornerstoneViewportService.getPresentation( viewportIndex ); - const { presentationSync } = stateSyncService.getState(); - if (currentPresentation) { - stateSyncService.store({ - presentationSync: { - ...presentationSync, - [currentPresentation.id]: currentPresentation, - }, - }); + 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(() => { @@ -384,18 +396,25 @@ const OHIFCornerstoneViewport = React.memo(props => { storePresentation(); - const { presentationSync } = stateSyncService.getState(); - const { presentationId } = viewportOptions; - const presentation = presentationId - ? (presentationSync[presentationId] as Presentation) - : null; + const { + lutPresentationStore, + positionPresentationStore, + } = stateSyncService.getState(); + const { presentationIds } = viewportOptions; + const presentations = { + positionPresentation: + positionPresentationStore[presentationIds?.positionPresentationId], + lutPresentation: + lutPresentationStore[presentationIds?.lutPresentationId], + }; + console.log('Using presentations', presentations); cornerstoneViewportService.setViewportData( viewportIndex, viewportData, viewportOptions, displaySetOptions, - presentation + presentations ); }; diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index b969787cf..858ac41f1 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -98,9 +98,15 @@ export default async function init({ _showCPURenderingModal(uiModalService, hangingProtocolService); } - // Stores a map from `presentationId` to a Presentation object so that - // an OHIFCornerstoneViewport can be redisplayed with the same attributes - stateSyncService.register('presentationSync', { clearOnModeExit: true }); + // Stores a map from `lutPresentationId` to a Presentation object so that + // an OHIFCornerstoneViewport can be redisplayed with the same LUT + stateSyncService.register('lutPresentationStore', { clearOnModeExit: true }); + + // Stores a map from `positionPresentationId` to a Presentation object so that + // an OHIFCornerstoneViewport can be redisplayed with the same position + stateSyncService.register('positionPresentationStore', { + clearOnModeExit: true, + }); const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap; diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 56ff73781..949d54c90 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -1591,7 +1591,7 @@ class SegmentationService { segmentInfo.isVisible = isVisible; - cstSegmentation.config.visibility.setVisibilityForSegmentIndex( + cstSegmentation.config.visibility.setSegmentVisibility( toolGroupId, segmentationRepresentationUID, segmentIndex, diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 37e0b2721..81623a738 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -21,11 +21,7 @@ import { StackViewportData, VolumeViewportData, } from '../../types/CornerstoneCacheService'; -import { - Presentation, - StackPresentation, - VolumePresentation, -} from '../../types/Presentation'; +import { Presentation, Presentations } from '../../types/Presentation'; import { setColormap, setLowerUpperColorTransferFunction, @@ -173,23 +169,30 @@ class CornerstoneViewportService extends PubSubService this.viewportsById.delete(viewportId); } + public setPresentations(viewport, presentations?: Presentations): void { + const properties = presentations.lutPresentation?.properties; + if (properties) viewport.setProperties(properties); + const camera = presentations.positionPresentation?.camera; + if (camera) viewport.setCamera(camera); + } + public getPresentation(viewportIndex: number): Presentation { const viewportInfo = this.viewportsInfo.get(viewportIndex); if (!viewportInfo) return; - const { - presentationId: id, - viewportType, - } = viewportInfo.getViewportOptions(); - if (!id) return; + const { viewportType, presentationIds } = viewportInfo.getViewportOptions(); const csViewport = this.getCornerstoneViewportByIndex(viewportIndex); if (!csViewport) return; const properties = csViewport.getProperties(); + if (properties.isComputedVOI) { + delete properties.voiRange; + delete properties.VOILUTFunction; + } const initialImageIndex = csViewport.getCurrentImageIdIndex(); const camera = csViewport.getCamera(); return { - id, + presentationIds, viewportType: !viewportType || viewportType === 'stack' ? 'stack' : 'volume', properties, @@ -211,7 +214,7 @@ class CornerstoneViewportService extends PubSubService viewportData: StackViewportData | VolumeViewportData, publicViewportOptions: PublicViewportOptions, publicDisplaySetOptions: DisplaySetOptions[], - presentation?: Presentation + presentations?: Presentations ): void { const renderingEngine = this.getRenderingEngine(); const viewportId = @@ -276,7 +279,7 @@ class CornerstoneViewportService extends PubSubService renderingEngine.enableElement(viewportInput); const viewport = renderingEngine.getViewport(viewportId); - this._setDisplaySets(viewport, viewportData, viewportInfo, presentation); + this._setDisplaySets(viewport, viewportData, viewportInfo, presentations); } public getCornerstoneViewport( @@ -340,7 +343,7 @@ class CornerstoneViewportService extends PubSubService viewport: Types.IStackViewport, viewportData: StackViewportData, viewportInfo: ViewportInfo, - presentation?: StackPresentation + presentations: Presentations ): void { const displaySetOptions = viewportInfo.getDisplaySetOptions(); @@ -353,7 +356,8 @@ class CornerstoneViewportService extends PubSubService this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]); let initialImageIndexToUse = - presentation?.initialImageIndex ?? initialImageIndex; + presentations?.positionPresentation?.initialImageIndex ?? + initialImageIndex; if ( initialImageIndexToUse === undefined || @@ -363,8 +367,8 @@ class CornerstoneViewportService extends PubSubService this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0; } - const properties = presentation?.properties || {}; - if (!presentation?.properties) { + const properties = { ...presentations.lutPresentation?.properties }; + if (!presentations.lutPresentation?.properties) { const { voi, voiInverted } = displaySetOptions[0]; if (voi && (voi.windowWidth || voi.windowCenter)) { const { lower, upper } = csUtils.windowLevel.toLowHighRange( @@ -385,7 +389,8 @@ class CornerstoneViewportService extends PubSubService // The scroll, however, works fine in CS3D viewport.scroll(initialImageIndexToUse); viewport.setProperties(properties); - if (presentation?.camera) viewport.setCamera(presentation.camera); + const camera = presentations.positionPresentation?.camera; + if (camera) viewport.setCamera(camera); }); } @@ -441,7 +446,7 @@ class CornerstoneViewportService extends PubSubService viewport: Types.IVolumeViewport, viewportData: VolumeViewportData, viewportInfo: ViewportInfo, - presentation: VolumePresentation + presentations: Presentations ): Promise { // TODO: We need to overhaul the way data sources work so requests can be made // async. I think we should follow the image loader pattern which is async and @@ -514,10 +519,18 @@ class CornerstoneViewportService extends PubSubService }); // This returns the async continuation only - return this.setVolumesForViewport(viewport, volumeInputArray, presentation); + return this.setVolumesForViewport( + viewport, + volumeInputArray, + presentations + ); } - public async setVolumesForViewport(viewport, volumeInputArray, presentation) { + public async setVolumesForViewport( + viewport, + volumeInputArray, + presentations + ) { const { displaySetService, segmentationService, @@ -525,9 +538,7 @@ class CornerstoneViewportService extends PubSubService } = this.servicesManager.services; await viewport.setVolumes(volumeInputArray); - const { properties, camera } = presentation || {}; - if (properties) viewport.setProperties(properties); - if (camera) viewport.setCamera(camera); + this.setPresentations(viewport, presentations); // load any secondary displaySets const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id); @@ -703,21 +714,21 @@ class CornerstoneViewportService extends PubSubService viewport: StackViewport | VolumeViewport, viewportData: StackViewportData | VolumeViewportData, viewportInfo: ViewportInfo, - presentation?: Presentation + presentations: Presentations = {} ): void { if (viewport instanceof StackViewport) { this._setStackViewport( viewport, viewportData as StackViewportData, viewportInfo, - presentation as StackPresentation + presentations ); } else if (viewport instanceof VolumeViewport) { this._setVolumeViewport( viewport, viewportData as VolumeViewportData, viewportInfo, - presentation as VolumePresentation + presentations ); } else { throw new Error('Unknown viewport type'); diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index 51b97bf91..aefbc07ce 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -1,4 +1,5 @@ import { Types, Enums } from '@cornerstonejs/core'; +import { Types as UITypes } from '@ohif/ui'; import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode'; import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation'; import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; @@ -15,12 +16,13 @@ export type InitialImageOptions = { }; export type ViewportOptions = { + id?: string; viewportType: Enums.ViewportType; toolGroupId: string; viewportId: string; // Presentation ID to store/load presentation state from - presentationId?: string; - orientation?: Types.Orientation; + presentationIds?: UITypes.PresentationIds; + orientation?: Enums.OrientationAxis; background?: Types.Point3; syncGroups?: SyncGroup[]; initialImageOptions?: InitialImageOptions; @@ -33,11 +35,12 @@ export type ViewportOptions = { }; export type PublicViewportOptions = { + id?: string; viewportType?: string; toolGroupId?: string; - presentationId?: string; + presentationIds?: UITypes.PresentationIds; viewportId?: string; - orientation?: string; + orientation?: Enums.OrientationAxis; background?: Types.Point3; syncGroups?: SyncGroup[]; initialImageOptions?: InitialImageOptions; @@ -51,6 +54,10 @@ export type DisplaySetSelector = { }; export type PublicDisplaySetOptions = { + /** The display set options can have an id in order to distinguish + * it from other similar items. + */ + id?: string; voi?: VOI; voiInverted?: boolean; blendMode?: string; @@ -59,6 +66,7 @@ export type PublicDisplaySetOptions = { }; export type DisplaySetOptions = { + id?: string; voi?: VOI; voiInverted: boolean; blendMode?: Enums.BlendModes; @@ -177,7 +185,7 @@ class ViewportInfo { let viewportType = viewportOptionsEntry.viewportType; const { toolGroupId = DEFAULT_TOOLGROUP_ID, - presentationId, + presentationIds, } = viewportOptionsEntry; let orientation; @@ -202,7 +210,7 @@ class ViewportInfo { viewportType: viewportType as Enums.ViewportType, orientation, toolGroupId, - presentationId, + presentationIds, }); } diff --git a/extensions/cornerstone/src/tools/CalibrationLineTool.ts b/extensions/cornerstone/src/tools/CalibrationLineTool.ts index 27e521d46..1db404034 100644 --- a/extensions/cornerstone/src/tools/CalibrationLineTool.ts +++ b/extensions/cornerstone/src/tools/CalibrationLineTool.ts @@ -1,9 +1,10 @@ import { metaData } from '@cornerstonejs/core'; -import { LengthTool } from '@cornerstonejs/tools'; -import { calibrateImageSpacing } from '@cornerstonejs/tools/dist/esm/utilities'; +import { LengthTool, utilities } from '@cornerstonejs/tools'; import callInputDialog from '../utils/callInputDialog'; import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement'; +const { calibrateImageSpacing } = utilities; + /** * Calibration Line tool works almost the same as the */ diff --git a/extensions/cornerstone/src/types/Presentation.ts b/extensions/cornerstone/src/types/Presentation.ts index 4520f3b11..82f13f745 100644 --- a/extensions/cornerstone/src/types/Presentation.ts +++ b/extensions/cornerstone/src/types/Presentation.ts @@ -1,23 +1,23 @@ /** Store presentation data for either stack viewports or volume viewports */ import { Types } from '@cornerstonejs/core'; +import { Types as UITypes } from '@ohif/ui'; -export interface BasePresentation { - id: string; - properties: Record; - initialImageIndex?: number; +/** + * Has information on the presentation of the viewport. + */ +export interface Presentation extends Types.StackViewportProperties { + presentationIds: UITypes.PresentationIds; + viewportType: string; + initialImageIndex: number; camera: Types.ICamera; + properties: Types.StackViewportProperties | Types.VolumeViewportProperties; + zoom?: number; + pan?: [number, number]; } -export interface StackPresentation extends BasePresentation { - viewportType: 'stack'; -} - -export interface VolumePresentation extends BasePresentation { - viewportType: 'volume'; -} - -// Currently it seems like the entire presentation state can be shared between -// Stack and Volume, but is setup to allow differences -export type Presentation = StackPresentation | VolumePresentation; +export type Presentations = { + positionPresentation?: Presentation; + lutPresentation?: Presentation; +}; export default Presentation; diff --git a/extensions/default/src/commandsModule.ts b/extensions/default/src/commandsModule.ts index 7d26048dc..5582dd5a8 100644 --- a/extensions/default/src/commandsModule.ts +++ b/extensions/default/src/commandsModule.ts @@ -153,8 +153,8 @@ const commandsModule = ({ servicesManager, commandsManager }) => { const restoreProtocol = !!viewportGridStore[storedHanging]; if ( - protocolId === hpInfo.hangingProtocolId && - useStageIdx === hpInfo.stageIdx && + protocolId === hpInfo.protocolId && + useStageIdx === hpInfo.stageIndex && !activeStudyUID ) { // Clear the HP setting to reset them diff --git a/extensions/default/src/utils/reuseCachedLayouts.ts b/extensions/default/src/utils/reuseCachedLayouts.ts index 09e1a9fef..38ecf8298 100644 --- a/extensions/default/src/utils/reuseCachedLayouts.ts +++ b/extensions/default/src/utils/reuseCachedLayouts.ts @@ -58,7 +58,7 @@ const reuseCachedLayout = ( } if (displaySetOptions[i]?.id) { displaySetSelectorMap[ - `${activeStudyUID}: ${displaySetOptions[i].id}: ${displaySetOptions[i] + `${activeStudyUID}:${displaySetOptions[i].id}:${displaySetOptions[i] .matchedDisplaySetsIndex || 0}` ] = displaySetUID; } diff --git a/extensions/test-extension/src/hp/hpMN.ts b/extensions/test-extension/src/hp/hpMN.ts index 70bc365bb..fc8fe4538 100644 --- a/extensions/test-extension/src/hp/hpMN.ts +++ b/extensions/test-extension/src/hp/hpMN.ts @@ -186,7 +186,6 @@ const hpMN: Types.HangingProtocol.Protocol = { }, }, viewportStructure: { - layoutType: 'grid', layoutType: 'grid', properties: { rows: 1, @@ -245,7 +244,6 @@ const hpMN: Types.HangingProtocol.Protocol = { }, displaySets: [ { - matchedDisplaySetsIndex: 1, id: 'defaultDisplaySetId', }, ], diff --git a/platform/core/src/services/StateSyncService/StateSyncService.ts b/platform/core/src/services/StateSyncService/StateSyncService.ts index b2d91c0ce..5a28bc041 100644 --- a/platform/core/src/services/StateSyncService/StateSyncService.ts +++ b/platform/core/src/services/StateSyncService/StateSyncService.ts @@ -42,6 +42,12 @@ export default class StateSyncService extends PubSubService { public init(extensionManager: ExtensionManager): void { } + /** Registers a new sync store called `id`. The state + * defines how the state is stored, and any default clearing of the + * state. + * A default store has the lifetime of the application. + * The other available store is cleared `onModeExit` + */ public register(id: string, config: StateConfig): void { this.registeredStateSets[id] = config; this.store({ [id]: {} }); diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts index e7e7eddee..5cb26e20c 100644 --- a/platform/core/src/types/HangingProtocol.ts +++ b/platform/core/src/types/HangingProtocol.ts @@ -141,8 +141,8 @@ export type initialImageOptions = { }; export type ViewportOptions = { - toolGroupId: string; - viewportType: string; + toolGroupId?: string; + viewportType?: string; id?: string; orientation?: string; viewportId?: string; diff --git a/platform/docs/docs/platform/services/data/StateSyncService.md b/platform/docs/docs/platform/services/data/StateSyncService.md index 98d9e82ad..4e6964d17 100644 --- a/platform/docs/docs/platform/services/data/StateSyncService.md +++ b/platform/docs/docs/platform/services/data/StateSyncService.md @@ -60,13 +60,18 @@ clears all states registered with `clearOnModeExit: true`. To avoid clearing the state, the mode definition should store any transient state in the mode onModeExit and recover it in the `mode.onModeEnter`. -## OHIF Registered State -There are a number of defined states here. It is recommended to update this -list as states are added: +## OHIF Registered State Sync Stores +There are a number of defined stores here. It is recommended to update this +list as state stores are added: + +### Default Extension Stores * `viewportGridStore` has viewport grid restore information for returning to an earlier grid layout. * `reuseIdMap` has a map of names to display sets for preserving user changes to hp display set selections. * `hanging` has a map of the hanging protocol stage information applied (HPInfo) -* `presentationSync` has the cornerstone presentation state information * `toggleHangingProtocol` has the previously applied hanging protocol, to toggle an HP off. -* `querySync` has the previously applied query information. Not fully implemented yet. + +### Cornerstone Extension Stores + +* `lutPresentationStore` has the cornerstone LUT (window level) presentation state information +* `positionPresentationStore` has the cornerstone viewport position (camera, initial image) information diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.tsx b/platform/ui/src/contextProviders/ViewportGridProvider.tsx index b11f1d301..c3bff44df 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.tsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.tsx @@ -8,7 +8,7 @@ import React, { import PropTypes from 'prop-types'; import isEqual from 'lodash.isequal'; import viewportLabels from '../utils/viewportLabels'; -import getPresentationId from './getPresentationId'; +import getPresentationIds from './getPresentationIds'; const DEFAULT_STATE = { activeViewportIndex: 0, @@ -69,7 +69,7 @@ const reuseViewport = (idSet, viewport, stateViewports) => { // }; } // Find a viewport instance number different from earlier viewports having - // the same presentationId as this one would - will be less than 10k + // the same presentationIds as this one would - will be less than 10k // viewports hopefully :-) for (let i = 0; i < 10000; i++) { const viewportId = 'viewport-' + i; @@ -121,7 +121,10 @@ export function ViewportGridProvider({ children, service }) { displaySetOptions, viewportLabel: viewportLabels[viewportIndex], }; - viewportOptions.presentationId = getPresentationId(newView, viewports); + viewportOptions.presentationIds = getPresentationIds( + newView, + viewports + ); // Make sure we assign a viewport id newView = reuseViewport({}, newView, state.viewports); @@ -207,8 +210,8 @@ export function ViewportGridProvider({ children, service }) { viewports[viewportIndex], state.viewports ); - if (!viewport.viewportOptions.presentationId) { - viewport.viewportOptions.presentationId = getPresentationId( + if (!viewport.viewportOptions.presentationIds) { + viewport.viewportOptions.presentationIds = getPresentationIds( viewport, viewports ); diff --git a/platform/ui/src/contextProviders/getPresentationId.ts b/platform/ui/src/contextProviders/getPresentationId.ts deleted file mode 100644 index 7b41eaf8a..000000000 --- a/platform/ui/src/contextProviders/getPresentationId.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Selects a presentation ID to use for this viewport. - * This is done to allow the same display set to be displayed more than once - * on screen, with different attributes such as window level and initial position. - * Then, when redisplaying that, the nearest/most common attribute is re-used. - * - * For example, for display set , in a viewport of type volume, - * the generated presentationID might be - * `volume:axial:`. This can then be used to store and retrieve - * presentation information in state sync service 'presentationSync' state. - * - * The generated value attempts to generate a unique value for every type - * of viewport which should have it's own presentation information. Thus, the - * following values are used for presentation ID: - * - * 1. viewportType - since the presentation information for a volume is different than for a stack - * 2. orientation - since the camera is different for different orientations - * 3. display set instance UID - since different display sets should get displayed differently - * 4. instance count - since displaying the same series twice should allow applying different window level etc - * - * @param viewport requiring a presentation Id - * @param viewports is the list of viewports being shown. Any presentation ID's - * among them must not be re-used in order to have each viewport have it's own presentation ID. - * @returns Presentation ID id, or undefined if nothing displayed - */ -const getPresentationId = (viewport, viewports): string => { - if (!viewport) return; - const { viewportOptions, displaySetInstanceUIDs } = viewport; - if (!viewportOptions || !displaySetInstanceUIDs?.length) { - console.log('No viewport type or display sets in', viewport); - return; - } - - const viewportType = viewportOptions.viewportType || 'stack'; - const idArr = [viewportType, 0, ...displaySetInstanceUIDs]; - if (viewportOptions.orientation) { - idArr.splice(2, 0, viewportOptions.orientation); - } - - // Allow setting a custom presentation prefix in the hanging protocol - // This allows defining new - // presentation groups to be set automatically when one knows that the - // same display set will be displayed in different ways. - // This is the recommended way to manage a hanging protocol which displays - // multiple views of a single display set, eg to display brain, bone, soft - // tissue views in different viewports. - if (viewportOptions.presentationPrefix) { - idArr.push(viewportOptions.presentationPrefix); - } - if (!viewports) { - console.log('viewports not defined', idArr.join(',')); - return idArr.join('&'); - } - - // This code finds the first unique index to add to the presentation id so that - // two viewports containing the same display set in the same type of viewport - // can have different presentation information. This allows comparison of - // a single display set in two or more viewports, when the user has simply - // 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. - for (let displayInstance = 0; displayInstance < 128; displayInstance++) { - idArr[1] = displayInstance; - const testId = idArr.join('&'); - if (!viewports.find(it => it.viewportOptions?.presentationId === testId)) { - break; - } - } - const id = idArr.join('&'); - return id; -}; - -export default getPresentationId; diff --git a/platform/ui/src/contextProviders/getPresentationIds.ts b/platform/ui/src/contextProviders/getPresentationIds.ts new file mode 100644 index 000000000..e5a41d7c9 --- /dev/null +++ b/platform/ui/src/contextProviders/getPresentationIds.ts @@ -0,0 +1,120 @@ +const JOIN_STR = '&'; + +// The default lut presentation id if none defined +const DEFAULT = 'default'; + +// This code finds the first unique index to add to the presentation id so that +// two viewports containing the same display set in the same type of viewport +// can have different presentation information. This allows comparison of +// a single display set in two or more viewports, when the user has simply +// 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) => { + arr.push(0); + // 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. + for (let displayInstance = 0; displayInstance < 128; displayInstance++) { + arr[arr.length - 1] = displayInstance; + const testId = arr.join(JOIN_STR); + if ( + !viewports.find( + viewport => viewport.viewportOptions?.presentationIds?.[key] === testId + ) + ) { + break; + } + } +}; + +const getLutId = (ds): string => { + if (!ds || !ds.options) return DEFAULT; + if (ds.options.id) return ds.options.id; + const arr = Object.entries(ds.options).map(([key, val]) => `${key}=${value}`); + if (!arr.length) return DEFAULT; + return arr.join(JOIN_STR); +}; + +export type PresentationIds = { + positionPresentationId?: string; + lutPresentationId?: string; +}; + +/** + * Gets a set of presentation IDs for a viewport. The presentation IDs are + * used to remember the presentation state of the viewport when it is navigated + * to different layouts. + * + * The design of this is setup to allow preserving the view information in the + * following cases: + * + * + * * If a set of display sets was previously displayed in the same initial + * position as it is currently being asked to be displayed, + * then remember the camera position as previously displayed + * + * * If a set of display sets was previously displayed with the same initial + * LUT conditions, then remember the last LUT displayed for that display set + * and re-apply it. + * + * * Otherwise, apply the initial hanging protocol specified LUT and camera + * position to new display sets. + * + * This means generating two presentationId keys: + * + * `positionPresentationId` + * + * Used for getting the camera/initial position state sync values. + * This is a combination of: + * * `viewportOptions.id` + * * `viewportOptions.orientation` + * * display set UID's - as displayed for this viewport, excluding seg + * * a unique index number if the previous key is already displayed + * + * `lutPresentationId` + * + * Used for getting the voi LUT information. Generated from: + * + * * `displaySetOption[0].options` - including the id if present + * * displaySetUID's + * * a unique index number if the previously generated key is already + * displayed. + * + * @param viewport requiring a presentation Id + * @param viewports is the list of viewports being shown. Any presentation ID's + * among them must not be re-used in order to have each viewport have it's own presentation ID. + * @returns PresentationIds + */ +const getPresentationIds = (viewport, viewports): PresentationIds => { + if (!viewport) return; + const { + viewportOptions, + displaySetInstanceUIDs, + displaySetOptions, + } = viewport; + if (!viewportOptions || !displaySetInstanceUIDs?.length) { + return; + } + + const { id, orientation } = viewportOptions; + const lutId = getLutId(displaySetOptions[0]); + const lutPresentationArr = [lutId]; + + const positionPresentationArr = [orientation || 'acquisition']; + if (id) positionPresentationArr.push(id); + + for (const uid of displaySetInstanceUIDs) { + positionPresentationArr.push(uid); + lutPresentationArr.push(uid); + } + + addUniqueIndex(positionPresentationArr, 'positionPresentationId', viewports); + addUniqueIndex(lutPresentationArr, 'lutPresentationId', viewports); + + const lutPresentationId = lutPresentationArr.join(JOIN_STR); + const positionPresentationId = positionPresentationArr.join(JOIN_STR); + return { lutPresentationId, positionPresentationId }; +}; + +export default getPresentationIds; diff --git a/platform/ui/src/types/index.ts b/platform/ui/src/types/index.ts index 6fd59a088..f74bbecb9 100644 --- a/platform/ui/src/types/index.ts +++ b/platform/ui/src/types/index.ts @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import ThumbnailType from './ThumbnailType'; +import { PresentationIds } from '../contextProviders/getPresentationIds'; // A few miscellaneous types declared inline here. @@ -14,4 +15,4 @@ const StringNumber = PropTypes.oneOfType([PropTypes.string, PropTypes.number]); */ const StringArray = PropTypes.oneOfType([PropTypes.string, PropTypes.array]); -export { StringNumber, StringArray, ThumbnailType }; +export { StringNumber, StringArray, ThumbnailType, PresentationIds }; diff --git a/platform/viewer/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js b/platform/viewer/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js index 8264c9635..c7f1dadba 100644 --- a/platform/viewer/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js +++ b/platform/viewer/cypress/integration/measurement-tracking/OHIFStudyBrowser.spec.js @@ -51,6 +51,8 @@ describe('OHIF Study Viewer Page', function() { }); it('performs double-click to load thumbnail in active viewport', () => { + // Have to finish rendering the image before this works + cy.wait(250); cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick(); //cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText); diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx index 70a9ed018..2eea5be09 100644 --- a/platform/viewer/src/components/ViewportGrid.tsx +++ b/platform/viewer/src/components/ViewportGrid.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; -import { ServicesManager } from '@ohif/core'; +import { ServicesManager, Types } from '@ohif/core'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import { utils } from '@ohif/core'; import EmptyViewport from './EmptyViewport'; @@ -23,15 +23,6 @@ const ORIENTATION_MAP = { }, }; -const createHpInfo = (protocol, stage, activeStudyUID) => { - return { - hangingProtocolId: protocol.id, - stageId: stage.stageId, - stageIdx: protocol.stages.findIndex(it => it === stage), - activeStudyUID, - }; -}; - const compareViewportOptions = (opts1, opts2) => { if ((opts1.viewportType || 'stack') != opts2.viewportType) { return false; @@ -62,7 +53,7 @@ function ViewerViewportGrid(props) { */ const updateDisplaySetsFromProtocol = ( - protocol, + protocol: Types.HangingProtocol.Protocol, stage, activeStudyUID, viewportMatchDetails @@ -119,7 +110,6 @@ function ViewerViewportGrid(props) { numCols, layoutType, layoutOptions, - hpInfo: createHpInfo(protocol, stage, activeStudyUID), findOrCreateViewport, }); };