WIP DisplaySetService

This commit is contained in:
James A. Petts 2020-05-18 16:21:57 +01:00
parent 649216fde4
commit 7a8418f64b
29 changed files with 460 additions and 541 deletions

View File

@ -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({

View File

@ -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';

View File

@ -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 = (
<React.Fragment>

View File

@ -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 (
<div>
<ViewportDataCreator setViewportData={setViewportData}/>
<Header
tools={primaryToolBarLayout.tools}
moreTools={primaryToolBarLayout.moreTools}
@ -102,8 +105,8 @@ function ViewerLayout({
<Toolbar type="secondary" tools={secondaryToolBarLayout.tools} />
</div>
<div className="flex flex-1 h-full overflow-hidden bg-black items-center justify-center pb-2 pt-1">
<ViewportGrid
viewportData={viewportData}
<children.ViewportGrid
servicesManager={servicesManager}
viewportComponents={viewportComponents}
/>
{/*
@ -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 = {

View File

@ -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,
},
];
}

View File

@ -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';

View File

@ -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.
}
}

View File

@ -3,8 +3,6 @@ import {
addInstancesToStudy,
} from './studies/services/wado/studyInstanceHelpers';
import displaySetManager from './displaySetManager';
class StudyMetadata {
constructor(StudyInstanceUID) {
this.StudyInstanceUID = StudyInstanceUID;

View File

@ -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;

View File

@ -184,6 +184,7 @@ export default class ExtensionManager {
appConfig: this._appConfig,
configuration,
api: this._api,
extensionManager: this,
});
if (!extensionModule) {

View File

@ -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,

View File

@ -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);
}
}
}
};
}

View File

@ -0,0 +1,8 @@
import DisplaySetService from './DisplaySetService';
export default {
name: 'DisplaySetService',
create: ({ configuration = {} }) => {
return new DisplaySetService();
},
};

View File

@ -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,
};

View File

@ -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 && (
<ThumbnailList
dragData={{ type: 'displayset' }}
thumbnails={displaySets}
thumbnailActive={thumbnailActive}
onThumbnailClick={thumbnailId => {
@ -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 <ThumbnailList> 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,

View File

@ -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 (
<div className="bg-black py-3">
{thumbnails.map(
@ -27,6 +32,7 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
return (
<Thumbnail
key={displaySetInstanceUID}
dragData={dragData}
description={description}
seriesNumber={seriesNumber}
numInstances={numInstances}
@ -41,6 +47,7 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
return (
<ThumbnailNoImage
key={displaySetInstanceUID}
dragData={dragData}
modality={modality}
seriesDate={seriesDate}
description={description}
@ -51,6 +58,7 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
return (
<ThumbnailTracked
key={displaySetInstanceUID}
dragData={dragData}
description={description}
seriesNumber={seriesNumber}
numInstances={numInstances}
@ -72,6 +80,17 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
};
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,

View File

@ -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
<Playground>
<DragAndDropProvider>
<div className="p-4 h-56">
<ViewportGrid numRows={1} numCols={2}>
<div className="flex items-center justify-center h-full text-white">
@ -30,6 +31,7 @@ import { ViewportGrid } from '@ohif/ui';
</div>
</ViewportGrid>
</div>
</DragAndDropProvider>
</Playground>
## Properties

View File

@ -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,
};

View File

@ -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 (
<div>
<Header />
<div
className="flex flex-row flex-no-wrap flex-1 items-stretch overflow-hidden w-full"
style={{ height: 'calc(100vh - 57px' }}
>
<SidePanel
side="left"
iconName="group-layers"
iconLabel="Studies"
componentLabel="Studies"
defaultIsOpen={true}
<DragAndDropProvider>
<div>
<Header />
<div
className="flex flex-row flex-no-wrap flex-1 items-stretch overflow-hidden w-full"
style={{ height: 'calc(100vh - 57px' }}
>
<StudyBrowser />
</SidePanel>
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
<ViewportToolbar />
<div>CONTENT</div>
</div>
<SidePanel
side="right"
iconName="list-bullets"
iconLabel="Measure"
componentLabel="Measurements"
defaultIsOpen={false}
>
<div className="flex justify-center text-white p-2">
panel placeholder
<SidePanel
side="left"
iconName="group-layers"
iconLabel="Studies"
componentLabel="Studies"
defaultIsOpen={true}
>
<StudyBrowser />
</SidePanel>
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
<ViewportToolbar />
<div>CONTENT</div>
</div>
</SidePanel>
<SidePanel
side="right"
iconName="list-bullets"
iconLabel="Measure"
componentLabel="Measurements"
defaultIsOpen={false}
>
<div className="flex justify-center text-white p-2">
panel placeholder
</div>
</SidePanel>
</div>
</div>
</div>
</DragAndDropProvider>
);
};

View File

@ -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}>

View File

@ -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,
]);
/**

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -11,6 +11,7 @@ export default function DisplaySetCreator({
mode,
dataSourceName,
extensionManager,
DisplaySetService,
}) {
console.warn('DisplaySetCreator rerendering');
const { routes, sopClassHandlers } = mode;

View File

@ -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,

View File

@ -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>
);

View File

@ -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>