diff --git a/extensions/cornerstone/src/OHIFCornerstoneViewport.js b/extensions/cornerstone/src/OHIFCornerstoneViewport.js index 02156a991..8e567e89c 100644 --- a/extensions/cornerstone/src/OHIFCornerstoneViewport.js +++ b/extensions/cornerstone/src/OHIFCornerstoneViewport.js @@ -169,7 +169,7 @@ class OHIFCornerstoneViewport extends Component { const debouncedNewImageHandler = debounce( ({ currentImageIdIndex, sopInstanceUid }) => { - const { displaySet } = this.props.viewportData; + const { displaySet } = this.props; const { StudyInstanceUID } = displaySet; if (currentImageIdIndex > 0) { this.props.onNewImage({ diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 20ffe125a..2feb5d441 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -6,12 +6,7 @@ import { processResults, processSeriesResults, } from './qido.js'; -import { - dicomMetadataStore, - IWebApiDataSource, - utils, - displaySetManager, -} from '@ohif/core'; +import { dicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core'; import getImageId from './utils/getImageId'; import * as dcmjs from 'dcmjs'; diff --git a/extensions/default/src/MeasurementTable.js b/extensions/default/src/MeasurementTable.js index 56f306d69..7ba1e010e 100644 --- a/extensions/default/src/MeasurementTable.js +++ b/extensions/default/src/MeasurementTable.js @@ -10,7 +10,7 @@ import { export default function MeasurementTable({ servicesManager, commandsManager }) { const { MeasurementService } = servicesManager.services; - console.error('MeasurementTable rendering!!!!!!!!!!!!!'); + console.log('MeasurementTable rendering!!!!!!!!!!!!!'); const actionButtons = ( diff --git a/extensions/default/src/ViewerLayout/index.jsx b/extensions/default/src/ViewerLayout/index.jsx index e27ca087b..75e9b0ca5 100644 --- a/extensions/default/src/ViewerLayout/index.jsx +++ b/extensions/default/src/ViewerLayout/index.jsx @@ -1,33 +1,38 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { SidePanel, Toolbar } from '@ohif/ui'; -import { useToolbarLayout, useViewModel } from '@ohif/core'; +import { useToolbarLayout } from '@ohif/core'; // import Header from './Header.jsx'; import { displaySetManager } from '@ohif/core'; -function ViewportDataCreator({setViewportData}) { - const { displaySetInstanceUIDs } = useViewModel() - console.log(displaySetInstanceUIDs); +// function ViewportDataCreator({ setViewportData }) { +// const { displaySetInstanceUIDs } = useViewModel(); +// console.log(displaySetInstanceUIDs); - useEffect(() => { - setViewportData([ - displaySetManager.getDisplaySetByUID(displaySetInstanceUIDs[0]), - ]); - }, [displaySetInstanceUIDs, setViewportData]); +// useEffect(() => { +// setViewportData([ +// displaySetManager.getDisplaySetByUID(displaySetInstanceUIDs[0]), +// ]); +// }, [displaySetInstanceUIDs, setViewportData]); - return null; -} +// return null; +// } function ViewerLayout({ // From Extension Module Params extensionManager, + servicesManager, // From Modes leftPanels, rightPanels, viewports, - ViewportGrid, + children, }) { + const [displaySets, setDisplaySets] = useState({}); + + console.log(children); + debugger; /** * Set body classes (tailwindcss) that don't allow vertical * or horizontal overflow (no scrolling). Also guarantee window @@ -41,10 +46,6 @@ function ViewerLayout({ document.body.classList.remove('overflow-hidden'); }; }, []); - - // TODO -> Need some way of selecting which displaySets hit the viewports. - const [viewportData, setViewportData] = useState([]); - const getPanelData = id => { const entry = extensionManager.getModuleEntry(id); // TODO, not sure why sidepanel content has to be JSX, and not a children prop? @@ -72,16 +73,18 @@ function ViewerLayout({ const rightPanelComponents = rightPanels.map(getPanelData); const viewportComponents = viewports.map(getViewportComponentData); - let { toolBarLayout } = useToolbarLayout(); - if (!toolBarLayout.length) { - return null; - } + // let { toolBarLayout } = useToolbarLayout(); + // if (!toolBarLayout.length) { + // return null; + // } + + // TODO -> make toolbar service + const toolBarLayout = { tools: [], moreTools: [] }; const [primaryToolBarLayout, secondaryToolBarLayout] = toolBarLayout; return (
-
- {/* @@ -189,7 +192,7 @@ ViewerLayout.propTypes = { leftPanels: PropTypes.array, rightPanels: PropTypes.array, /** Responsible for rendering our grid of viewports; provided by consuming application */ - ViewportGrid: PropTypes.oneOfType(PropTypes.node, PropTypes.func).isRequired, + children: PropTypes.oneOfType(PropTypes.node, PropTypes.func).isRequired, }; ViewerLayout.defaultProps = { diff --git a/extensions/default/src/getLayoutTemplateModule.js b/extensions/default/src/getLayoutTemplateModule.js index 6df430358..595f83474 100644 --- a/extensions/default/src/getLayoutTemplateModule.js +++ b/extensions/default/src/getLayoutTemplateModule.js @@ -5,14 +5,18 @@ import ViewerLayout from './ViewerLayout'; - Init layout based on the displaySets and the objects. */ -export default function() { +export default function({ servicesManager, extensionManager }) { + function ViewerLayoutWithServices(props) { + return ViewerLayout({ servicesManager, extensionManager, ...props }); + } + return [ // Layout Template Definition // TODO: this is weird naming { name: 'viewerLayout', id: 'viewerLayout', - component: ViewerLayout, + component: ViewerLayoutWithServices, }, ]; } diff --git a/extensions/default/src/getPanelModule.js b/extensions/default/src/getPanelModule.js index 48a567520..a4c05e0f1 100644 --- a/extensions/default/src/getPanelModule.js +++ b/extensions/default/src/getPanelModule.js @@ -1,11 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import { StudyBrowser } from '@ohif/ui'; -import { - dicomMetadataStore, - useViewModel, - useToolbarLayout, - displaySetManager, -} from '@ohif/core'; +import { dicomMetadataStore, useViewModel, useToolbarLayout } from '@ohif/core'; import MeasurementTable from './MeasurementTable.js'; diff --git a/platform/core/src/ToolBarManager.js b/platform/core/src/ToolBarManager.js index 6e8d55337..0c585d8d0 100644 --- a/platform/core/src/ToolBarManager.js +++ b/platform/core/src/ToolBarManager.js @@ -1,8 +1,7 @@ export default class toolBarManager { - constructor(extensionManager, setToolBarLayout) { + constructor(extensionManager) { this.buttons = {}; this.extensionManager = extensionManager; - this.viewModelContextSetToolBarLayout = setToolBarLayout; } addButtons(buttons) { @@ -42,6 +41,6 @@ export default class toolBarManager { toolBarLayout.push(toolBarDefinitions); }); - this.viewModelContextSetToolBarLayout(toolBarLayout); + // TODO -> Change this to a service. => emit an event to subscribers to update the toolbar layout. } } diff --git a/platform/core/src/dicomMetadataStore.js b/platform/core/src/dicomMetadataStore.js index eb824df25..bd4bfcd2f 100644 --- a/platform/core/src/dicomMetadataStore.js +++ b/platform/core/src/dicomMetadataStore.js @@ -3,8 +3,6 @@ import { addInstancesToStudy, } from './studies/services/wado/studyInstanceHelpers'; -import displaySetManager from './displaySetManager'; - class StudyMetadata { constructor(StudyInstanceUID) { this.StudyInstanceUID = StudyInstanceUID; diff --git a/platform/core/src/displaySetManager.js b/platform/core/src/displaySetManager.js deleted file mode 100644 index f1437d355..000000000 --- a/platform/core/src/displaySetManager.js +++ /dev/null @@ -1,95 +0,0 @@ -class DisplaySetManager { - constructor() { - this.displaySets = []; - } - - init(extensionManager, SOPClassHandlerIds, viewModelContext) { - this.extensionManager = extensionManager; - this.SOPClassHandlerIds = SOPClassHandlerIds; - - const { - displaySetInstanceUIDs, - setDisplaySetInstanceUIDs, - } = viewModelContext; - - this.displaySetInstanceUIDs = displaySetInstanceUIDs; - this.setDisplaySetInstanceUids = setDisplaySetInstanceUIDs; - - // Reset displaySetInstanceUIDs - this.setDisplaySetInstanceUids([]); - } - - _addDisplaySets(displaySets) { - //const displayInstanceUids = [...this.displaySetInstanceUIDs]; - - const addedDisplaySetUids = []; - - displaySets.forEach(displaySet => { - this.displaySets.push(displaySet); - - addedDisplaySetUids.push(displaySet.displaySetInstanceUID); - }); - - return addedDisplaySetUids; - } - - getDisplaySetsForSeries = SeriesInstanceUID => { - return this.displaySets.filter( - displaySet => displaySet.SeriesInstanceUID === SeriesInstanceUID - ); - }; - - getDisplaySetByUID = displaySetInstanceUID => { - return this.displaySets.find( - displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID - ); - }; - - makeDisplaySets = instances => { - if (!instances || !instances.length) { - throw new Error('No instances were provided.'); - } - - const instance = instances[0]; - - const existingDisplaySets = - this.getDisplaySetsForSeries(instance.SeriesInstanceUID) || []; - - const SOPClassHandlerIds = this.SOPClassHandlerIds; - - for (let i = 0; i < SOPClassHandlerIds.length; i++) { - const SOPClassHandlerId = SOPClassHandlerIds[i]; - const handler = this.extensionManager.getModuleEntry(SOPClassHandlerId); - - if (handler.sopClassUids.includes(instance.SOPClassUID)) { - let addedDisplaySetUids; - - // Check if displaySets are already created using this SeriesInstanceUID/SOPClassHandler pair. - const cachedDisplaySets = existingDisplaySets.filter( - displaySet => displaySet.SOPClassHandlerId === SOPClassHandlerId - ); - - if (cachedDisplaySets.length) { - addedDisplaySetUids = cachedDisplaySets.map( - displaySet => displaySet.displaySetInstanceUID - ); - } else { - const displaySets = handler.getDisplaySetsFromSeries(instances); - - addedDisplaySetUids = this._addDisplaySets(displaySets); - } - - this.displaySetInstanceUIDs = [ - ...addedDisplaySetUids, - ...this.displaySetInstanceUIDs, - ]; - - this.setDisplaySetInstanceUids(this.displaySetInstanceUIDs); - } - } - }; -} - -const displaySetManager = new DisplaySetManager(); - -export default displaySetManager; diff --git a/platform/core/src/extensions/ExtensionManager.js b/platform/core/src/extensions/ExtensionManager.js index a0492a69d..b8de97b20 100644 --- a/platform/core/src/extensions/ExtensionManager.js +++ b/platform/core/src/extensions/ExtensionManager.js @@ -184,6 +184,7 @@ export default class ExtensionManager { appConfig: this._appConfig, configuration, api: this._api, + extensionManager: this, }); if (!extensionModule) { diff --git a/platform/core/src/index.js b/platform/core/src/index.js index b42278007..a00f76d99 100644 --- a/platform/core/src/index.js +++ b/platform/core/src/index.js @@ -20,7 +20,6 @@ import studies from './studies/'; import ui from './ui'; import user from './user.js'; import dicomMetadataStore from './dicomMetadataStore'; -import displaySetManager from './displaySetManager'; import ToolBarManager from './ToolBarManager'; import { ViewModelProvider, useViewModel } from './ViewModelContext'; import { @@ -35,6 +34,7 @@ import { UIModalService, UINotificationService, UIViewportDialogService, + DisplaySetService, } from './services'; import IWebApiDataSource from './DataSources/IWebApiDataSource'; @@ -71,10 +71,11 @@ const OHIF = { UIModalService, UINotificationService, UIViewportDialogService, + DisplaySetService, MeasurementService, IWebApiDataSource, dicomMetadataStore, - displaySetManager, + // ToolBarManager, ViewModelProvider, useViewModel, @@ -111,10 +112,10 @@ export { UIModalService, UINotificationService, UIViewportDialogService, + DisplaySetService, MeasurementService, IWebApiDataSource, dicomMetadataStore, - displaySetManager, ToolBarManager, ViewModelProvider, useViewModel, diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.js b/platform/core/src/services/DisplaySetService/DisplaySetService.js new file mode 100644 index 000000000..4c3dd9baa --- /dev/null +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.js @@ -0,0 +1,173 @@ +import guid from '../../utils/guid'; + +const EVENTS = { + DISPLAY_SET_ADDED: 'event::displaySetService:displaySetAdded', +}; + +const displaySetCache = []; + +export default class DisplaySetService { + constructor() { + this.displaySets = {}; + this.EVENTS = EVENTS; + this.listeners = {}; + } + + init(extensionManager, SOPClassHandlerIds) { + this.extensionManager = extensionManager; + this.SOPClassHandlerIds = SOPClassHandlerIds; + this.activeDisplaySets = {}; + } + + /** + * Subscribe to measurement updates. + * + * @param {string} eventName The name of the event + * @param {Function} callback Events callback + * @return {Object} Observable object with actions + */ + subscribe(eventName, callback) { + debugger; + if (this._isValidEvent(eventName)) { + const listenerId = guid(); + const subscription = { id: listenerId, callback }; + + console.info(`displaySetService: Subscribing to '${eventName}'.`); + if (Array.isArray(this.listeners[eventName])) { + this.listeners[eventName].push(subscription); + } else { + this.listeners[eventName] = [subscription]; + } + + return { + unsubscribe: () => this._unsubscribe(eventName, listenerId), + }; + } else { + throw new Error(`Event ${eventName} not supported.`); + } + } + + /** + * Unsubscribe to measurement updates. + * + * @param {string} eventName The name of the event + * @param {string} listenerId The listeners id + * @return void + */ + _unsubscribe(eventName, listenerId) { + if (!this.listeners[eventName]) { + return; + } + + const listeners = this.listeners[eventName]; + if (Array.isArray(listeners)) { + this.listeners[eventName] = listeners.filter( + ({ id }) => id !== listenerId + ); + } else { + this.listeners[eventName] = undefined; + } + } + + /** + * Broadcasts displaySetService changes. + * + * @param {string} eventName The event name + * @return void + */ + _broadcastChange(eventName) { + const hasListeners = Object.keys(this.listeners).length > 0; + const hasCallbacks = Array.isArray(this.listeners[eventName]); + + if (hasListeners && hasCallbacks) { + this.listeners[eventName].forEach(listener => { + listener.callback(this.activeDisplaySets); + }); + } + } + + /** + * Check if a given displaySetService event is valid. + * + * @param {string} eventName The name of the event + * @return {boolean} Event name validation + */ + _isValidEvent(eventName) { + return Object.values(this.EVENTS).includes(eventName); + } + + _addDisplaySetsToCache(displaySets) { + displaySets.forEach(displaySet => { + displaySetCache.push(displaySet); + }); + } + + _addActiveDisplaySets(displaySets) { + const activeDisplaySets = this.activeDisplaySets; + + displaySets.forEach(displaySet => { + const { StudyInstanceUID } = displaySet; + + if (!Array.isArray(activeDisplaySets[StudyInstanceUID])) { + activeDisplaySets[StudyInstanceUID] = []; + } + + activeDisplaySets[StudyInstanceUID].push(displaySet); + }); + + console.log('DISPLAY_SET_ADDED'); + console.log(activeDisplaySets); + + this._broadcastChange(EVENTS.DISPLAY_SET_ADDED); + } + + getActiveDisplaySets() { + return this.activeDisplaySets; + } + + getDisplaySetsForSeries = SeriesInstanceUID => { + return displaySetCache.filter( + displaySet => displaySet.SeriesInstanceUID === SeriesInstanceUID + ); + }; + + getDisplaySetByUID = displaySetInstanceUid => { + return this.displaySets.find( + displaySet => displaySet.displaySetInstanceUid === displaySetInstanceUid + ); + }; + + makeDisplaySets = instances => { + if (!instances || !instances.length) { + throw new Error('No instances were provided.'); + } + + const instance = instances[0]; + + const existingDisplaySets = + this.getDisplaySetsForSeries(instance.SeriesInstanceUID) || []; + + const SOPClassHandlerIds = this.SOPClassHandlerIds; + + for (let i = 0; i < SOPClassHandlerIds.length; i++) { + const SOPClassHandlerId = SOPClassHandlerIds[i]; + const handler = this.extensionManager.getModuleEntry(SOPClassHandlerId); + + if (handler.sopClassUids.includes(instance.SOPClassUID)) { + // Check if displaySets are already created using this SeriesInstanceUID/SOPClassHandler pair. + const cachedDisplaySets = existingDisplaySets.filter( + displaySet => displaySet.SOPClassHandlerId === SOPClassHandlerId + ); + + if (cachedDisplaySets.length) { + this._addActiveDisplaySets(cachedDisplaySets); + } else { + const newDisplaySets = handler.getDisplaySetsFromSeries(instances); + + this._addDisplaySetsToCache(newDisplaySets); + this._addActiveDisplaySets(newDisplaySets); + } + } + } + }; +} diff --git a/platform/core/src/services/DisplaySetService/index.js b/platform/core/src/services/DisplaySetService/index.js new file mode 100644 index 000000000..4946d8e0e --- /dev/null +++ b/platform/core/src/services/DisplaySetService/index.js @@ -0,0 +1,8 @@ +import DisplaySetService from './DisplaySetService'; + +export default { + name: 'DisplaySetService', + create: ({ configuration = {} }) => { + return new DisplaySetService(); + }, +}; diff --git a/platform/core/src/services/index.js b/platform/core/src/services/index.js index 096283956..6487ed1c3 100644 --- a/platform/core/src/services/index.js +++ b/platform/core/src/services/index.js @@ -4,6 +4,7 @@ import UIDialogService from './UIDialogService'; import UIModalService from './UIModalService'; import UINotificationService from './UINotificationService'; import UIViewportDialogService from './UIViewportDialogService'; +import DisplaySetService from './DisplaySetService'; export { MeasurementService, @@ -12,4 +13,5 @@ export { UIModalService, UINotificationService, UIViewportDialogService, + DisplaySetService, }; diff --git a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx index 1b32c4cc2..0feab933b 100644 --- a/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx +++ b/platform/ui/src/components/StudyBrowser/StudyBrowser.jsx @@ -22,8 +22,16 @@ const getTrackedSeries = displaySets => { return trackedSeries; }; -const StudyBrowser = ({ tabs, activeTabName, onSetTabActive, onClickStudy, onClickThumbnail }) => { - const [tabActive, setTabActive] = useState(activeTabName || getInitialActiveTab(tabs)); +const StudyBrowser = ({ + tabs, + activeTabName, + onSetTabActive, + onClickStudy, + onClickThumbnail, +}) => { + const [tabActive, setTabActive] = useState( + activeTabName || getInitialActiveTab(tabs) + ); const [studyActive, setStudyActive] = useState(null); const [thumbnailActive, setThumbnailActive] = useState(null); @@ -66,6 +74,7 @@ const StudyBrowser = ({ tabs, activeTabName, onSetTabActive, onClickStudy, onCli /> {isActive && displaySets && ( { @@ -110,7 +119,7 @@ const StudyBrowser = ({ tabs, activeTabName, onSetTabActive, onClickStudy, onCli setStudyActive(null); if (onSetTabActive) { - onSetTabActive(name) + onSetTabActive(name); } }} > @@ -141,6 +150,21 @@ StudyBrowser.propTypes = { numInstances: PropTypes.number, modalities: PropTypes.string, description: PropTypes.string, + // These apply to each thumbnail; we should set Thumbnails + // As children and kill the component to + // make it easier to set this prop. + // Do much less nesting so we have a bit more control. + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), displaySets: PropTypes.arrayOf( PropTypes.shape({ displaySetInstanceUID: PropTypes.string.isRequired, diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx index 15416ecd5..0014a667a 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx @@ -3,7 +3,12 @@ import PropTypes from 'prop-types'; import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '@ohif/ui'; -const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => { +const ThumbnailList = ({ + dragData, + thumbnails, + thumbnailActive, + onThumbnailClick, +}) => { return (
{thumbnails.map( @@ -27,6 +32,7 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => { return ( { return ( { return ( { }; ThumbnailList.propTypes = { + /** + * Data the thumbnail should expose to a receiving drop target. Use a matching + * `dragData.type` to identify which targets can receive this draggable item. + * If this is not set, drag-n-drop will be disabled for this thumbnail. + * + * Ref: https://react-dnd.github.io/react-dnd/docs/api/use-drag#specification-object-members + */ + dragData: PropTypes.shape({ + /** Must match the "type" a dropTarget expects */ + type: PropTypes.string.isRequired, + }), thumbnails: PropTypes.arrayOf( PropTypes.shape({ displaySetInstanceUID: PropTypes.string.isRequired, diff --git a/platform/ui/src/components/ViewportGrid/ViewportGrid.mdx b/platform/ui/src/components/ViewportGrid/ViewportGrid.mdx index 9a9ed932e..392ffbe1a 100644 --- a/platform/ui/src/components/ViewportGrid/ViewportGrid.mdx +++ b/platform/ui/src/components/ViewportGrid/ViewportGrid.mdx @@ -5,7 +5,7 @@ route: components/viewportGrid --- import { Playground, Props } from 'docz'; -import { ViewportGrid } from '@ohif/ui'; +import { DragAndDropProvider, ViewportGrid } from '@ohif/ui'; # ViewportGrid @@ -20,6 +20,7 @@ import { ViewportGrid } from '@ohif/ui'; ## Basic usage +
@@ -30,6 +31,7 @@ import { ViewportGrid } from '@ohif/ui';
+
## Properties diff --git a/platform/ui/src/components/ViewportPane/ViewportPane.jsx b/platform/ui/src/components/ViewportPane/ViewportPane.jsx index 99f77b21a..3f848126c 100644 --- a/platform/ui/src/components/ViewportPane/ViewportPane.jsx +++ b/platform/ui/src/components/ViewportPane/ViewportPane.jsx @@ -6,20 +6,22 @@ import { useDrop } from 'react-dnd'; // NOTE: If we found a way to make `useDrop` conditional, // Or we provided a HOC of this component, we could provide // this UI without the DragAndDropContext dependency. -function ViewportPane({ children, className, isActive, onDrop }) { +function ViewportPane({ + children, + className, + isActive, + onDrop, + acceptDropsFor, +}) { const [{ isHovered, isHighlighted }, drop] = useDrop({ - accept: 'displayset', + accept: acceptDropsFor, // TODO: pass in as prop? - drop: (displaySet, monitor) => { + drop: (droppedItem, monitor) => { const canDrop = monitor.canDrop(); const isOver = monitor.isOver(); if (canDrop && isOver && onDrop) { - const { StudyInstanceUID, displaySetInstanceUID } = displaySet; - - onDrop({ - //viewportIndex, StudyInstanceUID, displaySetInstanceUID - }); + onDrop(droppedItem); } }, // Monitor, and collect props; returned as values by `useDrop` @@ -53,6 +55,8 @@ ViewportPane.propTypes = { className: PropTypes.string, /** Bool to show active styling */ isActive: PropTypes.bool.isRequired, + /** Indicates drag items we should accept for drops */ + acceptDropsFor: PropTypes.string.isRequired, /** Function that handles drop events */ onDrop: PropTypes.func.isRequired, }; diff --git a/platform/ui/src/views/Viewer/Viewer.js b/platform/ui/src/views/Viewer/Viewer.js index 2bb52a3e4..55629d2a8 100644 --- a/platform/ui/src/views/Viewer/Viewer.js +++ b/platform/ui/src/views/Viewer/Viewer.js @@ -1,43 +1,51 @@ import React from 'react'; -import { SidePanel, StudyBrowser } from '@ohif/ui'; +import { + SidePanel, + StudyBrowser, + DragAndDropProvider, + ViewportGrid, + ViewportPane, +} from '@ohif/ui'; import Header from './components/Header'; import ViewportToolbar from './components/ViewportToolBar'; const Viewer = () => { return ( -
-
-
- +
+
+
- - -
- -
CONTENT
-
- -
- panel placeholder + + + +
+ +
CONTENT
- + +
+ panel placeholder +
+
+
-
+ ); }; diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 722c6c035..cb122f9ff 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -7,7 +7,7 @@ import { ThemeWrapper } from '@ohif/ui'; // TODO: Should this influence study list? import { appConfigContext } from './state/appConfig.context'; import { useAppConfig } from './hooks/useAppConfig'; -import createAppRoutes from './routes'; +import createRoutes from './routes'; import appInit from './appInit.js'; // Temporarily for testing @@ -34,7 +34,12 @@ function App({ config, defaultExtensions }) { const appConfigContextApi = useAppConfig(init.appConfig); const { routerBasename, modes, dataSources } = appConfigContextApi.appConfig; // Use config to create routes - const appRoutes = createAppRoutes(modes, dataSources, extensionManager); + const appRoutes = createRoutes( + modes, + dataSources, + extensionManager, + servicesManager + ); return ( diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index 6ccf8feac..e47abcce5 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -7,6 +7,7 @@ import { UIModalService, UIDialogService, MeasurementService, + DisplaySetService, // utils, // redux as reduxOHIF, } from '@ohif/core'; @@ -43,6 +44,7 @@ function appInit(appConfigOrFunc, defaultExtensions) { UIModalService, UIDialogService, MeasurementService, + DisplaySetService, ]); /** diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx index e4defdc4f..f49fb433f 100644 --- a/platform/viewer/src/components/ViewportGrid.jsx +++ b/platform/viewer/src/components/ViewportGrid.jsx @@ -10,32 +10,48 @@ import { ViewportGrid, ViewportPane } from '@ohif/ui'; function ViewerViewportGrid(props) { const { activeViewportIndex, - viewportData, + servicesManager, viewportComponents, dataSource, } = props; - // From ViewportGridService and/or ContextProvider - const [viewportGrid, setViewportGrid] = useState({ - numRows: 2, - numCols: 2, - activeViewportIndex: 0, - viewports: [ - { - displaySetUid: undefined, - }, - { - displaySetUid: undefined, - }, - { - displaySetUid: undefined, - }, - { - displaySetUid: undefined, - }, - ], + debugger; + + // TODO -> Need some way of selecting which displaySets hit the viewports. + const { DisplaySetService } = servicesManager.services; + + // TODO -> Make a HangingProtocolService + const HangingProtocolService = displaySets => { + return { + numRows: 1, + numCols: 1, + activeViewportIndex: 0, + viewports: [ + { + displaySetInstanceUid: Object.keys(displaySets)[0][0] + .displaySetInstanceUid, + }, + ], + }; + }; + + const handleDisplaySetSubscription = useCallback(displaySets => { + setViewportGrid(HangingProtocolService(displaySets)); }); + useEffect(() => { + debugger; + const { unsubscribe } = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SET_ADDED, + handleDisplaySetSubscription + ); + + return unsubscribe; + }, []); + + // From ViewportGridService and/or ContextProvider + const [viewportGrid, setViewportGrid] = useState({}); + // viewportData --> displaySets const getViewportPanes = () => @@ -47,8 +63,10 @@ function ViewerViewportGrid(props) { { - /* setDisplaySet for Viewport */ + // Pass in as prop? + acceptDropsFor="displayset" + onDrop={droppedItem => { + console.warn('DROPPED ITEM:', droppedItem); }} isActive={activeViewportIndex === viewportIndex} > diff --git a/platform/viewer/src/components/ViewportGrid/ConnectedViewportGrid.js b/platform/viewer/src/components/ViewportGrid/ConnectedViewportGrid.js deleted file mode 100644 index 80ff5fa81..000000000 --- a/platform/viewer/src/components/ViewportGrid/ConnectedViewportGrid.js +++ /dev/null @@ -1,42 +0,0 @@ -import ViewportGrid from './ViewportGrid.js'; -import { MODULE_TYPES } from '@ohif/core'; -import { connect } from 'react-redux'; -import { extensionManager } from './../../App.jsx'; -import memoize from 'lodash/memoize'; - -const getAvailableViewportModules = memoize(viewportModules => { - const availableViewportModules = {}; - viewportModules.forEach(moduleDefinition => { - availableViewportModules[moduleDefinition.extensionId] = - moduleDefinition.module; - }); - return availableViewportModules; -}); - -const mapStateToProps = state => { - const viewportModules = extensionManager.modules[MODULE_TYPES.VIEWPORT]; - const availableViewportModules = getAvailableViewportModules(viewportModules); - - // TODO: Use something like state.plugins.defaultPlugin[MODULE_TYPES.VIEWPORT] - let defaultPlugin; - if (viewportModules.length) { - defaultPlugin = viewportModules[0].extensionId; - } - - const { numRows, numColumns, layout, activeViewportIndex } = state.viewports; - - return { - numRows, - numColumns, - layout, - activeViewportIndex, - // TODO: rename `availableViewportModules` - availablePlugins: availableViewportModules, - // TODO: rename `defaultViewportModule` - defaultPlugin, - }; -}; - -const ConnectedViewportGrid = connect(mapStateToProps, null)(ViewportGrid); - -export default ConnectedViewportGrid; diff --git a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js b/platform/viewer/src/components/ViewportGrid/ViewportGrid.js deleted file mode 100644 index 29bbb2ded..000000000 --- a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js +++ /dev/null @@ -1,207 +0,0 @@ -import './ViewportGrid.css'; - -import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { utils } from '@ohif/core'; -import { useSnackbarContext } from '@ohif/ui'; -// -import ViewportPane from './ViewportPane.js'; -import DefaultViewport from './DefaultViewport.js'; -import EmptyViewport from './EmptyViewport.js'; - -const { loadAndCacheDerivedDisplaySets } = utils; - -const ViewportGrid = function(props) { - const { - activeViewportIndex, - availablePlugins, - defaultPlugin: defaultPluginName, - layout, - numRows, - numColumns, - setViewportData, - studies, - viewportData, - children, - isStudyLoaded, - } = props; - - const rowSize = 100 / numRows; - const colSize = 100 / numColumns; - - // http://grid.malven.co/ - if (!viewportData || !viewportData.length) { - return null; - } - - const snackbar = useSnackbarContext(); - - useEffect(() => { - if (isStudyLoaded) { - viewportData.forEach(displaySet => { - const promises = loadAndCacheDerivedDisplaySets(displaySet, studies); - - promises.forEach(promise => { - promise.catch(error => { - snackbar.show({ - title: 'Error loading derived display set:', - message: error.message, - type: 'error', - autoClose: false, - }); - }); - }); - }); - } - }, [studies, viewportData, isStudyLoaded]); - - const getViewportPanes = () => - layout.viewports.map((layout, viewportIndex) => { - const displaySet = viewportData[viewportIndex]; - - if (!displaySet) { - return null; - } - - const data = { - displaySet, - studies, - }; - - // JAMES TODO: - - // Use whichever plugin is currently in use in the panel - // unless nothing is specified. If nothing is specified - // and the display set has a plugin specified, use that. - // - // TODO: Change this logic to: - // - Plugins define how capable they are of displaying a SopClass - // - When updating a panel, ensure that the currently enabled plugin - // in the viewport is capable of rendering this display set. If not - // then use the most capable available plugin - const pluginName = - !layout.plugin && displaySet && displaySet.plugin - ? displaySet.plugin - : layout.plugin; - - const ViewportComponent = _getViewportComponent( - data, // Why do we pass this as `ViewportData`, when that's not really what it is? - viewportIndex, - children, - availablePlugins, - pluginName, - defaultPluginName - ); - - return ( - - {ViewportComponent} - - ); - }); - - const ViewportPanes = React.useMemo(getViewportPanes, [ - layout, - viewportData, - studies, - children, - availablePlugins, - defaultPluginName, - setViewportData, - activeViewportIndex, - ]); - - return ( -
- {ViewportPanes} -
- ); -}; - -ViewportGrid.propTypes = { - viewportData: PropTypes.array.isRequired, - supportsDrop: PropTypes.bool.isRequired, - activeViewportIndex: PropTypes.number.isRequired, - layout: PropTypes.object.isRequired, - availablePlugins: PropTypes.object.isRequired, - setViewportData: PropTypes.func.isRequired, - studies: PropTypes.array, - children: PropTypes.node, - defaultPlugin: PropTypes.string, - numRows: PropTypes.number.isRequired, - numColumns: PropTypes.number.isRequired, -}; - -ViewportGrid.defaultProps = { - viewportData: [], - numRows: 1, - numColumns: 1, - layout: { - viewports: [{}], - }, - activeViewportIndex: 0, - supportsDrop: true, - availablePlugins: { - DefaultViewport, - }, - defaultPlugin: 'defaultViewportPlugin', -}; - -/** - * - * - * @param {*} plugin - * @param {*} viewportData - * @param {*} viewportIndex - * @param {*} children - * @returns - */ -function _getViewportComponent( - viewportData, - viewportIndex, - children, - availablePlugins, - pluginName, - defaultPluginName -) { - if (viewportData.displaySet) { - pluginName = pluginName || defaultPluginName; - const ViewportComponent = availablePlugins[pluginName]; - - if (!ViewportComponent) { - throw new Error( - `No Viewport Component available for name ${pluginName}. - Available plugins: ${JSON.stringify(availablePlugins)}` - ); - } - - return ( - - ); - } - - return ; -} - -export default ViewportGrid; diff --git a/platform/viewer/src/components/ViewportGrid/ViewportPane.js b/platform/viewer/src/components/ViewportGrid/ViewportPane.js deleted file mode 100644 index 15a6bcd65..000000000 --- a/platform/viewer/src/components/ViewportGrid/ViewportPane.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { useDrop } from 'react-dnd'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import './ViewportPane.css'; - -const ViewportPane = function(props) { - const { children, onDrop, viewportIndex, className: propClassName } = props; - const [{ hovered, highlighted }, drop] = useDrop({ - accept: 'thumbnail', - drop: (droppedItem, monitor) => { - const canDrop = monitor.canDrop(); - const isOver = monitor.isOver(); - - if (canDrop && isOver && onDrop) { - const { StudyInstanceUID, displaySetInstanceUID } = droppedItem; - - onDrop({ viewportIndex, StudyInstanceUID, displaySetInstanceUID }); - } - }, - // Monitor, and collect props. - // Returned as values by `useDrop` - collect: monitor => ({ - highlighted: monitor.canDrop(), - hovered: monitor.isOver(), - }), - }); - - return ( -
- {children} -
- ); -}; - -ViewportPane.propTypes = { - children: PropTypes.node.isRequired, - viewportIndex: PropTypes.number.isRequired, - onDrop: PropTypes.func.isRequired, - className: PropTypes.string, -}; - -export default ViewportPane; diff --git a/platform/viewer/src/routes/Mode/DisplaySetCreator.jsx b/platform/viewer/src/routes/Mode/DisplaySetCreator.jsx index 3375bee98..19b24647b 100644 --- a/platform/viewer/src/routes/Mode/DisplaySetCreator.jsx +++ b/platform/viewer/src/routes/Mode/DisplaySetCreator.jsx @@ -11,6 +11,7 @@ export default function DisplaySetCreator({ mode, dataSourceName, extensionManager, + DisplaySetService, }) { console.warn('DisplaySetCreator rerendering'); const { routes, sopClassHandlers } = mode; diff --git a/platform/viewer/src/routes/Mode/Mode.jsx b/platform/viewer/src/routes/Mode/Mode.jsx index 47925c14b..0e8891bfd 100644 --- a/platform/viewer/src/routes/Mode/Mode.jsx +++ b/platform/viewer/src/routes/Mode/Mode.jsx @@ -1,27 +1,30 @@ -import React from 'react'; +import React, { useEffect, useCallback } from 'react'; import PropTypes from 'prop-types'; // -import { ToolbarLayoutProvider } from '@ohif/core'; +import { ToolBarManager } from '@ohif/core'; import { DragAndDropProvider } from '@ohif/ui'; // import ViewportGrid from '@components/ViewportGrid'; import Compose from './Compose'; -import DisplaySetCreator from './DisplaySetCreator'; +//import DisplaySetCreator from './DisplaySetCreator'; export default function ModeRoute({ location, mode, dataSourceName, extensionManager, + servicesManager, }) { console.warn('ModeRoute rerendering'); - const { routes, extensions } = mode; + const { routes, extensions, sopClassHandlers } = mode; const dataSources = extensionManager.getDataSources(dataSourceName); // TODO: For now assume one unique datasource. const dataSource = dataSources[0]; const route = routes[0]; + const { DisplaySetService } = servicesManager.services; + // Only handling one route per mode for now // You can test via http://localhost:3000/example-mode/dicomweb const layoutTemplateData = route.layoutTemplate({ location }); @@ -51,32 +54,59 @@ export default function ModeRoute({ const CombinedContextProvider = ({ children }) => Compose({ components: contextModuleProviders, children }); + debugger; + function ViewportGridWithDataSource(props) { + debugger; return ViewportGrid({ ...props, dataSource }); } + useEffect(() => { + // TODO -> Make this into a service + let toolBarManager = new ToolBarManager(extensionManager); //, setToolBarLayout); + route.init({ toolBarManager }); + }, [mode, dataSourceName, location]); + + const createDisplaySets = useCallback(() => { + // Add SOPClassHandlers to a new SOPClassManager. + DisplaySetService.init(extensionManager, sopClassHandlers); + + const queryParams = location.search; + + // Call the data source to start building the view model? + dataSource.retrieve.series.metadata( + queryParams, + DisplaySetService.makeDisplaySets + ); + }, [location]); + + useEffect(() => { + createDisplaySets(); + }, [mode, dataSourceName, location]); + 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. + */} + + + + + + + {/**/} ); } @@ -88,7 +118,7 @@ ModeRoute.propTypes = { pathname: PropTypes.string.isRequired, search: PropTypes.string.isRequired, hash: PropTypes.string.isRequired, - state: PropTypes.object.isRequired, + //state: PropTypes.object.isRequired, }), mode: PropTypes.object.isRequired, dataSourceName: PropTypes.string, diff --git a/platform/viewer/src/routes/buildModeRoutes.js b/platform/viewer/src/routes/buildModeRoutes.js index c12deefb8..07e7762b5 100644 --- a/platform/viewer/src/routes/buildModeRoutes.js +++ b/platform/viewer/src/routes/buildModeRoutes.js @@ -22,7 +22,15 @@ import { ViewModelProvider } from '@ohif/core'; /:modeId/:modeRoute/?queryParameters=example */ -export default function buildModeRoutes(modes, dataSources, extensionManager) { +export default function buildModeRoutes( + modes, + + dataSources, + + extensionManager, + + servicesManager +) { const routes = []; // const dataSources = Object.keys(extensionManager.dataSourceMap).map(a => @@ -52,6 +60,7 @@ export default function buildModeRoutes(modes, dataSources, extensionManager) { mode={mode} dataSourceName={dataSourceName} extensionManager={extensionManager} + servicesManager={servicesManager} /> ); diff --git a/platform/viewer/src/routes/index.js b/platform/viewer/src/routes/index.js index 63b6b4930..8869837e9 100644 --- a/platform/viewer/src/routes/index.js +++ b/platform/viewer/src/routes/index.js @@ -20,12 +20,26 @@ const bakedInRoutes = [ { component: NotFound }, ]; -const createRoutes = (modes, dataSources, extensionManager) => { - const routes = buildModeRoutes(modes, dataSources, extensionManager) || []; +const createRoutes = ( + modes, + dataSources, + extensionManager, + servicesManager +) => { + const routes = + buildModeRoutes(modes, dataSources, extensionManager, servicesManager) || + []; // TODO: Shouldn't need to guard input routes with an empty array? const allRoutes = [...(routes || []), ...bakedInRoutes]; - console.log('Creating Routes: ', modes, dataSources, routes, bakedInRoutes, allRoutes); + console.log( + 'Creating Routes: ', + modes, + dataSources, + routes, + bakedInRoutes, + allRoutes + ); return (