WIP DisplaySetService
This commit is contained in:
parent
649216fde4
commit
7a8418f64b
@ -169,7 +169,7 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
|
|
||||||
const debouncedNewImageHandler = debounce(
|
const debouncedNewImageHandler = debounce(
|
||||||
({ currentImageIdIndex, sopInstanceUid }) => {
|
({ currentImageIdIndex, sopInstanceUid }) => {
|
||||||
const { displaySet } = this.props.viewportData;
|
const { displaySet } = this.props;
|
||||||
const { StudyInstanceUID } = displaySet;
|
const { StudyInstanceUID } = displaySet;
|
||||||
if (currentImageIdIndex > 0) {
|
if (currentImageIdIndex > 0) {
|
||||||
this.props.onNewImage({
|
this.props.onNewImage({
|
||||||
|
|||||||
@ -6,12 +6,7 @@ import {
|
|||||||
processResults,
|
processResults,
|
||||||
processSeriesResults,
|
processSeriesResults,
|
||||||
} from './qido.js';
|
} from './qido.js';
|
||||||
import {
|
import { dicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core';
|
||||||
dicomMetadataStore,
|
|
||||||
IWebApiDataSource,
|
|
||||||
utils,
|
|
||||||
displaySetManager,
|
|
||||||
} from '@ohif/core';
|
|
||||||
|
|
||||||
import getImageId from './utils/getImageId';
|
import getImageId from './utils/getImageId';
|
||||||
import * as dcmjs from 'dcmjs';
|
import * as dcmjs from 'dcmjs';
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
export default function MeasurementTable({ servicesManager, commandsManager }) {
|
export default function MeasurementTable({ servicesManager, commandsManager }) {
|
||||||
const { MeasurementService } = servicesManager.services;
|
const { MeasurementService } = servicesManager.services;
|
||||||
|
|
||||||
console.error('MeasurementTable rendering!!!!!!!!!!!!!');
|
console.log('MeasurementTable rendering!!!!!!!!!!!!!');
|
||||||
|
|
||||||
const actionButtons = (
|
const actionButtons = (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
|||||||
@ -1,33 +1,38 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { SidePanel, Toolbar } from '@ohif/ui';
|
import { SidePanel, Toolbar } from '@ohif/ui';
|
||||||
import { useToolbarLayout, useViewModel } from '@ohif/core';
|
import { useToolbarLayout } from '@ohif/core';
|
||||||
//
|
//
|
||||||
import Header from './Header.jsx';
|
import Header from './Header.jsx';
|
||||||
import { displaySetManager } from '@ohif/core';
|
import { displaySetManager } from '@ohif/core';
|
||||||
|
|
||||||
function ViewportDataCreator({setViewportData}) {
|
// function ViewportDataCreator({ setViewportData }) {
|
||||||
const { displaySetInstanceUIDs } = useViewModel()
|
// const { displaySetInstanceUIDs } = useViewModel();
|
||||||
console.log(displaySetInstanceUIDs);
|
// console.log(displaySetInstanceUIDs);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
setViewportData([
|
// setViewportData([
|
||||||
displaySetManager.getDisplaySetByUID(displaySetInstanceUIDs[0]),
|
// displaySetManager.getDisplaySetByUID(displaySetInstanceUIDs[0]),
|
||||||
]);
|
// ]);
|
||||||
}, [displaySetInstanceUIDs, setViewportData]);
|
// }, [displaySetInstanceUIDs, setViewportData]);
|
||||||
|
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
function ViewerLayout({
|
function ViewerLayout({
|
||||||
// From Extension Module Params
|
// From Extension Module Params
|
||||||
extensionManager,
|
extensionManager,
|
||||||
|
servicesManager,
|
||||||
// From Modes
|
// From Modes
|
||||||
leftPanels,
|
leftPanels,
|
||||||
rightPanels,
|
rightPanels,
|
||||||
viewports,
|
viewports,
|
||||||
ViewportGrid,
|
children,
|
||||||
}) {
|
}) {
|
||||||
|
const [displaySets, setDisplaySets] = useState({});
|
||||||
|
|
||||||
|
console.log(children);
|
||||||
|
debugger;
|
||||||
/**
|
/**
|
||||||
* Set body classes (tailwindcss) that don't allow vertical
|
* Set body classes (tailwindcss) that don't allow vertical
|
||||||
* or horizontal overflow (no scrolling). Also guarantee window
|
* or horizontal overflow (no scrolling). Also guarantee window
|
||||||
@ -41,10 +46,6 @@ function ViewerLayout({
|
|||||||
document.body.classList.remove('overflow-hidden');
|
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 getPanelData = id => {
|
||||||
const entry = extensionManager.getModuleEntry(id);
|
const entry = extensionManager.getModuleEntry(id);
|
||||||
// TODO, not sure why sidepanel content has to be JSX, and not a children prop?
|
// 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 rightPanelComponents = rightPanels.map(getPanelData);
|
||||||
const viewportComponents = viewports.map(getViewportComponentData);
|
const viewportComponents = viewports.map(getViewportComponentData);
|
||||||
|
|
||||||
let { toolBarLayout } = useToolbarLayout();
|
// let { toolBarLayout } = useToolbarLayout();
|
||||||
if (!toolBarLayout.length) {
|
// if (!toolBarLayout.length) {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// TODO -> make toolbar service
|
||||||
|
const toolBarLayout = { tools: [], moreTools: [] };
|
||||||
|
|
||||||
const [primaryToolBarLayout, secondaryToolBarLayout] = toolBarLayout;
|
const [primaryToolBarLayout, secondaryToolBarLayout] = toolBarLayout;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ViewportDataCreator setViewportData={setViewportData}/>
|
|
||||||
<Header
|
<Header
|
||||||
tools={primaryToolBarLayout.tools}
|
tools={primaryToolBarLayout.tools}
|
||||||
moreTools={primaryToolBarLayout.moreTools}
|
moreTools={primaryToolBarLayout.moreTools}
|
||||||
@ -102,8 +105,8 @@ function ViewerLayout({
|
|||||||
<Toolbar type="secondary" tools={secondaryToolBarLayout.tools} />
|
<Toolbar type="secondary" tools={secondaryToolBarLayout.tools} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 h-full overflow-hidden bg-black items-center justify-center pb-2 pt-1">
|
<div className="flex flex-1 h-full overflow-hidden bg-black items-center justify-center pb-2 pt-1">
|
||||||
<ViewportGrid
|
<children.ViewportGrid
|
||||||
viewportData={viewportData}
|
servicesManager={servicesManager}
|
||||||
viewportComponents={viewportComponents}
|
viewportComponents={viewportComponents}
|
||||||
/>
|
/>
|
||||||
{/*
|
{/*
|
||||||
@ -189,7 +192,7 @@ ViewerLayout.propTypes = {
|
|||||||
leftPanels: PropTypes.array,
|
leftPanels: PropTypes.array,
|
||||||
rightPanels: PropTypes.array,
|
rightPanels: PropTypes.array,
|
||||||
/** Responsible for rendering our grid of viewports; provided by consuming application */
|
/** 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 = {
|
ViewerLayout.defaultProps = {
|
||||||
|
|||||||
@ -5,14 +5,18 @@ import ViewerLayout from './ViewerLayout';
|
|||||||
- Init layout based on the displaySets and the objects.
|
- 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 [
|
return [
|
||||||
// Layout Template Definition
|
// Layout Template Definition
|
||||||
// TODO: this is weird naming
|
// TODO: this is weird naming
|
||||||
{
|
{
|
||||||
name: 'viewerLayout',
|
name: 'viewerLayout',
|
||||||
id: 'viewerLayout',
|
id: 'viewerLayout',
|
||||||
component: ViewerLayout,
|
component: ViewerLayoutWithServices,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { StudyBrowser } from '@ohif/ui';
|
import { StudyBrowser } from '@ohif/ui';
|
||||||
import {
|
import { dicomMetadataStore, useViewModel, useToolbarLayout } from '@ohif/core';
|
||||||
dicomMetadataStore,
|
|
||||||
useViewModel,
|
|
||||||
useToolbarLayout,
|
|
||||||
displaySetManager,
|
|
||||||
} from '@ohif/core';
|
|
||||||
|
|
||||||
import MeasurementTable from './MeasurementTable.js';
|
import MeasurementTable from './MeasurementTable.js';
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
export default class toolBarManager {
|
export default class toolBarManager {
|
||||||
constructor(extensionManager, setToolBarLayout) {
|
constructor(extensionManager) {
|
||||||
this.buttons = {};
|
this.buttons = {};
|
||||||
this.extensionManager = extensionManager;
|
this.extensionManager = extensionManager;
|
||||||
this.viewModelContextSetToolBarLayout = setToolBarLayout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addButtons(buttons) {
|
addButtons(buttons) {
|
||||||
@ -42,6 +41,6 @@ export default class toolBarManager {
|
|||||||
toolBarLayout.push(toolBarDefinitions);
|
toolBarLayout.push(toolBarDefinitions);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.viewModelContextSetToolBarLayout(toolBarLayout);
|
// TODO -> Change this to a service. => emit an event to subscribers to update the toolbar layout.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,8 +3,6 @@ import {
|
|||||||
addInstancesToStudy,
|
addInstancesToStudy,
|
||||||
} from './studies/services/wado/studyInstanceHelpers';
|
} from './studies/services/wado/studyInstanceHelpers';
|
||||||
|
|
||||||
import displaySetManager from './displaySetManager';
|
|
||||||
|
|
||||||
class StudyMetadata {
|
class StudyMetadata {
|
||||||
constructor(StudyInstanceUID) {
|
constructor(StudyInstanceUID) {
|
||||||
this.StudyInstanceUID = StudyInstanceUID;
|
this.StudyInstanceUID = StudyInstanceUID;
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -184,6 +184,7 @@ export default class ExtensionManager {
|
|||||||
appConfig: this._appConfig,
|
appConfig: this._appConfig,
|
||||||
configuration,
|
configuration,
|
||||||
api: this._api,
|
api: this._api,
|
||||||
|
extensionManager: this,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!extensionModule) {
|
if (!extensionModule) {
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import studies from './studies/';
|
|||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
import user from './user.js';
|
import user from './user.js';
|
||||||
import dicomMetadataStore from './dicomMetadataStore';
|
import dicomMetadataStore from './dicomMetadataStore';
|
||||||
import displaySetManager from './displaySetManager';
|
|
||||||
import ToolBarManager from './ToolBarManager';
|
import ToolBarManager from './ToolBarManager';
|
||||||
import { ViewModelProvider, useViewModel } from './ViewModelContext';
|
import { ViewModelProvider, useViewModel } from './ViewModelContext';
|
||||||
import {
|
import {
|
||||||
@ -35,6 +34,7 @@ import {
|
|||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
|
DisplaySetService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||||
@ -71,10 +71,11 @@ const OHIF = {
|
|||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
|
DisplaySetService,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
IWebApiDataSource,
|
IWebApiDataSource,
|
||||||
dicomMetadataStore,
|
dicomMetadataStore,
|
||||||
displaySetManager,
|
//
|
||||||
ToolBarManager,
|
ToolBarManager,
|
||||||
ViewModelProvider,
|
ViewModelProvider,
|
||||||
useViewModel,
|
useViewModel,
|
||||||
@ -111,10 +112,10 @@ export {
|
|||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
|
DisplaySetService,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
IWebApiDataSource,
|
IWebApiDataSource,
|
||||||
dicomMetadataStore,
|
dicomMetadataStore,
|
||||||
displaySetManager,
|
|
||||||
ToolBarManager,
|
ToolBarManager,
|
||||||
ViewModelProvider,
|
ViewModelProvider,
|
||||||
useViewModel,
|
useViewModel,
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
8
platform/core/src/services/DisplaySetService/index.js
Normal file
8
platform/core/src/services/DisplaySetService/index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import DisplaySetService from './DisplaySetService';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DisplaySetService',
|
||||||
|
create: ({ configuration = {} }) => {
|
||||||
|
return new DisplaySetService();
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import UIDialogService from './UIDialogService';
|
|||||||
import UIModalService from './UIModalService';
|
import UIModalService from './UIModalService';
|
||||||
import UINotificationService from './UINotificationService';
|
import UINotificationService from './UINotificationService';
|
||||||
import UIViewportDialogService from './UIViewportDialogService';
|
import UIViewportDialogService from './UIViewportDialogService';
|
||||||
|
import DisplaySetService from './DisplaySetService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
@ -12,4 +13,5 @@ export {
|
|||||||
UIModalService,
|
UIModalService,
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
|
DisplaySetService,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,8 +22,16 @@ const getTrackedSeries = displaySets => {
|
|||||||
return trackedSeries;
|
return trackedSeries;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StudyBrowser = ({ tabs, activeTabName, onSetTabActive, onClickStudy, onClickThumbnail }) => {
|
const StudyBrowser = ({
|
||||||
const [tabActive, setTabActive] = useState(activeTabName || getInitialActiveTab(tabs));
|
tabs,
|
||||||
|
activeTabName,
|
||||||
|
onSetTabActive,
|
||||||
|
onClickStudy,
|
||||||
|
onClickThumbnail,
|
||||||
|
}) => {
|
||||||
|
const [tabActive, setTabActive] = useState(
|
||||||
|
activeTabName || getInitialActiveTab(tabs)
|
||||||
|
);
|
||||||
const [studyActive, setStudyActive] = useState(null);
|
const [studyActive, setStudyActive] = useState(null);
|
||||||
const [thumbnailActive, setThumbnailActive] = useState(null);
|
const [thumbnailActive, setThumbnailActive] = useState(null);
|
||||||
|
|
||||||
@ -66,6 +74,7 @@ const StudyBrowser = ({ tabs, activeTabName, onSetTabActive, onClickStudy, onCli
|
|||||||
/>
|
/>
|
||||||
{isActive && displaySets && (
|
{isActive && displaySets && (
|
||||||
<ThumbnailList
|
<ThumbnailList
|
||||||
|
dragData={{ type: 'displayset' }}
|
||||||
thumbnails={displaySets}
|
thumbnails={displaySets}
|
||||||
thumbnailActive={thumbnailActive}
|
thumbnailActive={thumbnailActive}
|
||||||
onThumbnailClick={thumbnailId => {
|
onThumbnailClick={thumbnailId => {
|
||||||
@ -110,7 +119,7 @@ const StudyBrowser = ({ tabs, activeTabName, onSetTabActive, onClickStudy, onCli
|
|||||||
setStudyActive(null);
|
setStudyActive(null);
|
||||||
|
|
||||||
if (onSetTabActive) {
|
if (onSetTabActive) {
|
||||||
onSetTabActive(name)
|
onSetTabActive(name);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -141,6 +150,21 @@ StudyBrowser.propTypes = {
|
|||||||
numInstances: PropTypes.number,
|
numInstances: PropTypes.number,
|
||||||
modalities: PropTypes.string,
|
modalities: PropTypes.string,
|
||||||
description: 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(
|
displaySets: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||||
|
|||||||
@ -3,7 +3,12 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '@ohif/ui';
|
import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '@ohif/ui';
|
||||||
|
|
||||||
const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
|
const ThumbnailList = ({
|
||||||
|
dragData,
|
||||||
|
thumbnails,
|
||||||
|
thumbnailActive,
|
||||||
|
onThumbnailClick,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-black py-3">
|
<div className="bg-black py-3">
|
||||||
{thumbnails.map(
|
{thumbnails.map(
|
||||||
@ -27,6 +32,7 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
|
|||||||
return (
|
return (
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
key={displaySetInstanceUID}
|
key={displaySetInstanceUID}
|
||||||
|
dragData={dragData}
|
||||||
description={description}
|
description={description}
|
||||||
seriesNumber={seriesNumber}
|
seriesNumber={seriesNumber}
|
||||||
numInstances={numInstances}
|
numInstances={numInstances}
|
||||||
@ -41,6 +47,7 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
|
|||||||
return (
|
return (
|
||||||
<ThumbnailNoImage
|
<ThumbnailNoImage
|
||||||
key={displaySetInstanceUID}
|
key={displaySetInstanceUID}
|
||||||
|
dragData={dragData}
|
||||||
modality={modality}
|
modality={modality}
|
||||||
seriesDate={seriesDate}
|
seriesDate={seriesDate}
|
||||||
description={description}
|
description={description}
|
||||||
@ -51,6 +58,7 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
|
|||||||
return (
|
return (
|
||||||
<ThumbnailTracked
|
<ThumbnailTracked
|
||||||
key={displaySetInstanceUID}
|
key={displaySetInstanceUID}
|
||||||
|
dragData={dragData}
|
||||||
description={description}
|
description={description}
|
||||||
seriesNumber={seriesNumber}
|
seriesNumber={seriesNumber}
|
||||||
numInstances={numInstances}
|
numInstances={numInstances}
|
||||||
@ -72,6 +80,17 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ThumbnailList.propTypes = {
|
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(
|
thumbnails: PropTypes.arrayOf(
|
||||||
PropTypes.shape({
|
PropTypes.shape({
|
||||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
displaySetInstanceUID: PropTypes.string.isRequired,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ route: components/viewportGrid
|
|||||||
---
|
---
|
||||||
|
|
||||||
import { Playground, Props } from 'docz';
|
import { Playground, Props } from 'docz';
|
||||||
import { ViewportGrid } from '@ohif/ui';
|
import { DragAndDropProvider, ViewportGrid } from '@ohif/ui';
|
||||||
|
|
||||||
# ViewportGrid
|
# ViewportGrid
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ import { ViewportGrid } from '@ohif/ui';
|
|||||||
## Basic usage
|
## Basic usage
|
||||||
|
|
||||||
<Playground>
|
<Playground>
|
||||||
|
<DragAndDropProvider>
|
||||||
<div className="p-4 h-56">
|
<div className="p-4 h-56">
|
||||||
<ViewportGrid numRows={1} numCols={2}>
|
<ViewportGrid numRows={1} numCols={2}>
|
||||||
<div className="flex items-center justify-center h-full text-white">
|
<div className="flex items-center justify-center h-full text-white">
|
||||||
@ -30,6 +31,7 @@ import { ViewportGrid } from '@ohif/ui';
|
|||||||
</div>
|
</div>
|
||||||
</ViewportGrid>
|
</ViewportGrid>
|
||||||
</div>
|
</div>
|
||||||
|
</DragAndDropProvider>
|
||||||
</Playground>
|
</Playground>
|
||||||
|
|
||||||
## Properties
|
## Properties
|
||||||
|
|||||||
@ -6,20 +6,22 @@ import { useDrop } from 'react-dnd';
|
|||||||
// NOTE: If we found a way to make `useDrop` conditional,
|
// NOTE: If we found a way to make `useDrop` conditional,
|
||||||
// Or we provided a HOC of this component, we could provide
|
// Or we provided a HOC of this component, we could provide
|
||||||
// this UI without the DragAndDropContext dependency.
|
// this UI without the DragAndDropContext dependency.
|
||||||
function ViewportPane({ children, className, isActive, onDrop }) {
|
function ViewportPane({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
onDrop,
|
||||||
|
acceptDropsFor,
|
||||||
|
}) {
|
||||||
const [{ isHovered, isHighlighted }, drop] = useDrop({
|
const [{ isHovered, isHighlighted }, drop] = useDrop({
|
||||||
accept: 'displayset',
|
accept: acceptDropsFor,
|
||||||
// TODO: pass in as prop?
|
// TODO: pass in as prop?
|
||||||
drop: (displaySet, monitor) => {
|
drop: (droppedItem, monitor) => {
|
||||||
const canDrop = monitor.canDrop();
|
const canDrop = monitor.canDrop();
|
||||||
const isOver = monitor.isOver();
|
const isOver = monitor.isOver();
|
||||||
|
|
||||||
if (canDrop && isOver && onDrop) {
|
if (canDrop && isOver && onDrop) {
|
||||||
const { StudyInstanceUID, displaySetInstanceUID } = displaySet;
|
onDrop(droppedItem);
|
||||||
|
|
||||||
onDrop({
|
|
||||||
//viewportIndex, StudyInstanceUID, displaySetInstanceUID
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Monitor, and collect props; returned as values by `useDrop`
|
// Monitor, and collect props; returned as values by `useDrop`
|
||||||
@ -53,6 +55,8 @@ ViewportPane.propTypes = {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
/** Bool to show active styling */
|
/** Bool to show active styling */
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
|
/** Indicates drag items we should accept for drops */
|
||||||
|
acceptDropsFor: PropTypes.string.isRequired,
|
||||||
/** Function that handles drop events */
|
/** Function that handles drop events */
|
||||||
onDrop: PropTypes.func.isRequired,
|
onDrop: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,43 +1,51 @@
|
|||||||
import React from 'react';
|
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 Header from './components/Header';
|
||||||
import ViewportToolbar from './components/ViewportToolBar';
|
import ViewportToolbar from './components/ViewportToolBar';
|
||||||
|
|
||||||
const Viewer = () => {
|
const Viewer = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<DragAndDropProvider>
|
||||||
<Header />
|
<div>
|
||||||
<div
|
<Header />
|
||||||
className="flex flex-row flex-no-wrap flex-1 items-stretch overflow-hidden w-full"
|
<div
|
||||||
style={{ height: 'calc(100vh - 57px' }}
|
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}
|
|
||||||
>
|
>
|
||||||
<StudyBrowser />
|
<SidePanel
|
||||||
</SidePanel>
|
side="left"
|
||||||
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
|
iconName="group-layers"
|
||||||
<ViewportToolbar />
|
iconLabel="Studies"
|
||||||
<div>CONTENT</div>
|
componentLabel="Studies"
|
||||||
</div>
|
defaultIsOpen={true}
|
||||||
<SidePanel
|
>
|
||||||
side="right"
|
<StudyBrowser />
|
||||||
iconName="list-bullets"
|
</SidePanel>
|
||||||
iconLabel="Measure"
|
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
|
||||||
componentLabel="Measurements"
|
<ViewportToolbar />
|
||||||
defaultIsOpen={false}
|
<div>CONTENT</div>
|
||||||
>
|
|
||||||
<div className="flex justify-center text-white p-2">
|
|
||||||
panel placeholder
|
|
||||||
</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>
|
||||||
</div>
|
</DragAndDropProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { ThemeWrapper } from '@ohif/ui';
|
|||||||
// TODO: Should this influence study list?
|
// TODO: Should this influence study list?
|
||||||
import { appConfigContext } from './state/appConfig.context';
|
import { appConfigContext } from './state/appConfig.context';
|
||||||
import { useAppConfig } from './hooks/useAppConfig';
|
import { useAppConfig } from './hooks/useAppConfig';
|
||||||
import createAppRoutes from './routes';
|
import createRoutes from './routes';
|
||||||
import appInit from './appInit.js';
|
import appInit from './appInit.js';
|
||||||
|
|
||||||
// Temporarily for testing
|
// Temporarily for testing
|
||||||
@ -34,7 +34,12 @@ function App({ config, defaultExtensions }) {
|
|||||||
const appConfigContextApi = useAppConfig(init.appConfig);
|
const appConfigContextApi = useAppConfig(init.appConfig);
|
||||||
const { routerBasename, modes, dataSources } = appConfigContextApi.appConfig;
|
const { routerBasename, modes, dataSources } = appConfigContextApi.appConfig;
|
||||||
// Use config to create routes
|
// Use config to create routes
|
||||||
const appRoutes = createAppRoutes(modes, dataSources, extensionManager);
|
const appRoutes = createRoutes(
|
||||||
|
modes,
|
||||||
|
dataSources,
|
||||||
|
extensionManager,
|
||||||
|
servicesManager
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<appConfigContext.Provider value={appConfigContextApi}>
|
<appConfigContext.Provider value={appConfigContextApi}>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
UIModalService,
|
UIModalService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
// utils,
|
// utils,
|
||||||
// redux as reduxOHIF,
|
// redux as reduxOHIF,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
@ -43,6 +44,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
|||||||
UIModalService,
|
UIModalService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
|
DisplaySetService,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -10,32 +10,48 @@ import { ViewportGrid, ViewportPane } from '@ohif/ui';
|
|||||||
function ViewerViewportGrid(props) {
|
function ViewerViewportGrid(props) {
|
||||||
const {
|
const {
|
||||||
activeViewportIndex,
|
activeViewportIndex,
|
||||||
viewportData,
|
servicesManager,
|
||||||
viewportComponents,
|
viewportComponents,
|
||||||
dataSource,
|
dataSource,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// From ViewportGridService and/or ContextProvider
|
debugger;
|
||||||
const [viewportGrid, setViewportGrid] = useState({
|
|
||||||
numRows: 2,
|
// TODO -> Need some way of selecting which displaySets hit the viewports.
|
||||||
numCols: 2,
|
const { DisplaySetService } = servicesManager.services;
|
||||||
activeViewportIndex: 0,
|
|
||||||
viewports: [
|
// TODO -> Make a HangingProtocolService
|
||||||
{
|
const HangingProtocolService = displaySets => {
|
||||||
displaySetUid: undefined,
|
return {
|
||||||
},
|
numRows: 1,
|
||||||
{
|
numCols: 1,
|
||||||
displaySetUid: undefined,
|
activeViewportIndex: 0,
|
||||||
},
|
viewports: [
|
||||||
{
|
{
|
||||||
displaySetUid: undefined,
|
displaySetInstanceUid: Object.keys(displaySets)[0][0]
|
||||||
},
|
.displaySetInstanceUid,
|
||||||
{
|
},
|
||||||
displaySetUid: undefined,
|
],
|
||||||
},
|
};
|
||||||
],
|
};
|
||||||
|
|
||||||
|
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
|
// viewportData --> displaySets
|
||||||
|
|
||||||
const getViewportPanes = () =>
|
const getViewportPanes = () =>
|
||||||
@ -47,8 +63,10 @@ function ViewerViewportGrid(props) {
|
|||||||
<ViewportPane
|
<ViewportPane
|
||||||
key={viewportIndex}
|
key={viewportIndex}
|
||||||
className="m-1"
|
className="m-1"
|
||||||
onDrop={() => {
|
// Pass in as prop?
|
||||||
/* setDisplaySet for Viewport */
|
acceptDropsFor="displayset"
|
||||||
|
onDrop={droppedItem => {
|
||||||
|
console.warn('DROPPED ITEM:', droppedItem);
|
||||||
}}
|
}}
|
||||||
isActive={activeViewportIndex === viewportIndex}
|
isActive={activeViewportIndex === viewportIndex}
|
||||||
></ViewportPane>
|
></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,
|
mode,
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
|
DisplaySetService,
|
||||||
}) {
|
}) {
|
||||||
console.warn('DisplaySetCreator rerendering');
|
console.warn('DisplaySetCreator rerendering');
|
||||||
const { routes, sopClassHandlers } = mode;
|
const { routes, sopClassHandlers } = mode;
|
||||||
|
|||||||
@ -1,27 +1,30 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
//
|
//
|
||||||
import { ToolbarLayoutProvider } from '@ohif/core';
|
import { ToolBarManager } from '@ohif/core';
|
||||||
import { DragAndDropProvider } from '@ohif/ui';
|
import { DragAndDropProvider } from '@ohif/ui';
|
||||||
//
|
//
|
||||||
import ViewportGrid from '@components/ViewportGrid';
|
import ViewportGrid from '@components/ViewportGrid';
|
||||||
import Compose from './Compose';
|
import Compose from './Compose';
|
||||||
import DisplaySetCreator from './DisplaySetCreator';
|
//import DisplaySetCreator from './DisplaySetCreator';
|
||||||
|
|
||||||
export default function ModeRoute({
|
export default function ModeRoute({
|
||||||
location,
|
location,
|
||||||
mode,
|
mode,
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
|
servicesManager,
|
||||||
}) {
|
}) {
|
||||||
console.warn('ModeRoute rerendering');
|
console.warn('ModeRoute rerendering');
|
||||||
const { routes, extensions } = mode;
|
const { routes, extensions, sopClassHandlers } = mode;
|
||||||
const dataSources = extensionManager.getDataSources(dataSourceName);
|
const dataSources = extensionManager.getDataSources(dataSourceName);
|
||||||
// TODO: For now assume one unique datasource.
|
// TODO: For now assume one unique datasource.
|
||||||
|
|
||||||
const dataSource = dataSources[0];
|
const dataSource = dataSources[0];
|
||||||
const route = routes[0];
|
const route = routes[0];
|
||||||
|
|
||||||
|
const { DisplaySetService } = servicesManager.services;
|
||||||
|
|
||||||
// Only handling one route per mode for now
|
// Only handling one route per mode for now
|
||||||
// You can test via http://localhost:3000/example-mode/dicomweb
|
// You can test via http://localhost:3000/example-mode/dicomweb
|
||||||
const layoutTemplateData = route.layoutTemplate({ location });
|
const layoutTemplateData = route.layoutTemplate({ location });
|
||||||
@ -51,32 +54,59 @@ export default function ModeRoute({
|
|||||||
const CombinedContextProvider = ({ children }) =>
|
const CombinedContextProvider = ({ children }) =>
|
||||||
Compose({ components: contextModuleProviders, children });
|
Compose({ components: contextModuleProviders, children });
|
||||||
|
|
||||||
|
debugger;
|
||||||
|
|
||||||
function ViewportGridWithDataSource(props) {
|
function ViewportGridWithDataSource(props) {
|
||||||
|
debugger;
|
||||||
return ViewportGrid({ ...props, dataSource });
|
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 (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ToolbarLayoutProvider>
|
{/*<ToolbarLayoutProvider>*/}
|
||||||
<DisplaySetCreator
|
{/*
|
||||||
location={location}
|
<DisplaySetCreator
|
||||||
mode={mode}
|
location={location}
|
||||||
dataSourceName={dataSourceName}
|
mode={mode}
|
||||||
extensionManager={extensionManager}
|
dataSourceName={dataSourceName}
|
||||||
/>
|
extensionManager={extensionManager}
|
||||||
<CombinedContextProvider>
|
DisplaySetService={DisplaySetService}
|
||||||
{/* TODO: extensionManager is already provided to the extension module.
|
/>
|
||||||
* Use it from there instead of passing as a prop here.
|
*/}
|
||||||
*/}
|
<CombinedContextProvider>
|
||||||
<DragAndDropProvider>
|
{/* TODO: extensionManager is already provided to the extension module.
|
||||||
<LayoutComponent
|
* Use it from there instead of passing as a prop here.
|
||||||
extensionManager={extensionManager}
|
*/}
|
||||||
ViewportGrid={ViewportGridWithDataSource}
|
<DragAndDropProvider>
|
||||||
{...layoutTemplateData.props}
|
<LayoutComponent {...layoutTemplateData.props}>
|
||||||
/>
|
<ViewportGridWithDataSource />
|
||||||
</DragAndDropProvider>
|
</LayoutComponent>
|
||||||
</CombinedContextProvider>
|
</DragAndDropProvider>
|
||||||
</ToolbarLayoutProvider>
|
</CombinedContextProvider>
|
||||||
|
{/*</ToolbarLayoutProvider>*/}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -88,7 +118,7 @@ ModeRoute.propTypes = {
|
|||||||
pathname: PropTypes.string.isRequired,
|
pathname: PropTypes.string.isRequired,
|
||||||
search: PropTypes.string.isRequired,
|
search: PropTypes.string.isRequired,
|
||||||
hash: PropTypes.string.isRequired,
|
hash: PropTypes.string.isRequired,
|
||||||
state: PropTypes.object.isRequired,
|
//state: PropTypes.object.isRequired,
|
||||||
}),
|
}),
|
||||||
mode: PropTypes.object.isRequired,
|
mode: PropTypes.object.isRequired,
|
||||||
dataSourceName: PropTypes.string,
|
dataSourceName: PropTypes.string,
|
||||||
|
|||||||
@ -22,7 +22,15 @@ import { ViewModelProvider } from '@ohif/core';
|
|||||||
|
|
||||||
/:modeId/:modeRoute/?queryParameters=example
|
/:modeId/:modeRoute/?queryParameters=example
|
||||||
*/
|
*/
|
||||||
export default function buildModeRoutes(modes, dataSources, extensionManager) {
|
export default function buildModeRoutes(
|
||||||
|
modes,
|
||||||
|
|
||||||
|
dataSources,
|
||||||
|
|
||||||
|
extensionManager,
|
||||||
|
|
||||||
|
servicesManager
|
||||||
|
) {
|
||||||
const routes = [];
|
const routes = [];
|
||||||
|
|
||||||
// const dataSources = Object.keys(extensionManager.dataSourceMap).map(a =>
|
// const dataSources = Object.keys(extensionManager.dataSourceMap).map(a =>
|
||||||
@ -52,6 +60,7 @@ export default function buildModeRoutes(modes, dataSources, extensionManager) {
|
|||||||
mode={mode}
|
mode={mode}
|
||||||
dataSourceName={dataSourceName}
|
dataSourceName={dataSourceName}
|
||||||
extensionManager={extensionManager}
|
extensionManager={extensionManager}
|
||||||
|
servicesManager={servicesManager}
|
||||||
/>
|
/>
|
||||||
</ViewModelProvider>
|
</ViewModelProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -20,12 +20,26 @@ const bakedInRoutes = [
|
|||||||
{ component: NotFound },
|
{ component: NotFound },
|
||||||
];
|
];
|
||||||
|
|
||||||
const createRoutes = (modes, dataSources, extensionManager) => {
|
const createRoutes = (
|
||||||
const routes = buildModeRoutes(modes, dataSources, extensionManager) || [];
|
modes,
|
||||||
|
dataSources,
|
||||||
|
extensionManager,
|
||||||
|
servicesManager
|
||||||
|
) => {
|
||||||
|
const routes =
|
||||||
|
buildModeRoutes(modes, dataSources, extensionManager, servicesManager) ||
|
||||||
|
[];
|
||||||
// TODO: Shouldn't need to guard input routes with an empty array?
|
// TODO: Shouldn't need to guard input routes with an empty array?
|
||||||
const allRoutes = [...(routes || []), ...bakedInRoutes];
|
const allRoutes = [...(routes || []), ...bakedInRoutes];
|
||||||
|
|
||||||
console.log('Creating Routes: ', modes, dataSources, routes, bakedInRoutes, allRoutes);
|
console.log(
|
||||||
|
'Creating Routes: ',
|
||||||
|
modes,
|
||||||
|
dataSources,
|
||||||
|
routes,
|
||||||
|
bakedInRoutes,
|
||||||
|
allRoutes
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user