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:
parent
544bf55a4f
commit
ee4e8a4105
@ -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
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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]: {} });
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
|
||||||
120
platform/ui/src/contextProviders/getPresentationIds.ts
Normal file
120
platform/ui/src/contextProviders/getPresentationIds.ts
Normal 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;
|
||||||
@ -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 };
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user