import React, { useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; import { ServicesManager, Types } from '@ohif/core'; import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; import { utils } from '@ohif/core'; import EmptyViewport from './EmptyViewport'; import classNames from 'classnames'; const { isEqualWithin } = utils; const ORIENTATION_MAP = { axial: { viewPlaneNormal: [0, 0, -1], viewUp: [0, -1, 0], }, sagittal: { viewPlaneNormal: [1, 0, 0], viewUp: [0, 0, 1], }, coronal: { viewPlaneNormal: [0, 1, 0], viewUp: [0, 0, 1], }, }; const compareViewportOptions = (opts1, opts2) => { if ((opts1.viewportType || 'stack') != opts2.viewportType) { return false; } return true; }; function ViewerViewportGrid(props) { const { servicesManager, viewportComponents, dataSource } = props; const [viewportGrid, viewportGridService] = useViewportGrid(); const { layout, activeViewportIndex, viewports } = viewportGrid; const { numCols, numRows } = layout; // TODO -> Need some way of selecting which displaySets hit the viewports. const { displaySetService, measurementService, hangingProtocolService, uiNotificationService, } = (servicesManager as ServicesManager).services; /** * This callback runs after the viewports structure has changed in any way. * On initial display, that means if it has changed by applying a HangingProtocol, * while subsequently it may mean by changing the stage or by manually adjusting * the layout. */ const updateDisplaySetsFromProtocol = ( protocol: Types.HangingProtocol.Protocol, stage, activeStudyUID, viewportMatchDetails ) => { const availableDisplaySets = displaySetService.getActiveDisplaySets(); if (!availableDisplaySets.length) { console.log('No available display sets', availableDisplaySets); return; } // Match each viewport individually const { layoutType } = stage.viewportStructure; const stageProps = stage.viewportStructure.properties; const { columns: numCols, rows: numRows, layoutOptions = [] } = stageProps; /** * This find or create viewport uses the hanging protocol results to * specify the viewport match details, which specifies the size and * setup of the various viewports. */ const findOrCreateViewport = viewportIndex => { const details = viewportMatchDetails.get(viewportIndex); if (!details) { console.log('No match details for viewport', viewportIndex); return; } const { displaySetsInfo, viewportOptions } = details; const displaySetUIDsToHang = []; const displaySetUIDsToHangOptions = []; displaySetsInfo.forEach( ({ displaySetInstanceUID, displaySetOptions }) => { if (displaySetInstanceUID) { displaySetUIDsToHang.push(displaySetInstanceUID); } displaySetUIDsToHangOptions.push(displaySetOptions); } ); return { displaySetInstanceUIDs: displaySetUIDsToHang, displaySetOptions: displaySetUIDsToHangOptions, viewportOptions: hangingProtocolService.getComputedOptions( viewportOptions, displaySetUIDsToHang ), }; }; viewportGridService.setLayout({ numRows, numCols, layoutType, layoutOptions, findOrCreateViewport, }); }; const _getUpdatedViewports = useCallback( (viewportIndex, displaySetInstanceUID) => { let updatedViewports = []; try { updatedViewports = hangingProtocolService.getViewportsRequireUpdate( viewportIndex, displaySetInstanceUID ); } catch (error) { console.warn(error); uiNotificationService.show({ title: 'Drag and Drop', message: 'The selected display sets could not be added to the viewport due to a mismatch in the Hanging Protocol rules.', type: 'info', duration: 3000, }); } return updatedViewports; }, [hangingProtocolService, uiNotificationService] ); // Using Hanging protocol engine to match the displaySets useEffect(() => { const { unsubscribe } = hangingProtocolService.subscribe( hangingProtocolService.EVENTS.PROTOCOL_CHANGED, ({ protocol, stage, activeStudyUID, viewportMatchDetails }) => { updateDisplaySetsFromProtocol( protocol, stage, activeStudyUID, viewportMatchDetails ); } ); return () => { unsubscribe(); }; }, []); useEffect(() => { const { unsubscribe } = measurementService.subscribe( measurementService.EVENTS.JUMP_TO_MEASUREMENT, ({ viewportIndex, measurement }) => { const { displaySetInstanceUID: referencedDisplaySetInstanceUID, metadata: { viewPlaneNormal }, } = measurement; // if we already have the displaySet in one of the viewports // Todo: handle fusion display sets? for (const viewport of viewports) { const isMatch = viewport.displaySetInstanceUIDs.includes( referencedDisplaySetInstanceUID ); if (isMatch) { return; } } const displaySet = displaySetService.getDisplaySetByUID( referencedDisplaySetInstanceUID ); let imageIndex; // jump straight to the initial image index if we can if (displaySet.images && measurement.SOPInstanceUID) { imageIndex = displaySet.images.findIndex( image => image.SOPInstanceUID === measurement.SOPInstanceUID ); } const updatedViewports = _getUpdatedViewports( viewportIndex, referencedDisplaySetInstanceUID ); if (!updatedViewports || !updatedViewports.length) { return; } updatedViewports.forEach(vp => { vp.viewportOptions ||= {}; const { orientation, viewportType } = vp.viewportOptions; let initialImageOptions; // For initial imageIndex to hang be careful for the volume viewport if (viewportType === 'stack' || !viewportType) { initialImageOptions = { index: imageIndex, }; } else if (viewportType === 'volume') { // For the volume viewports, be careful to not jump in the viewports // that are not in the same orientation. // Todo: this doesn't work for viewports that have custom orientation // vectors specified if ( orientation && viewPlaneNormal && isEqualWithin( ORIENTATION_MAP[orientation]?.viewPlaneNormal, viewPlaneNormal ) ) { initialImageOptions = { index: imageIndex, }; } } vp.viewportOptions.initialImageOptions = initialImageOptions; }); viewportGridService.setDisplaySetsForViewports(updatedViewports); } ); return () => { unsubscribe(); }; }, [viewports]); /** const onDoubleClick = viewportIndex => { // TODO -> Disabled for now. // onNewImage on a cornerstone viewport is firing setDisplaySetsForViewport. // Which it really really shouldn't. We need a larger fix for jump to // measurements and all cornerstone "imageIndex" state to fix this. if (cachedLayout) { viewportGridService.set({ numCols: cachedLayout.numCols, numRows: cachedLayout.numRows, activeViewportIndex: cachedLayout.activeViewportIndex, viewports: cachedLayout.viewports, cachedLayout: null, }); return; } const cachedViewports = viewports.map(viewport => { return { displaySetInstanceUID: viewport.displaySetInstanceUID, }; }); viewportGridService.set({ numCols: 1, numRows: 1, activeViewportIndex: 0, viewports: [ { displaySetInstanceUID: viewports[viewportIndex].displaySetInstanceUID, imageIndex: undefined, }, ], cachedLayout: { numCols, numRows, viewports: cachedViewports, activeViewportIndex: viewportIndex, }, }); }; */ const onDropHandler = (viewportIndex, { displaySetInstanceUID }) => { const updatedViewports = _getUpdatedViewports( viewportIndex, displaySetInstanceUID ); viewportGridService.setDisplaySetsForViewports(updatedViewports); }; const getViewportPanes = useCallback(() => { const viewportPanes = []; const numViewportPanes = viewportGridService.getNumViewportPanes(); for (let i = 0; i < numViewportPanes; i++) { const viewportIndex = i; const isActive = activeViewportIndex === viewportIndex; const paneMetadata = viewports[i] || {}; const viewportId = paneMetadata.viewportId || `viewport-${i}`; if (!paneMetadata.viewportId) { paneMetadata.viewportId = viewportId; } const { displaySetInstanceUIDs, viewportOptions, displaySetOptions, // array of options for each display set in the viewport x: viewportX, y: viewportY, width: viewportWidth, height: viewportHeight, viewportLabel, } = paneMetadata; const displaySetInstanceUIDsToUse = displaySetInstanceUIDs || []; // This is causing the viewport components re-render when the activeViewportIndex changes const displaySets = displaySetInstanceUIDsToUse.map( displaySetInstanceUID => { return ( displaySetService.getDisplaySetByUID(displaySetInstanceUID) || {} ); } ); const ViewportComponent = _getViewportComponent( displaySets, viewportComponents, uiNotificationService ); // look inside displaySets to see if they need reRendering const displaySetsNeedsRerendering = displaySets.some(displaySet => { return displaySet.needsRerendering; }); const onInteractionHandler = event => { if (isActive) return; if (event) { event.preventDefault(); event.stopPropagation(); } viewportGridService.setActiveViewportIndex(viewportIndex); }; // TEMP -> Double click disabled for now // onDoubleClick={() => onDoubleClick(viewportIndex)} viewportPanes[i] = (
1 ? viewportLabel : ''} dataSource={dataSource} viewportOptions={viewportOptions} displaySetOptions={displaySetOptions} needsRerendering={displaySetsNeedsRerendering} />
); } return viewportPanes; }, [viewports, activeViewportIndex, viewportComponents, dataSource]); /** * Loading indicator until numCols and numRows are gotten from the HangingProtocolService */ if (!numRows || !numCols) { return null; } return ( {/* {ViewportPanes} */} {getViewportPanes()} ); } ViewerViewportGrid.propTypes = { viewportComponents: PropTypes.array.isRequired, servicesManager: PropTypes.instanceOf(ServicesManager), }; ViewerViewportGrid.defaultProps = { viewportComponents: [], }; function _getViewportComponent( displaySets, viewportComponents, uiNotificationService ) { if (!displaySets || !displaySets.length) { return EmptyViewport; } // Todo: Do we have a viewport that has two different SOPClassHandlerIds? const SOPClassHandlerId = displaySets[0].SOPClassHandlerId; for (let i = 0; i < viewportComponents.length; i++) { if (!viewportComponents[i]) { throw new Error('viewport components not defined'); } if (!viewportComponents[i].displaySetsToDisplay) { throw new Error('displaySetsToDisplay is null'); } if ( viewportComponents[i].displaySetsToDisplay.includes(SOPClassHandlerId) ) { const { component } = viewportComponents[i]; return component; } } console.log("Can't show displaySet", SOPClassHandlerId, displaySets[0]); uiNotificationService.show({ title: 'Viewport Not Supported Yet', message: `Cannot display SOPClassId of ${displaySets[0].SOPClassUID} yet`, type: 'error', }); return EmptyViewport; } export default ViewerViewportGrid;