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:
1 parent
66f6e3eade
commit
b5b936b53e
21 files changed
+640
-298
No files matched your search
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+10
-5
@@ -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})`
|
||||
|
||||
Reference in new issue
Block a user