fix(jump): Jump to measurement wasn't jumping when presentation already existed (#3318)

* fix: Jump to measurement after presentation store

* Fix initial load of DICOM SR

* Fix measurement highlight issues

* Clear measurements changed to array

* Fix merge issues

* PR comments

* Removed unused formatting.  Change to single event

* Fix the unit test

* PR review comments

* refactor(viewport): simplify imports, align destructuring

Simplify import statements by removing unused imports, organize existing imports, and align destructured imports consistently across the component.

---------
This commit is contained in:
Bill Wallace authored and GitHub committed 2023-05-01 12:54:33 -04:00
1 parent 66f6e3eade
commit b5b936b53e
21 files changed
+640 -298

No files matched your search

@@ -3,10 +3,8 @@ import { DisplaySetService, classes } from '@ohif/core';
const ImageSet = classes.ImageSet;
const findInstance = (measurement, displaySetService: DisplaySetService) => {
const {
displaySetInstanceUID,
ReferencedSOPInstanceUID: sopUid,
} = measurement;
const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } =
measurement;
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
@@ -52,7 +50,7 @@ const findReferencedInstances = (
const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
const instances = findReferencedInstances(displaySetService, displaySet);
// This will be a member function of the created image set
const updateInstances = function() {
const updateInstances = function () {
this.images.splice(
0,
this.images.length,
@@ -77,6 +75,8 @@ const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
numImageFrames: instances.length,
SOPClassHandlerId: `@ohif/extension-default.sopClassHandlerModule.stack`,
isReconstructable: false,
// This object is made of multiple instances from other series
isCompositeStack: true,
madeInClient: true,
updateInstances,
});
@@ -2,18 +2,10 @@ import PropTypes from 'prop-types';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
import { CornerstoneServices } from '@ohif/extension-cornerstone/types';
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
import {
Icon,
Notification,
Tooltip,
useViewportDialog,
useViewportGrid,
ViewportActionBar,
} from '@ohif/ui';
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
@@ -40,7 +32,7 @@ function OHIFCornerstoneSRViewport(props) {
displaySetService,
cornerstoneViewportService,
measurementService,
} = servicesManager.services as CornerstoneServices;
} = servicesManager.services;
// SR viewport will always have a single display set
if (displaySets.length > 1) {
@@ -50,7 +42,6 @@ function OHIFCornerstoneSRViewport(props) {
const srDisplaySet = displaySets[0];
const [viewportGrid, viewportGridService] = useViewportGrid();
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
const [measurementSelected, setMeasurementSelected] = useState(0);
const [measurementCount, setMeasurementCount] = useState(1);
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(
@@ -9,6 +9,7 @@ import {
StackViewport,
utilities as csUtils,
} from '@cornerstonejs/core';
import { MeasurementService } from '@ohif/core';
import {
CinePlayer,
useCine,
@@ -30,6 +31,12 @@ import CornerstoneServices from '../types/CornerstoneServices';
const STACK = 'stack';
/**
* Caches the jump to measurement operation, so that if display set is shown,
* it can jump to the measurement.
*/
let cacheJumpToMeasurementEvent;
function areEqual(prevProps, nextProps) {
if (nextProps.needsRerendering) {
return false;
@@ -412,6 +419,13 @@ const OHIFCornerstoneViewport = React.memo(props => {
lutPresentation:
lutPresentationStore[presentationIds?.lutPresentationId],
};
let measurement;
if (cacheJumpToMeasurementEvent?.viewportIndex === viewportIndex) {
measurement = cacheJumpToMeasurementEvent.measurement;
// Delete the position presentation so that viewport navigates direct
presentations.positionPresentation = null;
cacheJumpToMeasurementEvent = null;
}
cornerstoneViewportService.setViewportData(
viewportIndex,
@@ -420,6 +434,10 @@ const OHIFCornerstoneViewport = React.memo(props => {
displaySetOptions,
presentations
);
if (measurement) {
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
}
};
loadViewportData();
@@ -536,22 +554,30 @@ function _subscribeToJumpToMeasurementEvents(
displaySet => displaySet.displaySetInstanceUID
);
const { unsubscribe } = measurementService.subscribe(
measurementService.EVENTS.JUMP_TO_MEASUREMENT,
({ measurement }) => {
if (!measurement) return;
// Jump the the measurement if the viewport contains the displaySetUID (fusion)
if (displaysUIDs.includes(measurement.displaySetInstanceUID)) {
_jumpToMeasurement(
measurement,
elementRef,
viewportIndex,
measurementService,
displaySetService,
viewportGridService,
cornerstoneViewportService
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
props => {
cacheJumpToMeasurementEvent = props;
const { viewportIndex: jumpIndex, measurement, isConsumed } = props;
if (!measurement || isConsumed) return;
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
// Decide on which viewport should handle this
cacheJumpToMeasurementEvent.cornerstoneViewport = cornerstoneViewportService.getViewportIndexToJump(
jumpIndex,
measurement.displaySetInstanceUID,
{ referencedImageId: measurement.referencedImageId }
);
}
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportIndex)
return;
_jumpToMeasurement(
measurement,
elementRef,
viewportIndex,
measurementService,
displaySetService,
viewportGridService,
cornerstoneViewportService
);
}
);
@@ -568,21 +594,19 @@ function _checkForCachedJumpToMeasurementEvents(
viewportGridService,
cornerstoneViewportService
) {
if (!cacheJumpToMeasurementEvent) return;
if (cacheJumpToMeasurementEvent.isConsumed) {
cacheJumpToMeasurementEvent = null;
return;
}
const displaysUIDs = displaySets.map(
displaySet => displaySet.displaySetInstanceUID
);
if (!displaysUIDs?.length) return;
const measurementIdToJumpTo = measurementService.getJumpToMeasurement(
viewportIndex
);
if (measurementIdToJumpTo && elementRef) {
// Jump to measurement if the measurement exists
const measurement = measurementService.getMeasurement(
measurementIdToJumpTo
);
// Jump to measurement if the measurement exists
const { measurement } = cacheJumpToMeasurementEvent;
if (measurement && elementRef) {
if (displaysUIDs.includes(measurement?.displaySetInstanceUID)) {
_jumpToMeasurement(
measurement,
@@ -611,6 +635,7 @@ function _jumpToMeasurement(
if (!SOPInstanceUID) {
console.warn('cannot jump in a non-acquisition plane measurements yet');
return;
}
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
@@ -623,15 +648,17 @@ function _jumpToMeasurement(
viewportGridService.setActiveViewportIndex(viewportIndex);
const enableElement = getEnabledElement(targetElement);
const enabledElement = getEnabledElement(targetElement);
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
viewportIndex
);
if (enableElement) {
if (enabledElement) {
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
const viewport = enableElement.viewport as IStackViewport | IVolumeViewport;
const viewport = enabledElement.viewport as
| IStackViewport
| IVolumeViewport;
let imageIdIndex = 0;
let viewportCameraDirectionMatch = true;
@@ -680,7 +707,8 @@ function _jumpToMeasurement(
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
// Jump to measurement consumed, remove.
measurementService.removeJumpToMeasurement(viewportIndex);
cacheJumpToMeasurementEvent?.consume?.();
cacheJumpToMeasurementEvent = null;
}
}
+32 -77
View File
@@ -10,6 +10,7 @@ import {
utilities as cstUtils,
ReferenceLinesTool,
} from '@cornerstonejs/tools';
import { Types as OhifTypes } from '@ohif/core';
import CornerstoneViewportDownloadForm from './utils/CornerstoneViewportDownloadForm';
import callInputDialog from './utils/callInputDialog';
@@ -19,7 +20,10 @@ import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/u
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
import { CornerstoneServices } from './types';
function commandsModule({ servicesManager, commandsManager }) {
function commandsModule({
servicesManager,
commandsManager,
}: OhifTypes.Extensions.ExtensionParams): OhifTypes.Extensions.CommandsModule {
const {
viewportGridService,
toolGroupService,
@@ -484,33 +488,22 @@ function commandsModule({ servicesManager, commandsManager }) {
}
}
},
firstImage: () => {
// Get current active viewport (return if none active)
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
}
const { viewport } = enabledElement;
// Check viewport is supported
if (
viewport! instanceof StackViewport &&
viewport! instanceof VolumeViewport
) {
throw new Error('Unsupported viewport type');
}
// Set slice to first slice
const options = { imageIndex: 0 };
cstUtils.jumpToSlice(viewport.element, options);
},
lastImage: () => {
/** Jumps the active viewport or the specified one to the given slice index */
jumpToImage: ({ imageIndex, viewport: gridViewport }): void => {
// Get current active viewport (return if none active)
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
let viewport;
if (!gridViewport) {
const enabledElement = _getActiveViewportEnabledElement();
if (!enabledElement) {
return;
}
viewport = enabledElement.viewport;
} else {
viewport = cornerstoneViewportService.getCornerstoneViewport(
gridViewport.id
);
}
const { viewport } = enabledElement;
// Get number of slices
// -> Copied from cornerstone3D jumpToSlice\_getImageSliceData()
@@ -525,8 +518,14 @@ function commandsModule({ servicesManager, commandsManager }) {
throw new Error('Unsupported viewport type');
}
const jumpIndex =
imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex;
if (jumpIndex >= numberOfSlices || jumpIndex < 0) {
throw new Error(`Can't jump to ${imageIndex}`);
}
// Set slice to last slice
const options = { imageIndex: numberOfSlices - 1 };
const options = { imageIndex: jumpIndex };
cstUtils.jumpToSlice(viewport.element, options);
},
scroll: ({ direction }) => {
@@ -625,8 +624,6 @@ function commandsModule({ servicesManager, commandsManager }) {
getNearbyToolData: {
commandFn: actions.getNearbyToolData,
storeContexts: [],
options: {},
},
getNearbyAnnotation: {
commandFn: actions.getNearbyAnnotation,
@@ -636,142 +633,100 @@ function commandsModule({ servicesManager, commandsManager }) {
deleteMeasurement: {
commandFn: actions.deleteMeasurement,
storeContexts: [],
options: {},
},
setMeasurementLabel: {
commandFn: actions.setMeasurementLabel,
storeContexts: [],
options: {},
},
updateMeasurement: {
commandFn: actions.updateMeasurement,
storeContexts: [],
options: {},
},
setWindowLevel: {
commandFn: actions.setWindowLevel,
storeContexts: [],
options: {},
},
toolbarServiceRecordInteraction: {
commandFn: actions.toolbarServiceRecordInteraction,
storeContexts: [],
options: {},
},
setToolActive: {
commandFn: actions.setToolActive,
storeContexts: [],
options: {},
},
rotateViewportCW: {
commandFn: actions.rotateViewport,
storeContexts: [],
options: { rotation: 90 },
},
rotateViewportCCW: {
commandFn: actions.rotateViewport,
storeContexts: [],
options: { rotation: -90 },
},
incrementActiveViewport: {
commandFn: actions.incrementActiveViewport,
storeContexts: [],
},
decrementActiveViewport: {
commandFn: actions.decrementActiveViewport,
storeContexts: [],
},
flipViewportHorizontal: {
commandFn: actions.flipViewportHorizontal,
storeContexts: [],
options: {},
},
flipViewportVertical: {
commandFn: actions.flipViewportVertical,
storeContexts: [],
options: {},
},
invertViewport: {
commandFn: actions.invertViewport,
storeContexts: [],
options: {},
},
resetViewport: {
commandFn: actions.resetViewport,
storeContexts: [],
options: {},
},
scaleUpViewport: {
commandFn: actions.scaleViewport,
storeContexts: [],
options: { direction: 1 },
},
scaleDownViewport: {
commandFn: actions.scaleViewport,
storeContexts: [],
options: { direction: -1 },
},
fitViewportToWindow: {
commandFn: actions.scaleViewport,
storeContexts: [],
options: { direction: 0 },
},
nextImage: {
commandFn: actions.scroll,
storeContexts: [],
options: { direction: 1 },
},
previousImage: {
commandFn: actions.scroll,
storeContexts: [],
options: { direction: -1 },
},
firstImage: {
commandFn: actions.firstImage,
storeContexts: [],
options: {},
commandFn: actions.jumpToImage,
options: { imageIndex: 0 },
},
lastImage: {
commandFn: actions.lastImage,
storeContexts: [],
options: {},
commandFn: actions.jumpToImage,
options: { imageIndex: -1 },
},
jumpToImage: {
commandFn: actions.jumpToImage,
},
showDownloadViewportModal: {
commandFn: actions.showDownloadViewportModal,
storeContexts: [],
options: {},
},
toggleCine: {
commandFn: actions.toggleCine,
storeContexts: [],
options: {},
},
arrowTextCallback: {
commandFn: actions.arrowTextCallback,
storeContexts: [],
options: {},
},
setViewportActive: {
commandFn: actions.setViewportActive,
storeContexts: [],
options: {},
},
setViewportColormap: {
commandFn: actions.setViewportColormap,
storeContexts: [],
options: {},
},
toggleStackImageSync: {
commandFn: actions.toggleStackImageSync,
storeContexts: [],
options: {},
},
toggleReferenceLines: {
commandFn: actions.toggleReferenceLines,
storeContexts: [],
options: {},
},
};
@@ -16,14 +16,14 @@ const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
class CornerstoneCacheService {
static REGISTRATION = {
name: 'cornerstoneCacheService',
altName: 'CornerstoneCacheService',
name: 'cornerstoneCacheService',
altName: 'CornerstoneCacheService',
create: ({
servicesManager,
}: Types.Extensions.ExtensionParams): CornerstoneCacheService => {
return new CornerstoneCacheService(servicesManager);
},
};
},
};
stackImageIds: Map<string, string[]> = new Map();
volumeImageIds: Map<string, string[]> = new Map();
@@ -143,13 +143,18 @@ class CornerstoneCacheService {
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
}
const { displaySetInstanceUID, StudyInstanceUID } = displaySet;
const {
displaySetInstanceUID,
StudyInstanceUID,
isCompositeStack,
} = displaySet;
const StackViewportData: StackViewportData = {
viewportType,
data: {
StudyInstanceUID,
displaySetInstanceUID,
isCompositeStack,
imageIds: stackImageIds,
},
};
@@ -906,6 +906,34 @@ class CornerstoneViewportService extends PubSubService
return images[0].FrameOfReferenceUID;
}
}
/**
* Looks through the viewports to see if the specified measurement can be
* displayed in one of the viewports.
*
* @param measurement
* The measurement that is desired to view.
* @param activeViewportIndex - the index that was active at the time the jump
* was initiated.
* @return the viewportIndex to display the given measurement
*/
public getViewportIndexToJump(
activeViewportIndex: number,
displaySetInstanceUID: string,
cameraProps: unknown
): number {
const viewportInfo = this.viewportsInfo.get(activeViewportIndex);
const { referencedImageId } = cameraProps;
if (viewportInfo?.contains(displaySetInstanceUID, referencedImageId)) {
return activeViewportIndex;
}
return (
[...this.viewportsById.values()].find(viewportInfo =>
viewportInfo.contains(displaySetInstanceUID, referencedImageId)
)?.viewportIndex ?? -1
);
}
}
export default CornerstoneViewportService;
@@ -88,6 +88,20 @@ export type DisplaySet = {
const STACK = 'stack';
const DEFAULT_TOOLGROUP_ID = 'default';
// Return true if the data contains the given display set UID OR the imageId
// if it is a composite object.
const dataContains = (
data,
displaySetUID: string,
imageId?: string
): boolean => {
if (data.displaySetInstanceUID === displaySetUID) return true;
if (imageId && data.isCompositeStack && data.imageIds) {
return !!data.imageIds.find(dataId => dataId === imageId);
}
return false;
};
class ViewportInfo {
private viewportId = '';
private viewportIndex: number;
@@ -104,6 +118,21 @@ class ViewportInfo {
this.setPublicDisplaySetOptions([{}]);
}
/**
* Return true if the viewport contains the given display set UID,
* OR if it is a composite stack and contains the given imageId
*/
public contains(displaySetUID: string, imageId: string): boolean {
if (!this.viewportData?.data) return false;
if (this.viewportData.data.length) {
return !!this.viewportData.find(data =>
dataContains(data, displaySetUID, imageId)
);
}
return dataContains(this.viewportData.data, displaySetUID, imageId);
}
public destroy = (): void => {
this.element = null;
this.viewportData = null;
@@ -1,12 +1,12 @@
import {
Enums,
Types,
} from '@cornerstonejs/core';
import { Enums, Types } from '@cornerstonejs/core';
type StackData = {
StudyInstanceUID: string;
displaySetInstanceUID: string;
// A composite stack is one created from other display sets - kind of like
// madeInClient, but specific to indicating that the imageIds can come from
// different series or even studies.
isCompositeStack?: boolean;
imageIds: string[];
frameRate?: number;
isClip?: boolean;
@@ -20,19 +20,14 @@ type VolumeData = {
imageIds?: string[];
};
type StackViewportData = {
type StackViewportData = {
viewportType: Enums.ViewportType;
data: StackData;
};
type VolumeViewportData = {
type VolumeViewportData = {
viewportType: Enums.ViewportType;
data: VolumeData[];
};
export type {
StackViewportData,
VolumeViewportData,
StackData,
VolumeData,
};
export type { StackViewportData, VolumeViewportData, StackData, VolumeData };
@@ -188,6 +188,7 @@ function getDisplayText(mappedAnnotations, displaySet) {
const instanceText = InstanceNumber ? ` I: ${InstanceNumber}` : '';
const frameText = displaySet.isMultiFrame ? ` F: ${frameNumber}` : '';
if (length === null || length === undefined) return displayText;
const roundedLength = utils.roundNumber(length, 2);
displayText.push(
`${roundedLength} mm (S: ${SeriesNumber}${instanceText}${frameText})`
+7 -1
View File
@@ -1,4 +1,4 @@
import { ServicesManager, utils } from '@ohif/core';
import { ServicesManager, utils, Types } from '@ohif/core';
import {
ContextMenuController,
@@ -244,6 +244,12 @@ const commandsModule = ({
}
}
// Do this after successfully applying the update
// Note, don't store the active display set - it is only needed while
// changing display sets. This causes jump to measurement to fail on
// multi-study display.
delete displaySetSelectorMap[
`${activeStudyUID || hpInfo.activeStudyUID}:activeDisplaySet:0`
];
stateSyncService.store(stateSyncReduce);
// This is a default action applied
actions.toggleHpTools(hangingProtocolService.getActiveProtocol());
@@ -47,6 +47,15 @@ function TrackedMeasurementsContextProvider(
const uid = trackedMeasurements[0].uid;
console.log(
'jumping to measurement reset viewport',
viewportGrid.activeViewportIndex,
trackedMeasurements[0]
);
viewportGridService.setDisplaySetsForViewport({
viewportIndex: viewportGrid.activeViewportIndex,
displaySetInstanceUIDs: [trackedMeasurements[0].displaySetInstanceUID],
});
measurementService.jumpToMeasurement(
viewportGrid.activeViewportIndex,
uid