diff --git a/extensions/default/src/Panels/PanelStudyBrowser.jsx b/extensions/default/src/Panels/PanelStudyBrowser.jsx index ea705cabc..97564c115 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.jsx @@ -51,7 +51,7 @@ function PanelStudyBrowser({ // ~~ Initial Thumbnails useEffect(() => { - const currentDisplaySets = DisplaySetService.activeDisplaySets || []; + const currentDisplaySets = DisplaySetService.activeDisplaySets; currentDisplaySets.forEach(async dSet => { const newImageSrcEntry = {}; const displaySet = DisplaySetService.getDisplaySetByUID( @@ -71,7 +71,7 @@ function PanelStudyBrowser({ // ~~ displaySets useEffect(() => { // TODO: Are we sure `activeDisplaySets` will always be accurate? - const currentDisplaySets = DisplaySetService.activeDisplaySets || []; + const currentDisplaySets = DisplaySetService.activeDisplaySets; const mappedDisplaySets = _mapDisplaySets( currentDisplaySets, thumbnailImageSrcMap @@ -164,6 +164,7 @@ function PanelStudyBrowser({ PanelStudyBrowser.propTypes = { DisplaySetService: PropTypes.shape({ EVENTS: PropTypes.object.isRequired, + activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired, getDisplaySetByUID: PropTypes.func.isRequired, hasDisplaySetsForStudy: PropTypes.func.isRequired, subscribe: PropTypes.func.isRequired, diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js index 55bcf70f2..8fb176959 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js @@ -72,11 +72,16 @@ const BaseImplementation = { study = _model.studies[_model.studies.length - 1]; } - study.addSeries(instances); - this._broadcastEvent(EVENTS.INSTANCES_ADDED, { - StudyInstanceUID, - SeriesInstanceUID, - }); + // TODO: Worth identifying why this is being called many times with series + // that are already "added"? + const didAddSeries = study.addSeries(instances); + + if (didAddSeries) { + this._broadcastEvent(EVENTS.INSTANCES_ADDED, { + StudyInstanceUID, + SeriesInstanceUID, + }); + } }, addStudy(study) { const { StudyInstanceUID } = study; diff --git a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js index d645218b1..3472215b6 100644 --- a/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js +++ b/platform/core/src/services/DicomMetadataStore/createStudyMetadata.js @@ -4,9 +4,24 @@ function createStudyMetadata(StudyInstanceUID) { return { StudyInstanceUID, series: [], + /** + * + * @param {object[]} instances + * @param {string} instances[].SeriesInstanceUID + * @returns {bool} true if series were added; false if series already exist + */ addSeries: function(instances) { - const series = createSeriesMetadata(instances); - this.series.push(series); + const { SeriesInstanceUID } = instances[0]; + const seriesExists = this.series.some( + s => s.SeriesInstanceUID === SeriesInstanceUID + ); + + if (!seriesExists) { + const series = createSeriesMetadata(instances); + this.series.push(series); + } + + return !seriesExists; }, }; } diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.js b/platform/core/src/services/DisplaySetService/DisplaySetService.js index b09aa8c03..d9275c648 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.js +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.js @@ -6,6 +6,7 @@ const displaySetCache = []; export default class DisplaySetService { constructor() { this.displaySets = {}; + this.activeDisplaySets = []; this.listeners = {}; this.EVENTS = EVENTS; @@ -77,9 +78,10 @@ export default class DisplaySetService { displaySetsAdded = displaySets; } + // TODO: This is tricky. How do we know we're not resetting to the same/existing DSs? + // TODO: This is likely run anytime we touch DicomMetadataStore. How do we prevent uneccessary broadcasts? if (displaySetsAdded && displaySetsAdded.length) { this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, displaySetsAdded); - this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); } }; diff --git a/platform/core/src/services/_shared/pubSubServiceInterface.js b/platform/core/src/services/_shared/pubSubServiceInterface.js index 1719be1ca..9a22a3ccb 100644 --- a/platform/core/src/services/_shared/pubSubServiceInterface.js +++ b/platform/core/src/services/_shared/pubSubServiceInterface.js @@ -13,7 +13,7 @@ export default { }; /** - * Subscribe to measurement updates. + * Subscribe to updates. * * @param {string} eventName The name of the event * @param {Function} callback Events callback @@ -24,7 +24,7 @@ function subscribe(eventName, callback) { const listenerId = guid(); const subscription = { id: listenerId, callback }; - console.info(`displaySetService: Subscribing to '${eventName}'.`); + console.info(`Subscribing to '${eventName}'.`); if (Array.isArray(this.listeners[eventName])) { this.listeners[eventName].push(subscription); } else { @@ -60,7 +60,7 @@ function _unsubscribe(eventName, listenerId) { } /** - * Check if a given displaySetService event is valid. + * Check if a given event is valid. * * @param {string} eventName The name of the event * @return {boolean} Event name validation @@ -70,7 +70,7 @@ function _isValidEvent(eventName) { } /** - * Broadcasts displaySetService changes. + * Broadcasts changes. * * @param {string} eventName - The event name * @param {func} callbackProps - Properties to pass callback diff --git a/platform/ui/index.js b/platform/ui/index.js index 7c33669f1..827147317 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -14,6 +14,9 @@ export { ImageViewerContext, ImageViewerProvider, useImageViewer, + ViewportGridContext, + ViewportGridProvider, + useViewportGrid, } from './src/contextProviders'; /** COMPONENTS */ diff --git a/platform/ui/src/contextProviders/ViewportGridProvider.jsx b/platform/ui/src/contextProviders/ViewportGridProvider.jsx new file mode 100644 index 000000000..c56268cc4 --- /dev/null +++ b/platform/ui/src/contextProviders/ViewportGridProvider.jsx @@ -0,0 +1,20 @@ +import React, { createContext, useContext, useReducer } from 'react'; + +// export const VIEWPORT_GRID_DEFAULT_VALUE = { +// numCols: 1, +// numRows: 1, +// activeViewportIndex: 0, +// viewports: [], +// }; + +export const ViewportGridContext = createContext(); + +export function ViewportGridProvider({ reducer, initialState, children }) { + return ( + + {children} + + ); +} + +export const useViewportGrid = () => useContext(ViewportGridContext); diff --git a/platform/ui/src/contextProviders/index.js b/platform/ui/src/contextProviders/index.js index d879ca9ef..571ce4294 100644 --- a/platform/ui/src/contextProviders/index.js +++ b/platform/ui/src/contextProviders/index.js @@ -16,4 +16,10 @@ export { useImageViewer, } from './ImageViewerProvider'; +export { + ViewportGridContext, + ViewportGridProvider, + useViewportGrid, +} from './ViewportGridProvider'; + export { default as DragAndDropProvider } from './DragAndDropProvider'; diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index e85fea0cc..28ab3a4e9 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -6,7 +6,7 @@ import { ThemeWrapper } from '@ohif/ui'; // Viewer Project // TODO: Should this influence study list? import { appConfigContext } from '@state/appConfig.context'; -import { useAppConfig } from './hooks/useAppConfig'; +import { useAppConfig } from '@hooks/useAppConfig'; import createRoutes from './routes'; import appInit from './appInit.js'; diff --git a/platform/viewer/src/components/EmptyViewport.jsx b/platform/viewer/src/components/EmptyViewport.jsx new file mode 100644 index 000000000..70c684cfc --- /dev/null +++ b/platform/viewer/src/components/EmptyViewport.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +function EmptyViewport() { + return ( +
+

Please drag a stack here to view images.

+
+ ); +} + +export default EmptyViewport; diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index 20b4b54d7..e58bf8bbd 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -1,19 +1,17 @@ /** * CSS Grid Reference: http://grid.malven.co/ */ -import React, { useState, useCallback, useEffect } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { ViewportGrid, ViewportPane } from '@ohif/ui'; -// import DefaultViewport from './DefaultViewport.js'; -// import EmptyViewport from './EmptyViewport.js'; +import { ViewportGrid, ViewportPane, useViewportGrid } from '@ohif/ui'; +import EmptyViewport from './EmptyViewport'; function ViewerViewportGrid(props) { - const { - activeViewportIndex, - servicesManager, - viewportComponents, - dataSource, - } = props; + const { servicesManager, viewportComponents, dataSource } = props; + const [ + { numCols, numRows, activeViewportIndex, viewports }, + dispatch, + ] = useViewportGrid(); // TODO -> Need some way of selecting which displaySets hit the viewports. const { DisplaySetService } = servicesManager.services; @@ -23,8 +21,8 @@ function ViewerViewportGrid(props) { let displaySetInstanceUID; // Fallback - if(!displaySets || !displaySets.length) { - const displaySet = DisplaySetService.activeDisplaySets[0] + if (!displaySets || !displaySets.length) { + const displaySet = DisplaySetService.activeDisplaySets[0]; displaySetInstanceUID = displaySet.displaySetInstanceUID; } else { const displaySet = displaySets[0]; @@ -43,86 +41,97 @@ function ViewerViewportGrid(props) { }; }; - // From ViewportGridService and/or ContextProvider - const [viewportGrid, setViewportGrid] = useState({ - numCols: 1, - numRows: 1, - viewports: [] - }); + // TODO: + // Hmm... Should a "displaySet" being added update the viewport based on HP? + // I guess it might. + // This is where you would likely "fill" emptyViewports if none had content + // Or to recheck best placement/priority based on all activeDisplaySets + // useEffect(() => { + // const { unsubscribe } = DisplaySetService.subscribe( + // DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, + // displaySets => { + // displaySets.forEach(ds => console.log(`VPG:ADD::${ds.StudyInstanceUID}`)); + // const hp = HangingProtocolService(displaySets); + // viewportGrid.setViewportGrid(hp); + // }, + // ); - - useEffect(() => { - const { unsubscribe } = DisplaySetService.subscribe( - DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, - displaySets => { - setViewportGrid(HangingProtocolService(displaySets)); - }, - ); - - return unsubscribe; - }, []); + // return unsubscribe; + // }, []); // TODO: either need hover to change "active viewport" // so we can use it as our target for setting the displaySet, // or the dropHandler needs to know which viewport was dropped on // in event data - const onDropHandler = ({displaySetInstanceUID}) => { - const droppedDisplaySet = DisplaySetService.getDisplaySetByUID(displaySetInstanceUID); - const updatedViewportGridState = HangingProtocolService([droppedDisplaySet]); + const onDropHandler = ({ displaySetInstanceUID }) => { + const droppedDisplaySet = DisplaySetService.getDisplaySetByUID( + displaySetInstanceUID + ); + const updatedViewportGridState = HangingProtocolService([ + droppedDisplaySet, + ]); - console.warn('DROPPED: ', displaySetInstanceUID, droppedDisplaySet, updatedViewportGridState); + console.warn( + 'DROPPED: ', + displaySetInstanceUID, + droppedDisplaySet, + updatedViewportGridState + ); - // This is not updating the displayed DisplaySet - setViewportGrid(updatedViewportGridState); - } + dispatch({ type: 'action-name', payload: updatedViewportGridState }); + }; - // viewportData --> displaySets - const getViewportPanes = () => - viewportGrid.viewports.map((viewport, viewportIndex) => { - const displaySetInstanceUID = viewport.displaySetInstanceUID; - if (!displaySetInstanceUID) { - return null; - } + const getViewportPanes = () => { + const viewportPanes = []; + const numViewportPanes = numCols * numRows; - const displaySet = DisplaySetService.getDisplaySetByUID(displaySetInstanceUID); + for (let i = 0; i < numViewportPanes; i++) { + const viewportIndex = i; + const paneMeta = viewports[i]; + const isEmpty = !paneMeta || !paneMeta.displaySetInstanceUID; - // TODO: Better Empty Viewport - if (!displaySet) { - return ( + if (isEmpty) { + viewportPanes[i] = ( + > + + + ); + } else { + const displaySet = DisplaySetService.getDisplaySetByUID( + paneMeta.displaySetInstanceUID + ); + + const ViewportComponent = _getViewportComponent( + displaySet, + viewportComponents + ); + + viewportPanes[i] = ( + + + ); } + } - // TODO -> Need way for other viewport e.g. vtk to be used. - - const ViewportComponent = _getViewportComponent( - displaySet, - viewportComponents - ); - - return ( - - - - ); - }); + return viewportPanes; + }; // const ViewportPanes = React.useMemo(getViewportPanes, [ // viewportComp'onents, @@ -131,7 +140,7 @@ function ViewerViewportGrid(props) { // ]); return ( - + {/* {ViewportPanes} */} {getViewportPanes()} @@ -139,19 +148,11 @@ function ViewerViewportGrid(props) { } ViewerViewportGrid.propTypes = { - // viewports: PropTypes.array.isRequired, - activeViewportIndex: PropTypes.number.isRequired, viewportComponents: PropTypes.array.isRequired, - // numRows: PropTypes.number.isRequired, - // numColumns: PropTypes.number.isRequired, }; ViewerViewportGrid.defaultProps = { - // numRows: 1, - // numColumns: 1, - viewportData: [], viewportComponents: [], - activeViewportIndex: 0, }; function _getViewportComponent(displaySet, viewportComponents) { diff --git a/platform/viewer/src/routes/Mode/Mode.jsx b/platform/viewer/src/routes/Mode/Mode.jsx index 6bcde79d5..615bd5817 100644 --- a/platform/viewer/src/routes/Mode/Mode.jsx +++ b/platform/viewer/src/routes/Mode/Mode.jsx @@ -3,7 +3,11 @@ import { useParams } from 'react-router'; import PropTypes from 'prop-types'; // TODO: DicomMetadataStore should be injected? import { DicomMetadataStore, ToolBarManager } from '@ohif/core'; -import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui'; +import { + DragAndDropProvider, + ImageViewerProvider, + ViewportGridProvider, +} from '@ohif/ui'; // import { useQuery } from '@hooks'; import ViewportGrid from '@components/ViewportGrid'; @@ -119,23 +123,44 @@ export default function ModeRoute({ console.log(state, action); }; + const viewportGridReducer = (state, action) => { + console.log(state, action); + + switch (action.type) { + case 'DO_TODO': + return state; + default: + return action.payload; + } + }; + return ( - - {/* TODO: extensionManager is already provided to the extension module. - * Use it from there instead of passing as a prop here. - */} - - - - + + + {/* TODO: extensionManager is already provided to the extension module. + * Use it from there instead of passing as a prop here. + */} + + + + + ); } diff --git a/platform/viewer/src/state/ViewportGridProvider.js b/platform/viewer/src/state/ViewportGridProvider.js deleted file mode 100644 index 85fc225cc..000000000 --- a/platform/viewer/src/state/ViewportGridProvider.js +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useState, createContext, useContext } from 'react'; -import PropTypes from 'prop-types'; - -const ViewportGridContext = createContext(null); - -export const useViewportGrid = () => useContext(ViewportGridContext); - -export default function ViewportGridProvider({ children }) { - const [viewportGrid, setViewportGrid] = useState({ - rows: 1, - columns: 1, - viewports: [], - }); - - const get = () => { - return viewportGrid; - }; - - const set = ({ rows, columns, viewports }) => { - setViewportGrid({ rows, columns, viewports }); - }; - - return ( - - {/* {children} */} - - ); -} - -ViewportGridProvider.propTypes = { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.node), - PropTypes.node, - ]).isRequired, -}; - -export default ViewportGridProvider;