WIP DisplaySetService
This commit is contained in:
1 parent
649216fde4
commit
7a8418f64b
29 files changed
+460
-541
No files matched your search
@@ -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 (
|
||||
<appConfigContext.Provider value={appConfigContextApi}>
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
<ViewportPane
|
||||
key={viewportIndex}
|
||||
className="m-1"
|
||||
onDrop={() => {
|
||||
/* setDisplaySet for Viewport */
|
||||
// Pass in as prop?
|
||||
acceptDropsFor="displayset"
|
||||
onDrop={droppedItem => {
|
||||
console.warn('DROPPED ITEM:', droppedItem);
|
||||
}}
|
||||
isActive={activeViewportIndex === viewportIndex}
|
||||
></ViewportPane>
|
||||
|
||||
@@ -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;
|
||||
@@ -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 (
|
||||
<ViewportPane
|
||||
onDrop={setViewportData}
|
||||
viewportIndex={viewportIndex} // Needed by `setViewportData`
|
||||
className={classNames('viewport-container', {
|
||||
active: activeViewportIndex === viewportIndex,
|
||||
})}
|
||||
key={viewportIndex}
|
||||
>
|
||||
{ViewportComponent}
|
||||
</ViewportPane>
|
||||
);
|
||||
});
|
||||
|
||||
const ViewportPanes = React.useMemo(getViewportPanes, [
|
||||
layout,
|
||||
viewportData,
|
||||
studies,
|
||||
children,
|
||||
availablePlugins,
|
||||
defaultPluginName,
|
||||
setViewportData,
|
||||
activeViewportIndex,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cy="viewprt-grid"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: `repeat(${numRows}, ${rowSize}%)`,
|
||||
gridTemplateColumns: `repeat(${numColumns}, ${colSize}%)`,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{ViewportPanes}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<ViewportComponent
|
||||
viewportData={viewportData}
|
||||
viewportIndex={viewportIndex}
|
||||
children={[children]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <EmptyViewport />;
|
||||
}
|
||||
|
||||
export default ViewportGrid;
|
||||
@@ -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 (
|
||||
<div
|
||||
className={classNames(
|
||||
'viewport-drop-target',
|
||||
{ hovered: hovered },
|
||||
{ highlighted: highlighted },
|
||||
propClassName
|
||||
)}
|
||||
ref={drop}
|
||||
data-cy={`viewport-container-${viewportIndex}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ViewportPane.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
onDrop: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ViewportPane;
|
||||
@@ -11,6 +11,7 @@ export default function DisplaySetCreator({
|
||||
mode,
|
||||
dataSourceName,
|
||||
extensionManager,
|
||||
DisplaySetService,
|
||||
}) {
|
||||
console.warn('DisplaySetCreator rerendering');
|
||||
const { routes, sopClassHandlers } = mode;
|
||||
|
||||
@@ -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 (
|
||||
<React.Fragment>
|
||||
<ToolbarLayoutProvider>
|
||||
<DisplaySetCreator
|
||||
location={location}
|
||||
mode={mode}
|
||||
dataSourceName={dataSourceName}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
<CombinedContextProvider>
|
||||
{/* TODO: extensionManager is already provided to the extension module.
|
||||
* Use it from there instead of passing as a prop here.
|
||||
*/}
|
||||
<DragAndDropProvider>
|
||||
<LayoutComponent
|
||||
extensionManager={extensionManager}
|
||||
ViewportGrid={ViewportGridWithDataSource}
|
||||
{...layoutTemplateData.props}
|
||||
/>
|
||||
</DragAndDropProvider>
|
||||
</CombinedContextProvider>
|
||||
</ToolbarLayoutProvider>
|
||||
{/*<ToolbarLayoutProvider>*/}
|
||||
{/*
|
||||
<DisplaySetCreator
|
||||
location={location}
|
||||
mode={mode}
|
||||
dataSourceName={dataSourceName}
|
||||
extensionManager={extensionManager}
|
||||
DisplaySetService={DisplaySetService}
|
||||
/>
|
||||
*/}
|
||||
<CombinedContextProvider>
|
||||
{/* TODO: extensionManager is already provided to the extension module.
|
||||
* Use it from there instead of passing as a prop here.
|
||||
*/}
|
||||
<DragAndDropProvider>
|
||||
<LayoutComponent {...layoutTemplateData.props}>
|
||||
<ViewportGridWithDataSource />
|
||||
</LayoutComponent>
|
||||
</DragAndDropProvider>
|
||||
</CombinedContextProvider>
|
||||
{/*</ToolbarLayoutProvider>*/}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</ViewModelProvider>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Switch>
|
||||
|
||||
Reference in new issue
Block a user