diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index d7570200d..8045793c1 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -264,11 +264,10 @@ const OHIFCornerstoneViewport = React.memo( const presentations = getViewportPresentations(viewportId, viewportOptions); let measurement; - if (cacheJumpToMeasurementEvent?.viewportId === viewportId) { + if (cacheJumpToMeasurementEvent?.cornerstoneViewport === viewportId) { measurement = cacheJumpToMeasurementEvent.measurement; // Delete the position presentation so that viewport navigates direct presentations.positionPresentation = null; - cacheJumpToMeasurementEvent = null; } // Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component @@ -281,7 +280,7 @@ const OHIFCornerstoneViewport = React.memo( viewportOptions.needsRerendering = false; } - cornerstoneViewportService.setViewportData( + await cornerstoneViewportService.setViewportData( viewportId, viewportData, viewportOptions, @@ -290,7 +289,13 @@ const OHIFCornerstoneViewport = React.memo( ); if (measurement) { - cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid); + _checkForCachedJumpToMeasurementEvents( + elementRef, + viewportId, + displaySets, + servicesManager + ); + cacheJumpToMeasurementEvent = null; } }; @@ -318,8 +323,6 @@ const OHIFCornerstoneViewport = React.memo( servicesManager ); - _checkForCachedJumpToMeasurementEvents(elementRef, viewportId, displaySets, servicesManager); - return () => { unsubscribeFromJumpToMeasurementEvents(); }; @@ -451,6 +454,16 @@ function _subscribeToJumpToMeasurementEvents(elementRef, viewportId, servicesMan if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) { return; } + const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId); + // Confirm viewportInfo matches measurement displaySetInstanceUID and referencedImageId + if ( + !viewportInfo?.contains( + measurement.displaySetInstanceUID, + measurement.referencedImageId || measurement.metadata?.referencedImageId + ) + ) { + return; + } _jumpToMeasurement(measurement, elementRef, viewportId, servicesManager); } ); diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 82f7fbb03..bc3800928 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -372,7 +372,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi * @param publicDisplaySetOptions - The public display set options. * @param presentations - The presentations to set. */ - public setViewportData( + public async setViewportData( viewportId: string, viewportData: StackViewportData | VolumeViewportData, publicViewportOptions: PublicViewportOptions, @@ -458,21 +458,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi this.viewportsById.set(viewportId, viewportInfo); const viewport = renderingEngine.getViewport(viewportId); - const displaySetPromise = this._setDisplaySets( - viewport, - viewportData, - viewportInfo, - presentations - ); + await this._setDisplaySets(viewport, viewportData, viewportInfo, presentations); // The broadcast event here ensures that listeners have a valid, up to date // viewport to access. Doing it too early can result in exceptions or // invalid data. - displaySetPromise.then(() => { - this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { - viewportData, - viewportId, - }); + this._broadcastEvent(this.EVENTS.VIEWPORT_DATA_CHANGED, { + viewportData, + viewportId, }); } diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx index 53f8b2242..b00974fdc 100644 --- a/platform/app/src/components/ViewportGrid.tsx +++ b/platform/app/src/components/ViewportGrid.tsx @@ -6,6 +6,7 @@ import { useViewportGrid } from '@ohif/ui-next'; import EmptyViewport from './EmptyViewport'; import classNames from 'classnames'; import { useAppConfig } from '@state'; +import getClosestOrientationFromIOP from '../utils/getClosestOrientationFromIOP'; function ViewerViewportGrid(props: withAppTypes) { const { servicesManager, viewportComponents = [], dataSource } = props; @@ -177,7 +178,20 @@ function ViewerViewportGrid(props: withAppTypes) { const { displaySetInstanceUID: referencedDisplaySetInstanceUID } = measurement; const updatedViewports = _getUpdatedViewports(viewportId, referencedDisplaySetInstanceUID); - if (!updatedViewports[0]) { + let viewportToUpdate; + if (updatedViewports.length > 1) { + // To get the viewport which orientation is closest to IOP + const closestOrientation = getClosestOrientationFromIOP( + displaySetService, + referencedDisplaySetInstanceUID + ); + viewportToUpdate = updatedViewports.find( + viewport => viewport.viewportOptions?.orientation == closestOrientation + ); + } + // If closest orientation viewport is not find then choose 0 + viewportToUpdate = updatedViewports[0]; + if (!viewportToUpdate) { console.warn( 'ViewportGrid::Unable to navigate to viewport containing', referencedDisplaySetInstanceUID @@ -185,14 +199,9 @@ function ViewerViewportGrid(props: withAppTypes) { return; } - // Arbitrarily assign the viewport to element 0 - // TODO - this should perform a search to find the most suitable viewport. - updatedViewports[0] = { ...updatedViewports[0] }; - const [viewport] = updatedViewports; - // Copy the viewport options to prevent modifying the internal data - viewport.viewportOptions = { - ...viewport.viewportOptions, + viewportToUpdate.viewportOptions = { + ...viewportToUpdate.viewportOptions, orientation: 'acquisition', // The preferred way to jump to the measurement view is to set the // view reference, as this can hold information such as the orientation diff --git a/platform/app/src/utils/getClosestOrientationFromIOP.ts b/platform/app/src/utils/getClosestOrientationFromIOP.ts new file mode 100644 index 000000000..735b9ba57 --- /dev/null +++ b/platform/app/src/utils/getClosestOrientationFromIOP.ts @@ -0,0 +1,59 @@ +import { vec3 } from 'gl-matrix'; +import { Enums } from '@cornerstonejs/core'; +import OrientationAxis = Enums.OrientationAxis; + +/** + * Get the plane (orientation) to which the ImageOrientationPatient is most closely aligned + * + * @param displaySetService + * @param displaySetInstanceUID + * @returns orientation + */ +export default function getClosestOrientationFromIOP( + displaySetService, + displaySetInstanceUID +): OrientationAxis { + const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID); + const imageOrientationPatient = displaySet.instances[0].ImageOrientationPatient as Array; + // ImageOrientationPatient must be an array of length 6. + if (imageOrientationPatient?.length !== 6) { + return; + } + + // Take cross product to get vector coming "out" of image plane + const rowCosineVec = vec3.fromValues( + imageOrientationPatient[0], + imageOrientationPatient[1], + imageOrientationPatient[2] + ); + const colCosineVec = vec3.fromValues( + imageOrientationPatient[3], + imageOrientationPatient[4], + imageOrientationPatient[5] + ); + const scanAxisNormal = vec3.cross(vec3.create(), rowCosineVec, colCosineVec); + + // Define the reference vectors for axial, coronal, and sagittal planes + const unitVectors = { + [OrientationAxis.AXIAL]: vec3.fromValues(0, 0, 1), + [OrientationAxis.CORONAL]: vec3.fromValues(0, 1, 0), + [OrientationAxis.SAGITTAL]: vec3.fromValues(1, 0, 0), + }; + + // Compute dot products for each reference plane + // Because all vectors are normalized, dot product is bounded between -1 and 1 + let maxDot = 0; + let maxOrientation: string = ''; + for (const [k, v] of Object.entries(unitVectors)) { + // Absolute value of dot product because we only care about alignment with the axis + // For example, dot product of -1 for a given axis means perfect alignment + // but the image is pointing in the "opposite" direction + const res = Math.abs(vec3.dot(scanAxisNormal, v)); + if (res > maxDot) { + maxDot = res; + maxOrientation = k; + } + } + + return maxOrientation as OrientationAxis; +}