diff --git a/extensions/cornerstone/src/commandsModule.js b/extensions/cornerstone/src/commandsModule.js index a1d799b86..89ffa1bc4 100644 --- a/extensions/cornerstone/src/commandsModule.js +++ b/extensions/cornerstone/src/commandsModule.js @@ -11,12 +11,19 @@ const { studyMetadataManager } = OHIF.utils; const { setViewportSpecificData } = OHIF.redux.actions; const commandsModule = ({ servicesManager }) => { + const { ViewportGridService } = servicesManager.services; + + function _getActiveViewportsEnabledElement() { + const { activeViewportIndex } = ViewportGridService.getState(); + return getEnabledElement(activeViewportIndex); + } + const actions = { getCornerstoneLibraries: () => { return { cornerstone, cornerstoneTools }; }, - rotateViewport: ({ viewports, rotation }) => { - const enabledElement = getEnabledElement(viewports.activeViewportIndex); + rotateViewport: ({ rotation }) => { + const enabledElement = _getActiveViewportsEnabledElement(); if (enabledElement) { let viewport = cornerstone.getViewport(enabledElement); @@ -25,8 +32,7 @@ const commandsModule = ({ servicesManager }) => { } }, flipViewportHorizontal: () => { - const activeViewportIndex = 0; - const enabledElement = getEnabledElement(activeViewportIndex); + const enabledElement = _getActiveViewportsEnabledElement(); if (enabledElement) { let viewport = cornerstone.getViewport(enabledElement); @@ -34,8 +40,8 @@ const commandsModule = ({ servicesManager }) => { cornerstone.setViewport(enabledElement, viewport); } }, - flipViewportVertical: ({ viewports }) => { - const enabledElement = getEnabledElement(viewports.activeViewportIndex); + flipViewportVertical: () => { + const enabledElement = _getActiveViewportsEnabledElement(); if (enabledElement) { let viewport = cornerstone.getViewport(enabledElement); @@ -43,8 +49,8 @@ const commandsModule = ({ servicesManager }) => { cornerstone.setViewport(enabledElement, viewport); } }, - scaleViewport: ({ direction, viewports }) => { - const enabledElement = getEnabledElement(viewports.activeViewportIndex); + scaleViewport: ({ direction }) => { + const enabledElement = _getActiveViewportsEnabledElement(); const step = direction * 0.15; if (enabledElement) { @@ -58,16 +64,14 @@ const commandsModule = ({ servicesManager }) => { } }, resetViewport: () => { - const activeViewportIndex = 0; - const enabledElement = getEnabledElement(activeViewportIndex); + const enabledElement = _getActiveViewportsEnabledElement(); if (enabledElement) { cornerstone.reset(enabledElement); } }, invertViewport: () => { - const activeViewportIndex = 0; - const enabledElement = getEnabledElement(activeViewportIndex); + const enabledElement = _getActiveViewportsEnabledElement(); if (enabledElement) { let viewport = cornerstone.getViewport(enabledElement); @@ -83,8 +87,8 @@ const commandsModule = ({ servicesManager }) => { } cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 }); }, - clearAnnotations: ({ viewports }) => { - const element = getEnabledElement(viewports.activeViewportIndex); + clearAnnotations: () => { + const element = _getActiveViewportsEnabledElement(); if (!element) { return; } @@ -139,16 +143,16 @@ const commandsModule = ({ servicesManager }) => { }); }); }, - nextImage: ({ viewports }) => { - const enabledElement = getEnabledElement(viewports.activeViewportIndex); + nextImage: () => { + const enabledElement = _getActiveViewportsEnabledElement(); scroll(enabledElement, 1); }, - previousImage: ({ viewports }) => { - const enabledElement = getEnabledElement(viewports.activeViewportIndex); + previousImage: () => { + const enabledElement = _getActiveViewportsEnabledElement(); scroll(enabledElement, -1); }, - getActiveViewportEnabledElement: ({ viewports }) => { - const enabledElement = getEnabledElement(viewports.activeViewportIndex); + getActiveViewportEnabledElement: () => { + const enabledElement = _getActiveViewportsEnabledElement(); return enabledElement; }, showDownloadViewportModal: () => { @@ -251,8 +255,8 @@ const commandsModule = ({ servicesManager }) => { setCornerstoneLayout: () => { setCornerstoneLayout(); }, - setWindowLevel: ({ viewports, window, level }) => { - const enabledElement = getEnabledElement(viewports.activeViewportIndex); + setWindowLevel: ({ window, level }) => { + const enabledElement = _getActiveViewportsEnabledElement(); if (enabledElement) { let viewport = cornerstone.getViewport(enabledElement); @@ -326,7 +330,7 @@ const commandsModule = ({ servicesManager }) => { }, getActiveViewportEnabledElement: { commandFn: actions.getActiveViewportEnabledElement, - storeContexts: ['viewports'], + storeContexts: [], options: {}, }, rotateViewportCW: { @@ -336,7 +340,7 @@ const commandsModule = ({ servicesManager }) => { }, rotateViewportCCW: { commandFn: actions.rotateViewport, - storeContexts: ['viewports'], + storeContexts: [], options: { rotation: -90 }, }, invertViewport: { @@ -346,7 +350,7 @@ const commandsModule = ({ servicesManager }) => { }, flipViewportVertical: { commandFn: actions.flipViewportVertical, - storeContexts: ['viewports'], + storeContexts: [], options: {}, }, flipViewportHorizontal: { @@ -356,17 +360,17 @@ const commandsModule = ({ servicesManager }) => { }, scaleUpViewport: { commandFn: actions.scaleViewport, - storeContexts: ['viewports'], + storeContexts: [], options: { direction: 1 }, }, scaleDownViewport: { commandFn: actions.scaleViewport, - storeContexts: ['viewports'], + storeContexts: [], options: { direction: -1 }, }, fitViewportToWindow: { commandFn: actions.scaleViewport, - storeContexts: ['viewports'], + storeContexts: [], options: { direction: 0 }, }, resetViewport: { @@ -376,17 +380,17 @@ const commandsModule = ({ servicesManager }) => { }, clearAnnotations: { commandFn: actions.clearAnnotations, - storeContexts: ['viewports'], + storeContexts: [], options: {}, }, nextImage: { commandFn: actions.nextImage, - storeContexts: ['viewports'], + storeContexts: [], options: {}, }, previousImage: { commandFn: actions.previousImage, - storeContexts: ['viewports'], + storeContexts: [], options: {}, }, // TOOLS @@ -408,7 +412,7 @@ const commandsModule = ({ servicesManager }) => { }, setWindowLevel: { commandFn: actions.setWindowLevel, - storeContexts: ['viewports'], + storeContexts: [], options: {}, }, }; diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index 5f80a9d6f..e7b321935 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -6,6 +6,8 @@ import merge from 'lodash.merge'; import initCornerstoneTools from './initCornerstoneTools.js'; import initWADOImageLoader from './initWADOImageLoader.js'; import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory'; +// +import { setEnabledElement } from './state'; /** * @@ -54,6 +56,22 @@ export default function init({ servicesManager, configuration }) { }; initCornerstoneTools(defaultCsToolsConfig); + // TODO: Extensions are still registered at time of registration globally + // These should be registered as a part of mode route spin up, + // and they need to self-clean on mode route destroy + // THIS + // is a way for extensions that "depend" on this extension to notify it of + // new cornerstone enabled elements so it's commands continue to work. + const handleOhifCornerstoneEnabledElementEvent = function(evt) { + const { viewportIndex, enabledElement } = evt.detail; + + setEnabledElement(viewportIndex, enabledElement); + }; + + document.addEventListener( + 'ohif-cornerstone-enabled-element-event', + handleOhifCornerstoneEnabledElementEvent + ); const toolsGroupedByType = { touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool], diff --git a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx index b5a4253ab..ca7bc92c6 100644 --- a/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx +++ b/extensions/default/src/Toolbar/ToolbarLayoutSelector.jsx @@ -15,7 +15,7 @@ const DEFAULT_LAYOUT = { function LayoutSelector() { const [isOpen, setIsOpen] = useState(false); - const [viewportGridState, dispatch] = useViewportGrid(); + const [viewportGridState, viewportGridService] = useViewportGrid(); const closeOnOutsideClick = () => { if (isOpen) { @@ -51,13 +51,7 @@ function LayoutSelector() { DropdownContent !== null && ( { - dispatch({ - type: 'SET_LAYOUT', - payload: { - numCols, - numRows, - }, - }); + viewportGridService.setLayout({ numCols, numRows }); }} /> ) diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index c54203427..35fe5d621 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -25,7 +25,7 @@ function OHIFCornerstoneSRViewport({ viewportIndex, DisplaySetService, }) { - const [viewportGrid, dispatchViewportGrid] = useViewportGrid(); + const [viewportGrid, viewportGridService] = useViewportGrid(); const [measurementSelected, setMeasurementSelected] = useState(0); const [measurementCount, setMeasurementCount] = useState(1); const [viewportData, setViewportData] = useState(null); diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index 2e9f45429..2f4b279ce 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -19,10 +19,7 @@ function PanelStudyBrowserTracking({ // doesn't have to have such an intense shape. This works well enough for now. // Tabs --> Studies --> DisplaySets --> Thumbnails const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer(); - const [ - { activeViewportIndex, viewports }, - dispatchViewportGrid, - ] = useViewportGrid(); + const [{ activeViewportIndex, viewports }] = useViewportGrid(); const [ trackedMeasurements, sendTrackedMeasurementsEvent, diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index eae34e279..79138339a 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -38,10 +38,7 @@ function TrackedCornerstoneViewport({ viewportIndex, }) { const [trackedMeasurements] = useTrackedMeasurements(); - - const [ - { activeViewportIndex, viewports }, - ] = useViewportGrid(); + const [{ activeViewportIndex, viewports }] = useViewportGrid(); // viewportIndex, onSubmit const [viewportDialogState, viewportDialogApi] = useViewportDialog(); const [viewportData, setViewportData] = useState(null); @@ -122,6 +119,18 @@ function TrackedCornerstoneViewport({ } setElement(targetElement); + + const OHIFCornerstoneEnabledElementEvent = new CustomEvent( + 'ohif-cornerstone-enabled-element-event', + { + detail: { + enabledElement: targetElement, + viewportIndex, + }, + } + ); + + document.dispatchEvent(OHIFCornerstoneEnabledElementEvent); }; useEffect(() => { diff --git a/platform/core/src/index.js b/platform/core/src/index.js index 9ffa1c70a..acfdd3fbf 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -30,8 +30,9 @@ import { // DicomMetadataStore, DisplaySetService, - ToolBarSerivce, + ToolBarSerivce, // TODO: Typo MeasurementService, + ViewportGridService, } from './services'; import IWebApiDataSource from './DataSources/IWebApiDataSource'; @@ -70,7 +71,8 @@ const OHIF = { UIViewportDialogService, DisplaySetService, MeasurementService, - ToolBarSerivce, + ToolBarSerivce, // TODO: TYPO + ViewportGridService, IWebApiDataSource, DicomMetadataStore, // @@ -112,6 +114,7 @@ export { DisplaySetService, MeasurementService, ToolBarSerivce, + ViewportGridService, IWebApiDataSource, DicomMetadataStore, ViewModelProvider, diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.js b/platform/core/src/services/ViewportGridService/ViewportGridService.js new file mode 100644 index 000000000..662d543b9 --- /dev/null +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.js @@ -0,0 +1,63 @@ +const name = 'ViewportGridService'; + +const publicAPI = { + name, + getState: _getState, + setActiveViewportIndex: _setActiveViewportIndex, + setDisplaysetForViewport: _setDisplaysetForViewport, + setLayout: _setLayout, + setServiceImplementation, +}; + +const serviceImplementation = { + _getState: () => console.warn('getState() NOT IMPLEMENTED'), + _setActiveViewportIndex: () => console.warn('setActiveViewportIndex() NOT IMPLEMENTED'), + _setDisplaysetForViewport: () => console.warn('setDisplaysetForViewport() NOT IMPLEMENTED'), + _setLayout: () => console.warn('setLayout() NOT IMPLEMENTED'), +}; + +function _getState() { + return serviceImplementation._getState(); +} + +function _setActiveViewportIndex(index) { + return serviceImplementation._setActiveViewportIndex(index); +} + +function _setDisplaysetForViewport({ viewportIndex, displaySetInstanceUID }) { + return serviceImplementation._setDisplaysetForViewport({ + viewportIndex, + displaySetInstanceUID, + }); +} + +function _setLayout({ numCols, numRows }) { + return serviceImplementation._setLayout({ numCols, numRows }); +} + +function setServiceImplementation({ + getState: getStateImplementation, + setActiveViewportIndex: setActiveViewportIndexImplementation, + setDisplaysetForViewport: setDisplaysetForViewportImplementation, + setLayout: setLayoutImplementation, +}) { + if (getStateImplementation) { + serviceImplementation._getState = getStateImplementation; + } + if (setActiveViewportIndexImplementation) { + serviceImplementation._setActiveViewportIndex = setActiveViewportIndexImplementation; + } + if (setDisplaysetForViewportImplementation) { + serviceImplementation._setDisplaysetForViewport = setDisplaysetForViewportImplementation; + } + if (setLayoutImplementation) { + serviceImplementation._setLayout = setLayoutImplementation; + } +} + +export default { + name, + create: ({ configuration = {} }) => { + return publicAPI; + }, +}; diff --git a/platform/core/src/services/ViewportGridService/index.js b/platform/core/src/services/ViewportGridService/index.js index e69de29bb..47d083531 100644 --- a/platform/core/src/services/ViewportGridService/index.js +++ b/platform/core/src/services/ViewportGridService/index.js @@ -0,0 +1,3 @@ +import ViewportGridService from './ViewportGridService'; + +export default ViewportGridService; diff --git a/platform/core/src/services/index.js b/platform/core/src/services/index.js index 96ecfb4d5..3862a2554 100644 --- a/platform/core/src/services/index.js +++ b/platform/core/src/services/index.js @@ -7,6 +7,7 @@ import UIViewportDialogService from './UIViewportDialogService'; import DicomMetadataStore from './DicomMetadataStore'; import DisplaySetService from './DisplaySetService'; import ToolBarSerivce from './ToolBarService'; +import ViewportGridService from './ViewportGridService'; export { MeasurementService, @@ -18,4 +19,5 @@ export { DicomMetadataStore, DisplaySetService, ToolBarSerivce, + ViewportGridService, }; diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.jsx b/platform/ui/src/contextProviders/ViewportGridProvider.jsx index c56268cc4..fe77ef0a0 100644 --- a/platform/ui/src/contextProviders/ViewportGridProvider.jsx +++ b/platform/ui/src/contextProviders/ViewportGridProvider.jsx @@ -1,20 +1,133 @@ -import React, { createContext, useContext, useReducer } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useReducer, +} from 'react'; +import PropTypes from 'prop-types'; -// export const VIEWPORT_GRID_DEFAULT_VALUE = { -// numCols: 1, -// numRows: 1, -// activeViewportIndex: 0, -// viewports: [], -// }; +const DEFAULT_STATE = { + numRows: 1, + numCols: 1, + viewports: [], + activeViewportIndex: 0, +}; -export const ViewportGridContext = createContext(); +export const ViewportGridContext = createContext(DEFAULT_STATE); + +export function ViewportGridProvider({ children, service }) { + const viewportGridReducer = (state, action) => { + switch (action.type) { + case 'SET_ACTIVE_VIEWPORT_INDEX': + return { ...state, ...{ activeViewportIndex: action.payload } }; + case 'SET_DISPLAYSET_FOR_VIEWPORT': { + const { viewportIndex, displaySetInstanceUID } = action.payload; + const viewports = state.viewports.slice(); + + viewports[viewportIndex] = { displaySetInstanceUID }; + + return { ...state, ...{ viewports } }; + } + case 'SET_LAYOUT': { + const { numCols, numRows } = action.payload; + const numPanes = numCols * numRows; + const viewports = state.viewports.slice(); + const activeViewportIndex = + state.activeViewportIndex >= numPanes ? 0 : state.activeViewportIndex; + + while (viewports.length < numPanes) { + viewports.push({}); + } + while (viewports.length > numPanes) { + viewports.pop(); + } + + return { + ...state, + ...{ activeViewportIndex, numCols, numRows, viewports }, + }; + } + default: + return action.payload; + } + }; + + const [viewportGridState, dispatch] = useReducer( + viewportGridReducer, + DEFAULT_STATE, + ); + + const getState = useCallback(() => viewportGridState, [viewportGridState]); + const setActiveViewportIndex = useCallback( + index => dispatch({ type: 'SET_ACTIVE_VIEWPORT_INDEX', payload: index }), + [dispatch] + ); + const setDisplaysetForViewport = useCallback( + ({ viewportIndex, displaySetInstanceUID }) => + dispatch({ + type: 'SET_DISPLAYSET_FOR_VIEWPORT', + payload: { + viewportIndex, + displaySetInstanceUID, + }, + }), + [dispatch] + ); + + const setLayout = useCallback( + ({ numCols, numRows }) => + dispatch({ + type: 'SET_LAYOUT', + payload: { + numCols, + numRows, + }, + }), + [dispatch] + ); + + /** + * Sets the implementation of a modal service that can be used by extensions. + * + * @returns void + */ + useEffect(() => { + if (service) { + service.setServiceImplementation({ + getState, + setActiveViewportIndex, + setDisplaysetForViewport, + setLayout, + }); + } + }, [ + getState, + service, + setActiveViewportIndex, + setDisplaysetForViewport, + setLayout, + ]); + + const api = { + // getState, + setActiveViewportIndex, + setDisplaysetForViewport, + setLayout, + }; -export function ViewportGridProvider({ reducer, initialState, children }) { return ( - + {children} ); } +ViewportGridProvider.propTypes = { + children: PropTypes.any, + service: PropTypes.shape({ + setServiceImplementation: PropTypes.func, + }).isRequired, +}; + export const useViewportGrid = () => useContext(ViewportGridContext); diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 420695713..5ab382d4f 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -55,61 +55,15 @@ function App({ config, defaultExtensions }) { UIModalService, UINotificationService, UIViewportDialogService, + ViewportGridService, // TODO: Should this be a "UI" Service? } = servicesManager.services; - // A UI Service may need to use the ViewportGrid context - const viewportGridReducer = (state, action) => { - console.log(state, action); - - switch (action.type) { - case 'SET_ACTIVE_VIEWPORT_INDEX': - return { ...state, ...{ activeViewportIndex: action.payload } }; - case 'SET_DISPLAYSET_FOR_VIEWPORT': { - const { viewportIndex, displaySetInstanceUID } = action.payload; - const viewports = state.viewports.slice(); - - viewports[viewportIndex] = { displaySetInstanceUID }; - - return { ...state, ...{ viewports } }; - } - case 'SET_LAYOUT': { - const { numCols, numRows } = action.payload; - const numPanes = numCols * numRows; - const viewports = state.viewports.slice(); - const activeViewportIndex = - state.activeViewportIndex >= numPanes ? 0 : state.activeViewportIndex; - - while (viewports.length < numPanes) { - viewports.push({}); - } - while (viewports.length > numPanes) { - viewports.pop(); - } - - return { - ...state, - ...{ activeViewportIndex, numCols, numRows, viewports }, - }; - } - default: - return action.payload; - } - }; - return ( - + diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index ef8c16641..2f5433f54 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -10,6 +10,7 @@ import { MeasurementService, DisplaySetService, ToolBarSerivce, + ViewportGridService, // utils, // redux as reduxOHIF, } from '@ohif/core'; @@ -51,6 +52,7 @@ function appInit(appConfigOrFunc, defaultExtensions) { MeasurementService, DisplaySetService, ToolBarSerivce, + ViewportGridService, ]); /** diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index 2f99f7b48..f82fc5f18 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -12,11 +12,11 @@ function ViewerViewportGrid(props) { const { servicesManager, viewportComponents, dataSource } = props; const [ { numCols, numRows, activeViewportIndex, viewports }, - dispatch, + viewportGridService, ] = useViewportGrid(); const setActiveViewportIndex = index => { - dispatch({ type: 'SET_ACTIVE_VIEWPORT_INDEX', payload: index }); + viewportGridService.setActiveViewportIndex(index); }; // TODO -> Need some way of selecting which displaySets hit the viewports. @@ -96,12 +96,9 @@ function ViewerViewportGrid(props) { // ]); const onDropHandler = (viewportIndex, { displaySetInstanceUID }) => { console.warn(`DROPPED: ${displaySetInstanceUID}`); - dispatch({ - type: 'SET_DISPLAYSET_FOR_VIEWPORT', - payload: { - viewportIndex, - displaySetInstanceUID, - }, + viewportGridService.setDisplaysetForViewport({ + viewportIndex, + displaySetInstanceUID, }); };