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
This commit is contained in:
Bill Wallace 2023-03-20 15:11:21 -04:00 committed by GitHub
parent 544bf55a4f
commit ee4e8a4105
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 268 additions and 171 deletions

View File

@ -220,15 +220,27 @@ const OHIFCornerstoneViewport = React.memo(props => {
const currentPresentation = cornerstoneViewportService.getPresentation( const currentPresentation = cornerstoneViewportService.getPresentation(
viewportIndex viewportIndex
); );
const { presentationSync } = stateSyncService.getState(); if (!currentPresentation || !currentPresentation.presentationIds) return;
if (currentPresentation) { const {
stateSyncService.store({ lutPresentationStore,
presentationSync: { positionPresentationStore,
...presentationSync, } = stateSyncService.getState();
[currentPresentation.id]: currentPresentation, 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 cleanUpServices = useCallback(() => {
@ -384,18 +396,25 @@ const OHIFCornerstoneViewport = React.memo(props => {
storePresentation(); storePresentation();
const { presentationSync } = stateSyncService.getState(); const {
const { presentationId } = viewportOptions; lutPresentationStore,
const presentation = presentationId positionPresentationStore,
? (presentationSync[presentationId] as Presentation) } = stateSyncService.getState();
: null; const { presentationIds } = viewportOptions;
const presentations = {
positionPresentation:
positionPresentationStore[presentationIds?.positionPresentationId],
lutPresentation:
lutPresentationStore[presentationIds?.lutPresentationId],
};
console.log('Using presentations', presentations);
cornerstoneViewportService.setViewportData( cornerstoneViewportService.setViewportData(
viewportIndex, viewportIndex,
viewportData, viewportData,
viewportOptions, viewportOptions,
displaySetOptions, displaySetOptions,
presentation presentations
); );
}; };

View File

@ -98,9 +98,15 @@ export default async function init({
_showCPURenderingModal(uiModalService, hangingProtocolService); _showCPURenderingModal(uiModalService, hangingProtocolService);
} }
// Stores a map from `presentationId` to a Presentation object so that // Stores a map from `lutPresentationId` to a Presentation object so that
// an OHIFCornerstoneViewport can be redisplayed with the same attributes // an OHIFCornerstoneViewport can be redisplayed with the same LUT
stateSyncService.register('presentationSync', { clearOnModeExit: true }); 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 = const labelmapRepresentation =
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap; cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;

View File

@ -1591,7 +1591,7 @@ class SegmentationService {
segmentInfo.isVisible = isVisible; segmentInfo.isVisible = isVisible;
cstSegmentation.config.visibility.setVisibilityForSegmentIndex( cstSegmentation.config.visibility.setSegmentVisibility(
toolGroupId, toolGroupId,
segmentationRepresentationUID, segmentationRepresentationUID,
segmentIndex, segmentIndex,

View File

@ -21,11 +21,7 @@ import {
StackViewportData, StackViewportData,
VolumeViewportData, VolumeViewportData,
} from '../../types/CornerstoneCacheService'; } from '../../types/CornerstoneCacheService';
import { import { Presentation, Presentations } from '../../types/Presentation';
Presentation,
StackPresentation,
VolumePresentation,
} from '../../types/Presentation';
import { import {
setColormap, setColormap,
setLowerUpperColorTransferFunction, setLowerUpperColorTransferFunction,
@ -173,23 +169,30 @@ class CornerstoneViewportService extends PubSubService
this.viewportsById.delete(viewportId); 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 { public getPresentation(viewportIndex: number): Presentation {
const viewportInfo = this.viewportsInfo.get(viewportIndex); const viewportInfo = this.viewportsInfo.get(viewportIndex);
if (!viewportInfo) return; if (!viewportInfo) return;
const { const { viewportType, presentationIds } = viewportInfo.getViewportOptions();
presentationId: id,
viewportType,
} = viewportInfo.getViewportOptions();
if (!id) return;
const csViewport = this.getCornerstoneViewportByIndex(viewportIndex); const csViewport = this.getCornerstoneViewportByIndex(viewportIndex);
if (!csViewport) return; if (!csViewport) return;
const properties = csViewport.getProperties(); const properties = csViewport.getProperties();
if (properties.isComputedVOI) {
delete properties.voiRange;
delete properties.VOILUTFunction;
}
const initialImageIndex = csViewport.getCurrentImageIdIndex(); const initialImageIndex = csViewport.getCurrentImageIdIndex();
const camera = csViewport.getCamera(); const camera = csViewport.getCamera();
return { return {
id, presentationIds,
viewportType: viewportType:
!viewportType || viewportType === 'stack' ? 'stack' : 'volume', !viewportType || viewportType === 'stack' ? 'stack' : 'volume',
properties, properties,
@ -211,7 +214,7 @@ class CornerstoneViewportService extends PubSubService
viewportData: StackViewportData | VolumeViewportData, viewportData: StackViewportData | VolumeViewportData,
publicViewportOptions: PublicViewportOptions, publicViewportOptions: PublicViewportOptions,
publicDisplaySetOptions: DisplaySetOptions[], publicDisplaySetOptions: DisplaySetOptions[],
presentation?: Presentation presentations?: Presentations
): void { ): void {
const renderingEngine = this.getRenderingEngine(); const renderingEngine = this.getRenderingEngine();
const viewportId = const viewportId =
@ -276,7 +279,7 @@ class CornerstoneViewportService extends PubSubService
renderingEngine.enableElement(viewportInput); renderingEngine.enableElement(viewportInput);
const viewport = renderingEngine.getViewport(viewportId); const viewport = renderingEngine.getViewport(viewportId);
this._setDisplaySets(viewport, viewportData, viewportInfo, presentation); this._setDisplaySets(viewport, viewportData, viewportInfo, presentations);
} }
public getCornerstoneViewport( public getCornerstoneViewport(
@ -340,7 +343,7 @@ class CornerstoneViewportService extends PubSubService
viewport: Types.IStackViewport, viewport: Types.IStackViewport,
viewportData: StackViewportData, viewportData: StackViewportData,
viewportInfo: ViewportInfo, viewportInfo: ViewportInfo,
presentation?: StackPresentation presentations: Presentations
): void { ): void {
const displaySetOptions = viewportInfo.getDisplaySetOptions(); const displaySetOptions = viewportInfo.getDisplaySetOptions();
@ -353,7 +356,8 @@ class CornerstoneViewportService extends PubSubService
this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]); this.viewportsDisplaySets.set(viewport.id, [displaySetInstanceUID]);
let initialImageIndexToUse = let initialImageIndexToUse =
presentation?.initialImageIndex ?? initialImageIndex; presentations?.positionPresentation?.initialImageIndex ??
initialImageIndex;
if ( if (
initialImageIndexToUse === undefined || initialImageIndexToUse === undefined ||
@ -363,8 +367,8 @@ class CornerstoneViewportService extends PubSubService
this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0; this._getInitialImageIndexForStackViewport(viewportInfo, imageIds) || 0;
} }
const properties = presentation?.properties || {}; const properties = { ...presentations.lutPresentation?.properties };
if (!presentation?.properties) { if (!presentations.lutPresentation?.properties) {
const { voi, voiInverted } = displaySetOptions[0]; const { voi, voiInverted } = displaySetOptions[0];
if (voi && (voi.windowWidth || voi.windowCenter)) { if (voi && (voi.windowWidth || voi.windowCenter)) {
const { lower, upper } = csUtils.windowLevel.toLowHighRange( const { lower, upper } = csUtils.windowLevel.toLowHighRange(
@ -385,7 +389,8 @@ class CornerstoneViewportService extends PubSubService
// The scroll, however, works fine in CS3D // The scroll, however, works fine in CS3D
viewport.scroll(initialImageIndexToUse); viewport.scroll(initialImageIndexToUse);
viewport.setProperties(properties); 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, viewport: Types.IVolumeViewport,
viewportData: VolumeViewportData, viewportData: VolumeViewportData,
viewportInfo: ViewportInfo, viewportInfo: ViewportInfo,
presentation: VolumePresentation presentations: Presentations
): Promise<void> { ): Promise<void> {
// TODO: We need to overhaul the way data sources work so requests can be made // 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 // 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 // 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 { const {
displaySetService, displaySetService,
segmentationService, segmentationService,
@ -525,9 +538,7 @@ class CornerstoneViewportService extends PubSubService
} = this.servicesManager.services; } = this.servicesManager.services;
await viewport.setVolumes(volumeInputArray); await viewport.setVolumes(volumeInputArray);
const { properties, camera } = presentation || {}; this.setPresentations(viewport, presentations);
if (properties) viewport.setProperties(properties);
if (camera) viewport.setCamera(camera);
// load any secondary displaySets // load any secondary displaySets
const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id); const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id);
@ -703,21 +714,21 @@ class CornerstoneViewportService extends PubSubService
viewport: StackViewport | VolumeViewport, viewport: StackViewport | VolumeViewport,
viewportData: StackViewportData | VolumeViewportData, viewportData: StackViewportData | VolumeViewportData,
viewportInfo: ViewportInfo, viewportInfo: ViewportInfo,
presentation?: Presentation presentations: Presentations = {}
): void { ): void {
if (viewport instanceof StackViewport) { if (viewport instanceof StackViewport) {
this._setStackViewport( this._setStackViewport(
viewport, viewport,
viewportData as StackViewportData, viewportData as StackViewportData,
viewportInfo, viewportInfo,
presentation as StackPresentation presentations
); );
} else if (viewport instanceof VolumeViewport) { } else if (viewport instanceof VolumeViewport) {
this._setVolumeViewport( this._setVolumeViewport(
viewport, viewport,
viewportData as VolumeViewportData, viewportData as VolumeViewportData,
viewportInfo, viewportInfo,
presentation as VolumePresentation presentations
); );
} else { } else {
throw new Error('Unknown viewport type'); throw new Error('Unknown viewport type');

View File

@ -1,4 +1,5 @@
import { Types, Enums } from '@cornerstonejs/core'; import { Types, Enums } from '@cornerstonejs/core';
import { Types as UITypes } from '@ohif/ui';
import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode'; import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode';
import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation'; import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation';
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
@ -15,12 +16,13 @@ export type InitialImageOptions = {
}; };
export type ViewportOptions = { export type ViewportOptions = {
id?: string;
viewportType: Enums.ViewportType; viewportType: Enums.ViewportType;
toolGroupId: string; toolGroupId: string;
viewportId: string; viewportId: string;
// Presentation ID to store/load presentation state from // Presentation ID to store/load presentation state from
presentationId?: string; presentationIds?: UITypes.PresentationIds;
orientation?: Types.Orientation; orientation?: Enums.OrientationAxis;
background?: Types.Point3; background?: Types.Point3;
syncGroups?: SyncGroup[]; syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions; initialImageOptions?: InitialImageOptions;
@ -33,11 +35,12 @@ export type ViewportOptions = {
}; };
export type PublicViewportOptions = { export type PublicViewportOptions = {
id?: string;
viewportType?: string; viewportType?: string;
toolGroupId?: string; toolGroupId?: string;
presentationId?: string; presentationIds?: UITypes.PresentationIds;
viewportId?: string; viewportId?: string;
orientation?: string; orientation?: Enums.OrientationAxis;
background?: Types.Point3; background?: Types.Point3;
syncGroups?: SyncGroup[]; syncGroups?: SyncGroup[];
initialImageOptions?: InitialImageOptions; initialImageOptions?: InitialImageOptions;
@ -51,6 +54,10 @@ export type DisplaySetSelector = {
}; };
export type PublicDisplaySetOptions = { export type PublicDisplaySetOptions = {
/** The display set options can have an id in order to distinguish
* it from other similar items.
*/
id?: string;
voi?: VOI; voi?: VOI;
voiInverted?: boolean; voiInverted?: boolean;
blendMode?: string; blendMode?: string;
@ -59,6 +66,7 @@ export type PublicDisplaySetOptions = {
}; };
export type DisplaySetOptions = { export type DisplaySetOptions = {
id?: string;
voi?: VOI; voi?: VOI;
voiInverted: boolean; voiInverted: boolean;
blendMode?: Enums.BlendModes; blendMode?: Enums.BlendModes;
@ -177,7 +185,7 @@ class ViewportInfo {
let viewportType = viewportOptionsEntry.viewportType; let viewportType = viewportOptionsEntry.viewportType;
const { const {
toolGroupId = DEFAULT_TOOLGROUP_ID, toolGroupId = DEFAULT_TOOLGROUP_ID,
presentationId, presentationIds,
} = viewportOptionsEntry; } = viewportOptionsEntry;
let orientation; let orientation;
@ -202,7 +210,7 @@ class ViewportInfo {
viewportType: viewportType as Enums.ViewportType, viewportType: viewportType as Enums.ViewportType,
orientation, orientation,
toolGroupId, toolGroupId,
presentationId, presentationIds,
}); });
} }

View File

@ -1,9 +1,10 @@
import { metaData } from '@cornerstonejs/core'; import { metaData } from '@cornerstonejs/core';
import { LengthTool } from '@cornerstonejs/tools'; import { LengthTool, utilities } from '@cornerstonejs/tools';
import { calibrateImageSpacing } from '@cornerstonejs/tools/dist/esm/utilities';
import callInputDialog from '../utils/callInputDialog'; import callInputDialog from '../utils/callInputDialog';
import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement'; import getActiveViewportEnabledElement from '../utils/getActiveViewportEnabledElement';
const { calibrateImageSpacing } = utilities;
/** /**
* Calibration Line tool works almost the same as the * Calibration Line tool works almost the same as the
*/ */

View File

@ -1,23 +1,23 @@
/** Store presentation data for either stack viewports or volume viewports */ /** Store presentation data for either stack viewports or volume viewports */
import { Types } from '@cornerstonejs/core'; import { Types } from '@cornerstonejs/core';
import { Types as UITypes } from '@ohif/ui';
export interface BasePresentation { /**
id: string; * Has information on the presentation of the viewport.
properties: Record<string, unknown>; */
initialImageIndex?: number; export interface Presentation extends Types.StackViewportProperties {
presentationIds: UITypes.PresentationIds;
viewportType: string;
initialImageIndex: number;
camera: Types.ICamera; camera: Types.ICamera;
properties: Types.StackViewportProperties | Types.VolumeViewportProperties;
zoom?: number;
pan?: [number, number];
} }
export interface StackPresentation extends BasePresentation { export type Presentations = {
viewportType: 'stack'; positionPresentation?: Presentation;
} lutPresentation?: Presentation;
};
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 default Presentation; export default Presentation;

View File

@ -153,8 +153,8 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
const restoreProtocol = !!viewportGridStore[storedHanging]; const restoreProtocol = !!viewportGridStore[storedHanging];
if ( if (
protocolId === hpInfo.hangingProtocolId && protocolId === hpInfo.protocolId &&
useStageIdx === hpInfo.stageIdx && useStageIdx === hpInfo.stageIndex &&
!activeStudyUID !activeStudyUID
) { ) {
// Clear the HP setting to reset them // Clear the HP setting to reset them

View File

@ -58,7 +58,7 @@ const reuseCachedLayout = (
} }
if (displaySetOptions[i]?.id) { if (displaySetOptions[i]?.id) {
displaySetSelectorMap[ displaySetSelectorMap[
`${activeStudyUID}: ${displaySetOptions[i].id}: ${displaySetOptions[i] `${activeStudyUID}:${displaySetOptions[i].id}:${displaySetOptions[i]
.matchedDisplaySetsIndex || 0}` .matchedDisplaySetsIndex || 0}`
] = displaySetUID; ] = displaySetUID;
} }

View File

@ -186,7 +186,6 @@ const hpMN: Types.HangingProtocol.Protocol = {
}, },
}, },
viewportStructure: { viewportStructure: {
layoutType: 'grid',
layoutType: 'grid', layoutType: 'grid',
properties: { properties: {
rows: 1, rows: 1,
@ -245,7 +244,6 @@ const hpMN: Types.HangingProtocol.Protocol = {
}, },
displaySets: [ displaySets: [
{ {
matchedDisplaySetsIndex: 1,
id: 'defaultDisplaySetId', id: 'defaultDisplaySetId',
}, },
], ],

View File

@ -42,6 +42,12 @@ export default class StateSyncService extends PubSubService {
public init(extensionManager: ExtensionManager): void { } 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 { public register(id: string, config: StateConfig): void {
this.registeredStateSets[id] = config; this.registeredStateSets[id] = config;
this.store({ [id]: {} }); this.store({ [id]: {} });

View File

@ -141,8 +141,8 @@ export type initialImageOptions = {
}; };
export type ViewportOptions = { export type ViewportOptions = {
toolGroupId: string; toolGroupId?: string;
viewportType: string; viewportType?: string;
id?: string; id?: string;
orientation?: string; orientation?: string;
viewportId?: string; viewportId?: string;

View File

@ -60,13 +60,18 @@ clears all states registered with `clearOnModeExit: true`.
To avoid clearing the state, the mode definition should store any transient To avoid clearing the state, the mode definition should store any transient
state in the mode onModeExit and recover it in the `mode.onModeEnter`. state in the mode onModeExit and recover it in the `mode.onModeEnter`.
## OHIF Registered State ## OHIF Registered State Sync Stores
There are a number of defined states here. It is recommended to update this There are a number of defined stores here. It is recommended to update this
list as states are added: list as state stores are added:
### Default Extension Stores
* `viewportGridStore` has viewport grid restore information for returning to an earlier grid layout. * `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. * `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) * `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. * `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

View File

@ -8,7 +8,7 @@ import React, {
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import viewportLabels from '../utils/viewportLabels'; import viewportLabels from '../utils/viewportLabels';
import getPresentationId from './getPresentationId'; import getPresentationIds from './getPresentationIds';
const DEFAULT_STATE = { const DEFAULT_STATE = {
activeViewportIndex: 0, activeViewportIndex: 0,
@ -69,7 +69,7 @@ const reuseViewport = (idSet, viewport, stateViewports) => {
// }; // };
} }
// Find a viewport instance number different from earlier viewports having // 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 :-) // viewports hopefully :-)
for (let i = 0; i < 10000; i++) { for (let i = 0; i < 10000; i++) {
const viewportId = 'viewport-' + i; const viewportId = 'viewport-' + i;
@ -121,7 +121,10 @@ export function ViewportGridProvider({ children, service }) {
displaySetOptions, displaySetOptions,
viewportLabel: viewportLabels[viewportIndex], viewportLabel: viewportLabels[viewportIndex],
}; };
viewportOptions.presentationId = getPresentationId(newView, viewports); viewportOptions.presentationIds = getPresentationIds(
newView,
viewports
);
// Make sure we assign a viewport id // Make sure we assign a viewport id
newView = reuseViewport({}, newView, state.viewports); newView = reuseViewport({}, newView, state.viewports);
@ -207,8 +210,8 @@ export function ViewportGridProvider({ children, service }) {
viewports[viewportIndex], viewports[viewportIndex],
state.viewports state.viewports
); );
if (!viewport.viewportOptions.presentationId) { if (!viewport.viewportOptions.presentationIds) {
viewport.viewportOptions.presentationId = getPresentationId( viewport.viewportOptions.presentationIds = getPresentationIds(
viewport, viewport,
viewports viewports
); );

View File

@ -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 <displaySetUID>, in a viewport of type volume,
* the generated presentationID might be
* `volume:axial:<displaySetUID>`. 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;

View File

@ -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;

View File

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ThumbnailType from './ThumbnailType'; import ThumbnailType from './ThumbnailType';
import { PresentationIds } from '../contextProviders/getPresentationIds';
// A few miscellaneous types declared inline here. // 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]); const StringArray = PropTypes.oneOfType([PropTypes.string, PropTypes.array]);
export { StringNumber, StringArray, ThumbnailType }; export { StringNumber, StringArray, ThumbnailType, PresentationIds };

View File

@ -51,6 +51,8 @@ describe('OHIF Study Viewer Page', function() {
}); });
it('performs double-click to load thumbnail in active viewport', () => { 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('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
//cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText); //cy.get('@viewportInfoBottomLeft').should('contains.text', expectedText);

View File

@ -1,6 +1,6 @@
import React, { useEffect, useCallback } from 'react'; import React, { useEffect, useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ServicesManager } from '@ohif/core'; import { ServicesManager, Types } from '@ohif/core';
import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import EmptyViewport from './EmptyViewport'; 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) => { const compareViewportOptions = (opts1, opts2) => {
if ((opts1.viewportType || 'stack') != opts2.viewportType) { if ((opts1.viewportType || 'stack') != opts2.viewportType) {
return false; return false;
@ -62,7 +53,7 @@ function ViewerViewportGrid(props) {
*/ */
const updateDisplaySetsFromProtocol = ( const updateDisplaySetsFromProtocol = (
protocol, protocol: Types.HangingProtocol.Protocol,
stage, stage,
activeStudyUID, activeStudyUID,
viewportMatchDetails viewportMatchDetails
@ -119,7 +110,6 @@ function ViewerViewportGrid(props) {
numCols, numCols,
layoutType, layoutType,
layoutOptions, layoutOptions,
hpInfo: createHpInfo(protocol, stage, activeStudyUID),
findOrCreateViewport, findOrCreateViewport,
}); });
}; };