Remove connected components roped in from a rebase
This commit is contained in:
parent
fab7334696
commit
ae107f4d3b
@ -1,57 +0,0 @@
|
|||||||
import OHIF from '@ohif/core';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { StudyBrowser } from '@ohif/ui';
|
|
||||||
import cloneDeep from 'lodash.clonedeep';
|
|
||||||
import findDisplaySetByUID from './findDisplaySetByUID';
|
|
||||||
|
|
||||||
const { setActiveViewportSpecificData } = OHIF.redux.actions;
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// - Determine in which display set is active from Redux (activeViewportIndex and layout viewportData)
|
|
||||||
// - Pass in errors and stack loading progress from Redux
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
// If we know that the stack loading progress details have changed,
|
|
||||||
// we can try to update the component state so that the thumbnail
|
|
||||||
// progress bar is updated
|
|
||||||
const stackLoadingProgressMap = state.loading.progress;
|
|
||||||
const studiesWithLoadingData = cloneDeep(ownProps.studies);
|
|
||||||
|
|
||||||
studiesWithLoadingData.forEach(study => {
|
|
||||||
study.thumbnails.forEach(data => {
|
|
||||||
const { displaySetInstanceUID } = data;
|
|
||||||
const stackId = `StackProgress:${displaySetInstanceUID}`;
|
|
||||||
const stackProgressData = stackLoadingProgressMap[stackId];
|
|
||||||
|
|
||||||
let stackPercentComplete = 0;
|
|
||||||
if (stackProgressData) {
|
|
||||||
stackPercentComplete = stackProgressData.percentComplete;
|
|
||||||
}
|
|
||||||
|
|
||||||
data.stackPercentComplete = stackPercentComplete;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
studies: studiesWithLoadingData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
|
||||||
return {
|
|
||||||
onThumbnailClick: displaySetInstanceUID => {
|
|
||||||
const displaySet = findDisplaySetByUID(
|
|
||||||
ownProps.studyMetadata,
|
|
||||||
displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch(setActiveViewportSpecificData(displaySet));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedStudyBrowser = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(StudyBrowser);
|
|
||||||
|
|
||||||
export default ConnectedStudyBrowser;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import OHIF from '@ohif/core';
|
|
||||||
import ViewerMain from './ViewerMain';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
const {
|
|
||||||
setViewportSpecificData,
|
|
||||||
clearViewportSpecificData,
|
|
||||||
} = OHIF.redux.actions;
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
|
|
||||||
|
|
||||||
return {
|
|
||||||
activeViewportIndex,
|
|
||||||
layout,
|
|
||||||
viewportSpecificData,
|
|
||||||
viewports: state.viewports,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
setViewportSpecificData: (viewportIndex, data) => {
|
|
||||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
|
||||||
},
|
|
||||||
clearViewportSpecificData: () => {
|
|
||||||
dispatch(clearViewportSpecificData());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedViewerMain = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ViewerMain);
|
|
||||||
|
|
||||||
export default ConnectedViewerMain;
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
|
|
||||||
import OHIF from '@ohif/core';
|
|
||||||
|
|
||||||
const { clearViewportSpecificData } = OHIF.redux.actions;
|
|
||||||
const isActive = a => a.active === true;
|
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
const activeServer = state.servers.servers.find(isActive);
|
|
||||||
|
|
||||||
return {
|
|
||||||
server: ownProps.server || activeServer,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
clearViewportSpecificData: () => {
|
|
||||||
dispatch(clearViewportSpecificData());
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectedViewerRetrieveStudyData = connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps
|
|
||||||
)(ViewerRetrieveStudyData);
|
|
||||||
|
|
||||||
export default ConnectedViewerRetrieveStudyData;
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
import { ContextMenu } from '@ohif/ui';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { commandsManager } from './../App.jsx';
|
|
||||||
|
|
||||||
const toolTypes = [
|
|
||||||
'Angle',
|
|
||||||
'Bidirectional',
|
|
||||||
'Length',
|
|
||||||
'FreehandMouse',
|
|
||||||
'EllipticalRoi',
|
|
||||||
'CircleRoi',
|
|
||||||
'RectangleRoi',
|
|
||||||
];
|
|
||||||
|
|
||||||
const ToolContextMenu = ({
|
|
||||||
onSetLabel,
|
|
||||||
onSetDescription,
|
|
||||||
isTouchEvent,
|
|
||||||
eventData,
|
|
||||||
onClose,
|
|
||||||
onDelete,
|
|
||||||
}) => {
|
|
||||||
const defaultDropdownItems = [
|
|
||||||
{
|
|
||||||
label: 'Delete measurement',
|
|
||||||
actionType: 'Delete',
|
|
||||||
action: ({ nearbyToolData, eventData }) =>
|
|
||||||
onDelete(nearbyToolData, eventData),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Relabel',
|
|
||||||
actionType: 'setLabel',
|
|
||||||
action: ({ nearbyToolData, eventData }) => {
|
|
||||||
const { tool: measurementData } = nearbyToolData;
|
|
||||||
onSetLabel(eventData, measurementData);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
actionType: 'setDescription',
|
|
||||||
action: ({ nearbyToolData, eventData }) => {
|
|
||||||
const { tool: measurementData } = nearbyToolData;
|
|
||||||
onSetDescription(eventData, measurementData);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getDropdownItems = (eventData, isTouchEvent = false) => {
|
|
||||||
const nearbyToolData = commandsManager.runCommand('getNearbyToolData', {
|
|
||||||
element: eventData.element,
|
|
||||||
canvasCoordinates: eventData.currentPoints.canvas,
|
|
||||||
availableToolTypes: toolTypes,
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Annotate tools for touch events already have a press handle to edit it,
|
|
||||||
* has a better UX for deleting it.
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
isTouchEvent &&
|
|
||||||
nearbyToolData &&
|
|
||||||
nearbyToolData.toolType === 'arrowAnnotate'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dropdownItems = [];
|
|
||||||
if (nearbyToolData) {
|
|
||||||
defaultDropdownItems.forEach(item => {
|
|
||||||
item.params = { eventData, nearbyToolData };
|
|
||||||
|
|
||||||
if (item.actionType === 'setDescription') {
|
|
||||||
item.label = `${
|
|
||||||
nearbyToolData.tool.description ? 'Edit' : 'Add'
|
|
||||||
} Description`;
|
|
||||||
}
|
|
||||||
|
|
||||||
dropdownItems.push(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return dropdownItems;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClickHandler = ({ action, params }) => {
|
|
||||||
action(params);
|
|
||||||
if (onClose) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ToolContextMenu">
|
|
||||||
<ContextMenu items={dropdownItems} onClick={onClickHandler} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ToolContextMenu.propTypes = {
|
|
||||||
isTouchEvent: PropTypes.bool.isRequired,
|
|
||||||
eventData: PropTypes.object,
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
onSetDescription: PropTypes.func,
|
|
||||||
onSetLabel: PropTypes.func,
|
|
||||||
onDelete: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
ToolContextMenu.defaultProps = {
|
|
||||||
isTouchEvent: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ToolContextMenu;
|
|
||||||
@ -1,386 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { MODULE_TYPES } from '@ohif/core';
|
|
||||||
import {
|
|
||||||
ExpandableToolMenu,
|
|
||||||
RoundedButtonGroup,
|
|
||||||
ToolbarButton,
|
|
||||||
withModal,
|
|
||||||
withDialog,
|
|
||||||
} from '@ohif/ui';
|
|
||||||
|
|
||||||
import './ToolbarRow.css';
|
|
||||||
import { commandsManager, extensionManager } from './../App.jsx';
|
|
||||||
|
|
||||||
import ConnectedCineDialog from './ConnectedCineDialog';
|
|
||||||
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
|
||||||
import { withAppContext } from '../context/AppContext';
|
|
||||||
|
|
||||||
class ToolbarRow extends Component {
|
|
||||||
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
|
|
||||||
// closed if selected is null/undefined
|
|
||||||
static propTypes = {
|
|
||||||
isLeftSidePanelOpen: PropTypes.bool.isRequired,
|
|
||||||
isRightSidePanelOpen: PropTypes.bool.isRequired,
|
|
||||||
selectedLeftSidePanel: PropTypes.string.isRequired,
|
|
||||||
selectedRightSidePanel: PropTypes.string.isRequired,
|
|
||||||
handleSidePanelChange: PropTypes.func.isRequired,
|
|
||||||
activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
studies: PropTypes.array,
|
|
||||||
t: PropTypes.func.isRequired,
|
|
||||||
// NOTE: withDialog, withModal HOCs
|
|
||||||
dialog: PropTypes.any,
|
|
||||||
modal: PropTypes.any,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
studies: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const toolbarButtonDefinitions = _getVisibleToolbarButtons.call(this);
|
|
||||||
// TODO:
|
|
||||||
// If it's a tool that can be active... Mark it as active?
|
|
||||||
// - Tools that are on/off?
|
|
||||||
// - Tools that can be bound to multiple buttons?
|
|
||||||
|
|
||||||
// Normal ToolbarButtons...
|
|
||||||
// Just how high do we need to hoist this state?
|
|
||||||
// Why ToolbarRow instead of just Toolbar? Do we have any others?
|
|
||||||
this.state = {
|
|
||||||
toolbarButtons: toolbarButtonDefinitions,
|
|
||||||
activeButtons: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.seriesPerStudyCount = [];
|
|
||||||
|
|
||||||
this._handleBuiltIn = _handleBuiltIn.bind(this);
|
|
||||||
|
|
||||||
this.updateButtonGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateButtonGroups() {
|
|
||||||
const panelModules = extensionManager.modules[MODULE_TYPES.PANEL];
|
|
||||||
|
|
||||||
this.buttonGroups = {
|
|
||||||
left: [],
|
|
||||||
right: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// ~ FIND MENU OPTIONS
|
|
||||||
panelModules.forEach(panelExtension => {
|
|
||||||
const panelModule = panelExtension.module;
|
|
||||||
const defaultContexts = Array.from(panelModule.defaultContext);
|
|
||||||
|
|
||||||
panelModule.menuOptions.forEach(menuOption => {
|
|
||||||
const contexts = Array.from(menuOption.context || defaultContexts);
|
|
||||||
const hasActiveContext = this.props.activeContexts.some(actx =>
|
|
||||||
contexts.includes(actx)
|
|
||||||
);
|
|
||||||
|
|
||||||
// It's a bit beefy to pass studies; probably only need to be reactive on `studyInstanceUIDs` and activeViewport?
|
|
||||||
// Note: This does not cleanly handle `studies` prop updating with panel open
|
|
||||||
const isDisabled =
|
|
||||||
typeof menuOption.isDisabled === 'function' &&
|
|
||||||
menuOption.isDisabled(this.props.studies);
|
|
||||||
|
|
||||||
if (hasActiveContext && !isDisabled) {
|
|
||||||
const menuOptionEntry = {
|
|
||||||
value: menuOption.target,
|
|
||||||
icon: menuOption.icon,
|
|
||||||
bottomLabel: menuOption.label,
|
|
||||||
};
|
|
||||||
const from = menuOption.from || 'right';
|
|
||||||
|
|
||||||
this.buttonGroups[from].push(menuOptionEntry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: This should come from extensions, instead of being baked in
|
|
||||||
this.buttonGroups.left.unshift({
|
|
||||||
value: 'studies',
|
|
||||||
icon: 'th-large',
|
|
||||||
bottomLabel: this.props.t('Series'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const activeContextsChanged =
|
|
||||||
prevProps.activeContexts !== this.props.activeContexts;
|
|
||||||
|
|
||||||
const prevStudies = prevProps.studies;
|
|
||||||
const studies = this.props.studies;
|
|
||||||
const seriesPerStudyCount = this.seriesPerStudyCount;
|
|
||||||
|
|
||||||
let studiesUpdated = false;
|
|
||||||
|
|
||||||
if (prevStudies.length !== studies.length) {
|
|
||||||
studiesUpdated = true;
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < studies.length; i++) {
|
|
||||||
if (studies[i].series.length !== seriesPerStudyCount[i]) {
|
|
||||||
seriesPerStudyCount[i] = studies[i].series.length;
|
|
||||||
|
|
||||||
studiesUpdated = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (studiesUpdated) {
|
|
||||||
this.updateButtonGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeContextsChanged) {
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
toolbarButtons: _getVisibleToolbarButtons.call(this),
|
|
||||||
},
|
|
||||||
this.closeCineDialogIfNotApplicable
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeCineDialogIfNotApplicable = () => {
|
|
||||||
const { dialog } = this.props;
|
|
||||||
let { dialogId, activeButtons, toolbarButtons } = this.state;
|
|
||||||
if (dialogId) {
|
|
||||||
const cineButtonPresent = toolbarButtons.find(
|
|
||||||
button => button.options && button.options.behavior === 'CINE'
|
|
||||||
);
|
|
||||||
if (!cineButtonPresent) {
|
|
||||||
dialog.dismiss({ id: dialogId });
|
|
||||||
activeButtons = activeButtons.filter(
|
|
||||||
button => button.options && button.options.behavior !== 'CINE'
|
|
||||||
);
|
|
||||||
this.setState({ dialogId: null, activeButtons });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const buttonComponents = _getButtonComponents.call(
|
|
||||||
this,
|
|
||||||
this.state.toolbarButtons,
|
|
||||||
this.state.activeButtons
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPress = (side, value) => {
|
|
||||||
this.props.handleSidePanelChange(side, value);
|
|
||||||
};
|
|
||||||
const onPressLeft = onPress.bind(this, 'left');
|
|
||||||
const onPressRight = onPress.bind(this, 'right');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="ToolbarRow">
|
|
||||||
<div className="pull-left m-t-1 p-y-1" style={{ padding: '10px' }}>
|
|
||||||
<RoundedButtonGroup
|
|
||||||
options={this.buttonGroups.left}
|
|
||||||
value={this.props.selectedLeftSidePanel || ''}
|
|
||||||
onValueChanged={onPressLeft}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{buttonComponents}
|
|
||||||
<ConnectedLayoutButton />
|
|
||||||
<div
|
|
||||||
className="pull-right m-t-1 rm-x-1"
|
|
||||||
style={{ marginLeft: 'auto' }}
|
|
||||||
>
|
|
||||||
{this.buttonGroups.right.length && (
|
|
||||||
<RoundedButtonGroup
|
|
||||||
options={this.buttonGroups.right}
|
|
||||||
value={this.props.selectedRightSidePanel || ''}
|
|
||||||
onValueChanged={onPressRight}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getCustomButtonComponent(button, activeButtons) {
|
|
||||||
const CustomComponent = button.CustomComponent;
|
|
||||||
const isValidComponent = typeof CustomComponent === 'function';
|
|
||||||
|
|
||||||
// Check if its a valid customComponent. Later on an CustomToolbarComponent interface could be implemented.
|
|
||||||
if (isValidComponent) {
|
|
||||||
const parentContext = this;
|
|
||||||
const activeButtonsIds = activeButtons.map(button => button.id);
|
|
||||||
const isActive = activeButtonsIds.includes(button.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomComponent
|
|
||||||
parentContext={parentContext}
|
|
||||||
toolbarClickCallback={_handleToolbarButtonClick.bind(this)}
|
|
||||||
button={button}
|
|
||||||
key={button.id}
|
|
||||||
activeButtons={activeButtonsIds}
|
|
||||||
isActive={isActive}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getExpandableButtonComponent(button, activeButtons) {
|
|
||||||
// Iterate over button definitions and update `onClick` behavior
|
|
||||||
let activeCommand;
|
|
||||||
const childButtons = button.buttons.map(childButton => {
|
|
||||||
childButton.onClick = _handleToolbarButtonClick.bind(this, childButton);
|
|
||||||
|
|
||||||
if (activeButtons.map(button => button.id).indexOf(childButton.id) > -1) {
|
|
||||||
activeCommand = childButton.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return childButton;
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ExpandableToolMenu
|
|
||||||
key={button.id}
|
|
||||||
label={button.label}
|
|
||||||
icon={button.icon}
|
|
||||||
buttons={childButtons}
|
|
||||||
activeCommand={activeCommand}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _getDefaultButtonComponent(button, activeButtons) {
|
|
||||||
return (
|
|
||||||
<ToolbarButton
|
|
||||||
key={button.id}
|
|
||||||
label={button.label}
|
|
||||||
icon={button.icon}
|
|
||||||
onClick={_handleToolbarButtonClick.bind(this, button)}
|
|
||||||
isActive={activeButtons.map(button => button.id).includes(button.id)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Determine which extension buttons should be showing, if they're
|
|
||||||
* active, and what their onClick behavior should be.
|
|
||||||
*/
|
|
||||||
function _getButtonComponents(toolbarButtons, activeButtons) {
|
|
||||||
const _this = this;
|
|
||||||
return toolbarButtons.map(button => {
|
|
||||||
const hasCustomComponent = button.CustomComponent;
|
|
||||||
const hasNestedButtonDefinitions = button.buttons && button.buttons.length;
|
|
||||||
|
|
||||||
if (hasCustomComponent) {
|
|
||||||
return _getCustomButtonComponent.call(_this, button, activeButtons);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNestedButtonDefinitions) {
|
|
||||||
return _getExpandableButtonComponent.call(_this, button, activeButtons);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _getDefaultButtonComponent.call(_this, button, activeButtons);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: DEPRECATE
|
|
||||||
* This is used exclusively in `extensions/cornerstone/src`
|
|
||||||
* We have better ways with new UI Services to trigger "builtin" behaviors
|
|
||||||
*
|
|
||||||
* A handy way for us to handle different button types. IE. firing commands for
|
|
||||||
* buttons, or initiation built in behavior.
|
|
||||||
*
|
|
||||||
* @param {*} button
|
|
||||||
* @param {*} evt
|
|
||||||
* @param {*} props
|
|
||||||
*/
|
|
||||||
function _handleToolbarButtonClick(button, evt, props) {
|
|
||||||
const { activeButtons } = this.state;
|
|
||||||
|
|
||||||
if (button.commandName) {
|
|
||||||
const options = Object.assign({ evt }, button.commandOptions);
|
|
||||||
commandsManager.runCommand(button.commandName, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use Types ENUM
|
|
||||||
// TODO: We can update this to be a `getter` on the extension to query
|
|
||||||
// For the active tools after we apply our updates?
|
|
||||||
if (button.type === 'setToolActive') {
|
|
||||||
const toggables = activeButtons.filter(
|
|
||||||
({ options }) => options && !options.togglable
|
|
||||||
);
|
|
||||||
this.setState({ activeButtons: [...toggables, button] });
|
|
||||||
} else if (button.type === 'builtIn') {
|
|
||||||
this._handleBuiltIn(button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
function _getVisibleToolbarButtons() {
|
|
||||||
const toolbarModules = extensionManager.modules[MODULE_TYPES.TOOLBAR];
|
|
||||||
const toolbarButtonDefinitions = [];
|
|
||||||
|
|
||||||
toolbarModules.forEach(extension => {
|
|
||||||
const { definitions, defaultContext } = extension.module;
|
|
||||||
definitions.forEach(definition => {
|
|
||||||
const context = definition.context || defaultContext;
|
|
||||||
|
|
||||||
if (this.props.activeContexts.includes(context)) {
|
|
||||||
toolbarButtonDefinitions.push(definition);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return toolbarButtonDefinitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _handleBuiltIn(button) {
|
|
||||||
/* TODO: Keep cine button active until its unselected. */
|
|
||||||
const { dialog, t } = this.props;
|
|
||||||
const { dialogId } = this.state;
|
|
||||||
const { id, options } = button;
|
|
||||||
|
|
||||||
if (options.behavior === 'CINE') {
|
|
||||||
if (dialogId) {
|
|
||||||
dialog.dismiss({ id: dialogId });
|
|
||||||
this.setState(state => ({
|
|
||||||
dialogId: null,
|
|
||||||
activeButtons: [
|
|
||||||
...state.activeButtons.filter(button => button.id !== id),
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
const spacing = 20;
|
|
||||||
const { x, y } = document
|
|
||||||
.querySelector(`.ViewerMain`)
|
|
||||||
.getBoundingClientRect();
|
|
||||||
const newDialogId = dialog.create({
|
|
||||||
content: ConnectedCineDialog,
|
|
||||||
defaultPosition: {
|
|
||||||
x: x + spacing || 0,
|
|
||||||
y: y + spacing || 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.setState(state => ({
|
|
||||||
dialogId: newDialogId,
|
|
||||||
activeButtons: [...state.activeButtons, button],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') {
|
|
||||||
commandsManager.runCommand('showDownloadViewportModal', {
|
|
||||||
title: t('Download High Quality Image'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation(['Common', 'ViewportDownloadForm'])(
|
|
||||||
withModal(withDialog(withAppContext(ToolbarRow)))
|
|
||||||
);
|
|
||||||
@ -1,400 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { MODULE_TYPES } from '@ohif/core';
|
|
||||||
import OHIF, { DICOMSR } from '@ohif/core';
|
|
||||||
import { withDialog } from '@ohif/ui';
|
|
||||||
import moment from 'moment';
|
|
||||||
import ConnectedHeader from './ConnectedHeader.js';
|
|
||||||
import ToolbarRow from './ToolbarRow.js';
|
|
||||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
|
||||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
|
||||||
import SidePanel from './../components/SidePanel.js';
|
|
||||||
import { extensionManager } from './../App.jsx';
|
|
||||||
|
|
||||||
// Contexts
|
|
||||||
import WhiteLabelingContext from '../context/WhiteLabelingContext.js';
|
|
||||||
import UserManagerContext from '../context/UserManagerContext';
|
|
||||||
import AppContext from '../context/AppContext';
|
|
||||||
|
|
||||||
import './Viewer.css';
|
|
||||||
|
|
||||||
class Viewer extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
studies: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
StudyInstanceUID: PropTypes.string.isRequired,
|
|
||||||
StudyDate: PropTypes.string,
|
|
||||||
displaySets: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
displaySetInstanceUID: PropTypes.string.isRequired,
|
|
||||||
SeriesDescription: PropTypes.string,
|
|
||||||
SeriesNumber: PropTypes.number,
|
|
||||||
InstanceNumber: PropTypes.number,
|
|
||||||
numImageFrames: PropTypes.number,
|
|
||||||
Modality: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
getImageId: PropTypes.func.isRequired,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
studyInstanceUIDs: PropTypes.array,
|
|
||||||
activeServer: PropTypes.shape({
|
|
||||||
type: PropTypes.string,
|
|
||||||
wadoRoot: PropTypes.string,
|
|
||||||
}),
|
|
||||||
onTimepointsUpdated: PropTypes.func,
|
|
||||||
onMeasurementsUpdated: PropTypes.func,
|
|
||||||
// window.store.getState().viewports.viewportSpecificData
|
|
||||||
viewports: PropTypes.object.isRequired,
|
|
||||||
// window.store.getState().viewports.activeViewportIndex
|
|
||||||
activeViewportIndex: PropTypes.number.isRequired,
|
|
||||||
isStudyLoaded: PropTypes.bool,
|
|
||||||
dialog: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const { activeServer } = this.props;
|
|
||||||
const server = Object.assign({}, activeServer);
|
|
||||||
|
|
||||||
OHIF.measurements.MeasurementApi.setConfiguration({
|
|
||||||
dataExchange: {
|
|
||||||
retrieve: DICOMSR.retrieveMeasurements,
|
|
||||||
store: DICOMSR.storeMeasurements,
|
|
||||||
},
|
|
||||||
server,
|
|
||||||
});
|
|
||||||
|
|
||||||
OHIF.measurements.TimepointApi.setConfiguration({
|
|
||||||
dataExchange: {
|
|
||||||
retrieve: this.retrieveTimepoints,
|
|
||||||
store: this.storeTimepoints,
|
|
||||||
remove: this.removeTimepoint,
|
|
||||||
update: this.updateTimepoint,
|
|
||||||
disassociate: this.disassociateStudy,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
isLeftSidePanelOpen: true,
|
|
||||||
isRightSidePanelOpen: false,
|
|
||||||
selectedRightSidePanel: '',
|
|
||||||
selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this
|
|
||||||
thumbnails: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.props.dialog) {
|
|
||||||
this.props.dialog.dismissAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retrieveTimepoints = filter => {
|
|
||||||
OHIF.log.info('retrieveTimepoints');
|
|
||||||
|
|
||||||
// Get the earliest and latest study date
|
|
||||||
let earliestDate = new Date().toISOString();
|
|
||||||
let latestDate = new Date().toISOString();
|
|
||||||
if (this.props.studies) {
|
|
||||||
latestDate = new Date('1000-01-01').toISOString();
|
|
||||||
this.props.studies.forEach(study => {
|
|
||||||
const StudyDate = moment(study.StudyDate, 'YYYYMMDD').toISOString();
|
|
||||||
if (StudyDate < earliestDate) {
|
|
||||||
earliestDate = StudyDate;
|
|
||||||
}
|
|
||||||
if (StudyDate > latestDate) {
|
|
||||||
latestDate = StudyDate;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a generic timepoint
|
|
||||||
return Promise.resolve([
|
|
||||||
{
|
|
||||||
timepointType: 'baseline',
|
|
||||||
timepointId: 'TimepointId',
|
|
||||||
studyInstanceUIDs: this.props.studyInstanceUIDs,
|
|
||||||
PatientID: filter.PatientID,
|
|
||||||
earliestDate,
|
|
||||||
latestDate,
|
|
||||||
isLocked: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
storeTimepoints = timepointData => {
|
|
||||||
OHIF.log.info('storeTimepoints');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateTimepoint = (timepointData, query) => {
|
|
||||||
OHIF.log.info('updateTimepoint');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
removeTimepoint = timepointId => {
|
|
||||||
OHIF.log.info('removeTimepoint');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
disassociateStudy = (timepointIds, StudyInstanceUID) => {
|
|
||||||
OHIF.log.info('disassociateStudy');
|
|
||||||
return Promise.resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
onTimepointsUpdated = timepoints => {
|
|
||||||
if (this.props.onTimepointsUpdated) {
|
|
||||||
this.props.onTimepointsUpdated(timepoints);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMeasurementsUpdated = measurements => {
|
|
||||||
if (this.props.onMeasurementsUpdated) {
|
|
||||||
this.props.onMeasurementsUpdated(measurements);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { studies, isStudyLoaded } = this.props;
|
|
||||||
const { TimepointApi, MeasurementApi } = OHIF.measurements;
|
|
||||||
const currentTimepointId = 'TimepointId';
|
|
||||||
|
|
||||||
const timepointApi = new TimepointApi(currentTimepointId, {
|
|
||||||
onTimepointsUpdated: this.onTimepointsUpdated,
|
|
||||||
});
|
|
||||||
|
|
||||||
const measurementApi = new MeasurementApi(timepointApi, {
|
|
||||||
onMeasurementsUpdated: this.onMeasurementsUpdated,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentTimepointId = currentTimepointId;
|
|
||||||
this.timepointApi = timepointApi;
|
|
||||||
this.measurementApi = measurementApi;
|
|
||||||
|
|
||||||
if (studies) {
|
|
||||||
const PatientID = studies[0] && studies[0].PatientID;
|
|
||||||
|
|
||||||
timepointApi.retrieveTimepoints({ PatientID });
|
|
||||||
if (isStudyLoaded) {
|
|
||||||
this.measurementApi.retrieveMeasurements(PatientID, [
|
|
||||||
currentTimepointId,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
thumbnails: _mapStudiesToThumbnails(studies),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { studies, isStudyLoaded } = this.props;
|
|
||||||
if (studies !== prevProps.studies) {
|
|
||||||
this.setState({
|
|
||||||
thumbnails: _mapStudiesToThumbnails(studies),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) {
|
|
||||||
const PatientID = studies[0] && studies[0].PatientID;
|
|
||||||
const { currentTimepointId } = this;
|
|
||||||
|
|
||||||
this.timepointApi.retrieveTimepoints({ PatientID });
|
|
||||||
this.measurementApi.retrieveMeasurements(PatientID, [currentTimepointId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let VisiblePanelLeft, VisiblePanelRight;
|
|
||||||
const panelExtensions = extensionManager.modules[MODULE_TYPES.PANEL];
|
|
||||||
|
|
||||||
panelExtensions.forEach(panelExt => {
|
|
||||||
panelExt.module.components.forEach(comp => {
|
|
||||||
if (comp.id === this.state.selectedRightSidePanel) {
|
|
||||||
VisiblePanelRight = comp.component;
|
|
||||||
} else if (comp.id === this.state.selectedLeftSidePanel) {
|
|
||||||
VisiblePanelLeft = comp.component;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* HEADER */}
|
|
||||||
<WhiteLabelingContext.Consumer>
|
|
||||||
{whiteLabeling => (
|
|
||||||
<UserManagerContext.Consumer>
|
|
||||||
{userManager => (
|
|
||||||
<AppContext.Consumer>
|
|
||||||
{appContext => (
|
|
||||||
<ConnectedHeader
|
|
||||||
linkText={
|
|
||||||
appContext.appConfig.showStudyList
|
|
||||||
? 'Study List'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
linkPath={
|
|
||||||
appContext.appConfig.showStudyList ? '/' : undefined
|
|
||||||
}
|
|
||||||
userManager={userManager}
|
|
||||||
>
|
|
||||||
{whiteLabeling &&
|
|
||||||
whiteLabeling.createLogoComponentFn &&
|
|
||||||
whiteLabeling.createLogoComponentFn(React)}
|
|
||||||
</ConnectedHeader>
|
|
||||||
)}
|
|
||||||
</AppContext.Consumer>
|
|
||||||
)}
|
|
||||||
</UserManagerContext.Consumer>
|
|
||||||
)}
|
|
||||||
</WhiteLabelingContext.Consumer>
|
|
||||||
|
|
||||||
{/* TOOLBAR */}
|
|
||||||
<ToolbarRow
|
|
||||||
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
|
||||||
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
|
||||||
selectedLeftSidePanel={
|
|
||||||
this.state.isLeftSidePanelOpen
|
|
||||||
? this.state.selectedLeftSidePanel
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
selectedRightSidePanel={
|
|
||||||
this.state.isRightSidePanelOpen
|
|
||||||
? this.state.selectedRightSidePanel
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
handleSidePanelChange={(side, selectedPanel) => {
|
|
||||||
const sideClicked = side && side[0].toUpperCase() + side.slice(1);
|
|
||||||
const openKey = `is${sideClicked}SidePanelOpen`;
|
|
||||||
const selectedKey = `selected${sideClicked}SidePanel`;
|
|
||||||
const updatedState = Object.assign({}, this.state);
|
|
||||||
|
|
||||||
const isOpen = updatedState[openKey];
|
|
||||||
const prevSelectedPanel = updatedState[selectedKey];
|
|
||||||
// RoundedButtonGroup returns `null` if selected button is clicked
|
|
||||||
const isSameSelectedPanel =
|
|
||||||
prevSelectedPanel === selectedPanel || selectedPanel === null;
|
|
||||||
|
|
||||||
updatedState[selectedKey] = selectedPanel || prevSelectedPanel;
|
|
||||||
|
|
||||||
const isClosedOrShouldClose = !isOpen || isSameSelectedPanel;
|
|
||||||
if (isClosedOrShouldClose) {
|
|
||||||
updatedState[openKey] = !updatedState[openKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(updatedState);
|
|
||||||
}}
|
|
||||||
studies={this.props.studies}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
|
||||||
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
|
||||||
|
|
||||||
{/* VIEWPORTS + SIDEPANELS */}
|
|
||||||
<div className="FlexboxLayout">
|
|
||||||
{/* LEFT */}
|
|
||||||
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
|
|
||||||
{VisiblePanelLeft ? (
|
|
||||||
<VisiblePanelLeft
|
|
||||||
viewports={this.props.viewports}
|
|
||||||
studies={this.props.studies}
|
|
||||||
activeIndex={this.props.activeViewportIndex}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ConnectedStudyBrowser
|
|
||||||
studies={this.state.thumbnails}
|
|
||||||
studyMetadata={this.props.studies}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SidePanel>
|
|
||||||
|
|
||||||
{/* MAIN */}
|
|
||||||
<div className={classNames('main-content')}>
|
|
||||||
<ConnectedViewerMain
|
|
||||||
studies={this.props.studies}
|
|
||||||
isStudyLoaded={this.props.isStudyLoaded}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* RIGHT */}
|
|
||||||
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
|
|
||||||
{VisiblePanelRight && (
|
|
||||||
<VisiblePanelRight
|
|
||||||
isOpen={this.state.isRightSidePanelOpen}
|
|
||||||
viewports={this.props.viewports}
|
|
||||||
studies={this.props.studies}
|
|
||||||
activeIndex={this.props.activeViewportIndex}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SidePanel>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withDialog(Viewer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What types are these? Why do we have "mapping" dropped in here instead of in
|
|
||||||
* a mapping layer?
|
|
||||||
*
|
|
||||||
* TODO[react]:
|
|
||||||
* - Add sorting of display sets
|
|
||||||
* - Add showStackLoadingProgressBar option
|
|
||||||
*
|
|
||||||
* @param {Study[]} studies
|
|
||||||
* @param {DisplaySet[]} studies[].displaySets
|
|
||||||
*/
|
|
||||||
const _mapStudiesToThumbnails = function(studies) {
|
|
||||||
return studies.map(study => {
|
|
||||||
const { StudyInstanceUID } = study;
|
|
||||||
|
|
||||||
const thumbnails = study.displaySets.map(displaySet => {
|
|
||||||
const {
|
|
||||||
displaySetInstanceUID,
|
|
||||||
SeriesDescription,
|
|
||||||
SeriesNumber,
|
|
||||||
InstanceNumber,
|
|
||||||
numImageFrames,
|
|
||||||
} = displaySet;
|
|
||||||
|
|
||||||
let imageId;
|
|
||||||
let altImageText;
|
|
||||||
|
|
||||||
if (displaySet.Modality && displaySet.Modality === 'SEG') {
|
|
||||||
// TODO: We want to replace this with a thumbnail showing
|
|
||||||
// the segmentation map on the image, but this is easier
|
|
||||||
// and better than what we have right now.
|
|
||||||
altImageText = 'SEG';
|
|
||||||
} else if (displaySet.images && displaySet.images.length) {
|
|
||||||
const imageIndex = Math.floor(displaySet.images.length / 2);
|
|
||||||
|
|
||||||
imageId = displaySet.images[imageIndex].getImageId();
|
|
||||||
} else {
|
|
||||||
altImageText = displaySet.Modality ? displaySet.Modality : 'UN';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
imageId,
|
|
||||||
altImageText,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
SeriesDescription,
|
|
||||||
SeriesNumber,
|
|
||||||
InstanceNumber,
|
|
||||||
numImageFrames,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
StudyInstanceUID,
|
|
||||||
thumbnails,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,151 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { metadata, utils } from '@ohif/core';
|
|
||||||
|
|
||||||
import ConnectedViewer from './ConnectedViewer.js';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { extensionManager } from './../App.jsx';
|
|
||||||
import Dropzone from 'react-dropzone';
|
|
||||||
import filesToStudies from '../lib/filesToStudies';
|
|
||||||
import './ViewerLocalFileData.css';
|
|
||||||
import { withTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
const { OHIFStudyMetadata } = metadata;
|
|
||||||
const { studyMetadataManager } = utils;
|
|
||||||
|
|
||||||
const dropZoneLinkDialog = (onDrop, i18n, dir) => {
|
|
||||||
return (
|
|
||||||
<Dropzone onDrop={onDrop} noDrag>
|
|
||||||
{({ getRootProps, getInputProps }) => (
|
|
||||||
<span {...getRootProps()} className="link-dialog">
|
|
||||||
{dir ? (
|
|
||||||
<span>
|
|
||||||
{i18n('Load folders')}
|
|
||||||
<input
|
|
||||||
{...getInputProps()}
|
|
||||||
webkitdirectory="true"
|
|
||||||
mozdirectory="true"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
{i18n('Load files')}
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Dropzone>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const linksDialogMessage = (onDrop, i18n) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{i18n('Or click to ')}
|
|
||||||
{dropZoneLinkDialog(onDrop, i18n)}
|
|
||||||
{i18n(' or ')}
|
|
||||||
{dropZoneLinkDialog(onDrop, i18n, true)}
|
|
||||||
{i18n(' from dialog')}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
class ViewerLocalFileData extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
studies: PropTypes.array,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
studies: null,
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
updateStudies = studies => {
|
|
||||||
// Render the viewer when the data is ready
|
|
||||||
studyMetadataManager.purge();
|
|
||||||
|
|
||||||
// Map studies to new format, update metadata manager?
|
|
||||||
const updatedStudies = studies.map(study => {
|
|
||||||
const studyMetadata = new OHIFStudyMetadata(
|
|
||||||
study,
|
|
||||||
study.StudyInstanceUID
|
|
||||||
);
|
|
||||||
const sopClassHandlerModules =
|
|
||||||
extensionManager.modules['sopClassHandlerModule'];
|
|
||||||
|
|
||||||
study.displaySets =
|
|
||||||
study.displaySets ||
|
|
||||||
studyMetadata.createDisplaySets(sopClassHandlerModules);
|
|
||||||
studyMetadata.setDisplaySets(study.displaySets);
|
|
||||||
|
|
||||||
studyMetadata.forEachDisplaySet(displayset => {
|
|
||||||
displayset.localFile = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
studyMetadataManager.add(studyMetadata);
|
|
||||||
|
|
||||||
return study;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
studies: updatedStudies,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const onDrop = async acceptedFiles => {
|
|
||||||
this.setState({ loading: true });
|
|
||||||
|
|
||||||
const studies = await filesToStudies(acceptedFiles);
|
|
||||||
const updatedStudies = this.updateStudies(studies);
|
|
||||||
|
|
||||||
if (!updatedStudies) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ studies: updatedStudies, loading: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.state.error) {
|
|
||||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropzone onDrop={onDrop} noClick>
|
|
||||||
{({ getRootProps, getInputProps }) => (
|
|
||||||
<div {...getRootProps()} style={{ width: '100%', height: '100%' }}>
|
|
||||||
{this.state.studies ? (
|
|
||||||
<ConnectedViewer
|
|
||||||
studies={this.state.studies}
|
|
||||||
studyInstanceUIDs={
|
|
||||||
this.state.studies &&
|
|
||||||
this.state.studies.map(a => a.StudyInstanceUID)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className={'drag-drop-instructions'}>
|
|
||||||
<div className={'drag-drop-contents'}>
|
|
||||||
{this.state.loading ? (
|
|
||||||
<h3>{this.props.t('Loading...')}</h3>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<h3>
|
|
||||||
{this.props.t(
|
|
||||||
'Drag and Drop DICOM files here to load them in the Viewer'
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<h4>{linksDialogMessage(onDrop, this.props.t)}</h4>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Dropzone>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withTranslation('Common')(ViewerLocalFileData);
|
|
||||||
@ -1,193 +0,0 @@
|
|||||||
import './ViewerMain.css';
|
|
||||||
|
|
||||||
import { Component } from 'react';
|
|
||||||
import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import memoize from 'lodash/memoize';
|
|
||||||
import _values from 'lodash/values';
|
|
||||||
|
|
||||||
var values = memoize(_values);
|
|
||||||
|
|
||||||
class ViewerMain extends Component {
|
|
||||||
static propTypes = {
|
|
||||||
activeViewportIndex: PropTypes.number.isRequired,
|
|
||||||
studies: PropTypes.array,
|
|
||||||
viewportSpecificData: PropTypes.object.isRequired,
|
|
||||||
layout: PropTypes.object.isRequired,
|
|
||||||
setViewportSpecificData: PropTypes.func.isRequired,
|
|
||||||
clearViewportSpecificData: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
displaySets: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getDisplaySets(studies) {
|
|
||||||
const displaySets = [];
|
|
||||||
studies.forEach(study => {
|
|
||||||
study.displaySets.forEach(dSet => {
|
|
||||||
if (!dSet.plugin) {
|
|
||||||
dSet.plugin = 'cornerstone';
|
|
||||||
}
|
|
||||||
displaySets.push(dSet);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return displaySets;
|
|
||||||
}
|
|
||||||
|
|
||||||
findDisplaySet(studies, StudyInstanceUID, displaySetInstanceUID) {
|
|
||||||
const study = studies.find(study => {
|
|
||||||
return study.StudyInstanceUID === StudyInstanceUID;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!study) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return study.displaySets.find(displaySet => {
|
|
||||||
return displaySet.displaySetInstanceUID === displaySetInstanceUID;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// Add beforeUnload event handler to check for unsaved changes
|
|
||||||
//window.addEventListener('beforeunload', unloadHandlers.beforeUnload);
|
|
||||||
|
|
||||||
// Get all the display sets for the viewer studies
|
|
||||||
if (this.props.studies) {
|
|
||||||
const displaySets = this.getDisplaySets(this.props.studies);
|
|
||||||
this.setState({ displaySets }, this.fillEmptyViewportPanes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const prevViewportAmount = prevProps.layout.viewports.length;
|
|
||||||
const viewportAmount = this.props.layout.viewports.length;
|
|
||||||
const isVtk = this.props.layout.viewports.some(vp => !!vp.vtk);
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.props.studies !== prevProps.studies ||
|
|
||||||
(viewportAmount !== prevViewportAmount && !isVtk)
|
|
||||||
) {
|
|
||||||
const displaySets = this.getDisplaySets(this.props.studies);
|
|
||||||
this.setState({ displaySets }, this.fillEmptyViewportPanes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fillEmptyViewportPanes = () => {
|
|
||||||
// TODO: Here is the entry point for filling viewports on load.
|
|
||||||
const dirtyViewportPanes = [];
|
|
||||||
const { layout, viewportSpecificData } = this.props;
|
|
||||||
const { displaySets } = this.state;
|
|
||||||
|
|
||||||
if (!displaySets || !displaySets.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < layout.viewports.length; i++) {
|
|
||||||
const viewportPane = viewportSpecificData[i];
|
|
||||||
const isNonEmptyViewport =
|
|
||||||
viewportPane &&
|
|
||||||
viewportPane.StudyInstanceUID &&
|
|
||||||
viewportPane.displaySetInstanceUID;
|
|
||||||
|
|
||||||
if (isNonEmptyViewport) {
|
|
||||||
dirtyViewportPanes.push({
|
|
||||||
StudyInstanceUID: viewportPane.StudyInstanceUID,
|
|
||||||
displaySetInstanceUID: viewportPane.displaySetInstanceUID,
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const foundDisplaySet =
|
|
||||||
displaySets.find(
|
|
||||||
ds =>
|
|
||||||
!dirtyViewportPanes.some(
|
|
||||||
v => v.displaySetInstanceUID === ds.displaySetInstanceUID
|
|
||||||
)
|
|
||||||
) || displaySets[displaySets.length - 1];
|
|
||||||
|
|
||||||
dirtyViewportPanes.push(foundDisplaySet);
|
|
||||||
}
|
|
||||||
|
|
||||||
dirtyViewportPanes.forEach((vp, i) => {
|
|
||||||
if (vp && vp.StudyInstanceUID) {
|
|
||||||
this.setViewportData({
|
|
||||||
viewportIndex: i,
|
|
||||||
StudyInstanceUID: vp.StudyInstanceUID,
|
|
||||||
displaySetInstanceUID: vp.displaySetInstanceUID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setViewportData = ({
|
|
||||||
viewportIndex,
|
|
||||||
StudyInstanceUID,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
}) => {
|
|
||||||
const displaySet = this.findDisplaySet(
|
|
||||||
this.props.studies,
|
|
||||||
StudyInstanceUID,
|
|
||||||
displaySetInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
this.props.setViewportSpecificData(viewportIndex, displaySet);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { viewportSpecificData } = this.props;
|
|
||||||
const viewportData = values(viewportSpecificData);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ViewerMain">
|
|
||||||
{this.state.displaySets.length && (
|
|
||||||
<ConnectedViewportGrid
|
|
||||||
isStudyLoaded={this.props.isStudyLoaded}
|
|
||||||
studies={this.props.studies}
|
|
||||||
viewportData={viewportData}
|
|
||||||
setViewportData={this.setViewportData}
|
|
||||||
>
|
|
||||||
{/* Children to add to each viewport that support children */}
|
|
||||||
</ConnectedViewportGrid>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// Clear the entire viewport specific data
|
|
||||||
const { viewportSpecificData } = this.props;
|
|
||||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
|
||||||
this.props.clearViewportSpecificData(viewportIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: These don't have to be viewer specific?
|
|
||||||
// Could qualify for other routes?
|
|
||||||
// hotkeys.destroy();
|
|
||||||
|
|
||||||
// Remove beforeUnload event handler...
|
|
||||||
//window.removeEventListener('beforeunload', unloadHandlers.beforeUnload);
|
|
||||||
// Destroy the synchronizer used to update reference lines
|
|
||||||
//OHIF.viewer.updateImageSynchronizer.destroy();
|
|
||||||
// TODO: Instruct all plugins to clean up themselves
|
|
||||||
//
|
|
||||||
// Clear references to all stacks in the StackManager
|
|
||||||
//StackManager.clearStacks();
|
|
||||||
// @TypeSafeStudies
|
|
||||||
// Clears OHIF.viewer.Studies collection
|
|
||||||
//OHIF.viewer.Studies.removeAll();
|
|
||||||
// @TypeSafeStudies
|
|
||||||
// Clears OHIF.viewer.StudyMetadataList collection
|
|
||||||
//OHIF.viewer.StudyMetadataList.removeAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ViewerMain;
|
|
||||||
@ -1,375 +0,0 @@
|
|||||||
import React, { useState, useEffect, useContext } from 'react';
|
|
||||||
import { metadata, studies, utils, log } from '@ohif/core';
|
|
||||||
import usePrevious from '../customHooks/usePrevious';
|
|
||||||
|
|
||||||
import ConnectedViewer from './ConnectedViewer.js';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { extensionManager } from './../App.jsx';
|
|
||||||
import { useSnackbarContext } from '@ohif/ui';
|
|
||||||
|
|
||||||
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
|
|
||||||
const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies;
|
|
||||||
const { studyMetadataManager, makeCancelable } = utils;
|
|
||||||
|
|
||||||
// Contexts
|
|
||||||
import AppContext from '../context/AppContext';
|
|
||||||
|
|
||||||
const _promoteToFront = (list, value, searchMethod) => {
|
|
||||||
let response = [...list];
|
|
||||||
let promoted = false;
|
|
||||||
const index = response.findIndex(searchMethod.bind(undefined, value));
|
|
||||||
|
|
||||||
if (index > 0) {
|
|
||||||
const first = response.splice(index, 1);
|
|
||||||
response = [...first, ...response];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
promoted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
promoted,
|
|
||||||
data: response,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Promote series to front if find found equivalent on filters object
|
|
||||||
* @param {Object} study - study reference to promote series against
|
|
||||||
* @param {Object} [filters] - Object containing filters to be applied
|
|
||||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
|
||||||
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
|
||||||
*/
|
|
||||||
const _promoteList = (study, studyMetadata, filters, isFilterStrategy) => {
|
|
||||||
let promoted = false;
|
|
||||||
// Promote only if no filter should be applied
|
|
||||||
if (!isFilterStrategy) {
|
|
||||||
_sortStudyDisplaySet(study, studyMetadata);
|
|
||||||
promoted = _promoteStudyDisplaySet(study, studyMetadata, filters);
|
|
||||||
}
|
|
||||||
|
|
||||||
return promoted;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _promoteStudyDisplaySet = (study, studyMetadata, filters) => {
|
|
||||||
let promoted = false;
|
|
||||||
const queryParamsLength = Object.keys(filters).length;
|
|
||||||
const shouldPromoteToFront = queryParamsLength > 0;
|
|
||||||
|
|
||||||
if (shouldPromoteToFront) {
|
|
||||||
const { seriesInstanceUID } = filters;
|
|
||||||
|
|
||||||
const _seriesLookup = (valueToCompare, displaySet) => {
|
|
||||||
return displaySet.SeriesInstanceUID === valueToCompare;
|
|
||||||
};
|
|
||||||
const promotedResponse = _promoteToFront(
|
|
||||||
studyMetadata.getDisplaySets(),
|
|
||||||
seriesInstanceUID,
|
|
||||||
_seriesLookup
|
|
||||||
);
|
|
||||||
|
|
||||||
study.displaySets = promotedResponse.data;
|
|
||||||
promoted = promotedResponse.promoted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return promoted;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to identify if query param (from url) was applied to given list
|
|
||||||
* @param {Object} study - study reference to promote series against
|
|
||||||
* @param {Object} [filters] - Object containing filters to be applied
|
|
||||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
|
||||||
* @param {boolean} isFilterStrategy - if filtering by query param strategy ON
|
|
||||||
*/
|
|
||||||
const _isQueryParamApplied = (study, filters = {}, isFilterStrategy) => {
|
|
||||||
const { seriesInstanceUID } = filters;
|
|
||||||
let applied = true;
|
|
||||||
// skip in case no filter or no toast manager
|
|
||||||
if (!seriesInstanceUID) {
|
|
||||||
return applied;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { series = [], displaySets = [] } = study;
|
|
||||||
const firstSeries = isFilterStrategy ? series[0] : displaySets[0];
|
|
||||||
|
|
||||||
if (!firstSeries || firstSeries.SeriesInstanceUID !== seriesInstanceUID) {
|
|
||||||
applied = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return applied;
|
|
||||||
};
|
|
||||||
const _showUserMessage = (queryParamApplied, message, dialog = {}) => {
|
|
||||||
if (queryParamApplied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { show: showUserMessage = () => {} } = dialog;
|
|
||||||
showUserMessage({
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const _addSeriesToStudy = (studyMetadata, series) => {
|
|
||||||
const sopClassHandlerModules =
|
|
||||||
extensionManager.modules['sopClassHandlerModule'];
|
|
||||||
const study = studyMetadata.getData();
|
|
||||||
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
|
||||||
const existingSeries = studyMetadata.getSeriesByUID(series.SeriesInstanceUID);
|
|
||||||
if (existingSeries) {
|
|
||||||
studyMetadata.updateSeries(series.SeriesInstanceUID, seriesMetadata);
|
|
||||||
} else {
|
|
||||||
studyMetadata.addSeries(seriesMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
studyMetadata.createAndAddDisplaySetsForSeries(
|
|
||||||
sopClassHandlerModules,
|
|
||||||
seriesMetadata
|
|
||||||
);
|
|
||||||
study.displaySets = studyMetadata.getDisplaySets();
|
|
||||||
_updateStudyMetadataManager(study, studyMetadata);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _updateStudyMetadataManager = (study, studyMetadata) => {
|
|
||||||
const { StudyInstanceUID } = study;
|
|
||||||
|
|
||||||
if (!studyMetadataManager.get(StudyInstanceUID)) {
|
|
||||||
studyMetadataManager.add(studyMetadata);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const _updateStudyDisplaySets = (study, studyMetadata) => {
|
|
||||||
const sopClassHandlerModules =
|
|
||||||
extensionManager.modules['sopClassHandlerModule'];
|
|
||||||
|
|
||||||
if (!study.displaySets) {
|
|
||||||
study.displaySets = studyMetadata.createDisplaySets(sopClassHandlerModules);
|
|
||||||
}
|
|
||||||
|
|
||||||
studyMetadata.setDisplaySets(study.displaySets);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _sortStudyDisplaySet = (study, studyMetadata) => {
|
|
||||||
studyMetadata.sortDisplaySets(study.displaySets);
|
|
||||||
};
|
|
||||||
|
|
||||||
function ViewerRetrieveStudyData({
|
|
||||||
server,
|
|
||||||
studyInstanceUIDs,
|
|
||||||
seriesInstanceUIDs,
|
|
||||||
clearViewportSpecificData,
|
|
||||||
}) {
|
|
||||||
// hooks
|
|
||||||
const [error, setError] = useState(false);
|
|
||||||
const [studies, setStudies] = useState([]);
|
|
||||||
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
|
|
||||||
const snackbarContext = useSnackbarContext();
|
|
||||||
const { appConfig = {} } = useContext(AppContext);
|
|
||||||
const {
|
|
||||||
filterQueryParam: isFilterStrategy = false,
|
|
||||||
maxConcurrentMetadataRequests,
|
|
||||||
} = appConfig;
|
|
||||||
|
|
||||||
let cancelableSeriesPromises;
|
|
||||||
let cancelableStudiesPromises;
|
|
||||||
/**
|
|
||||||
* Callback method when study is totally loaded
|
|
||||||
* @param {object} study study loaded
|
|
||||||
* @param {object} studyMetadata studyMetadata for given study
|
|
||||||
* @param {Object} [filters] - Object containing filters to be applied
|
|
||||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
|
||||||
*/
|
|
||||||
const studyDidLoad = (study, studyMetadata, filters) => {
|
|
||||||
// User message
|
|
||||||
const promoted = _promoteList(
|
|
||||||
study,
|
|
||||||
studyMetadata,
|
|
||||||
filters,
|
|
||||||
isFilterStrategy
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear viewport to allow new promoted one to be displayed
|
|
||||||
if (promoted) {
|
|
||||||
clearViewportSpecificData(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isQueryParamApplied = _isQueryParamApplied(
|
|
||||||
study,
|
|
||||||
filters,
|
|
||||||
isFilterStrategy
|
|
||||||
);
|
|
||||||
// Show message in case not promoted neither filtered but should to
|
|
||||||
_showUserMessage(
|
|
||||||
isQueryParamApplied,
|
|
||||||
'Query parameters were not applied. Using original series list for given study.',
|
|
||||||
snackbarContext
|
|
||||||
);
|
|
||||||
|
|
||||||
setStudies([...studies, study]);
|
|
||||||
setIsStudyLoaded(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to process studies. It will update displaySet, studyMetadata, load remaining series, ...
|
|
||||||
* @param {Array} studiesData Array of studies retrieved from server
|
|
||||||
* @param {Object} [filters] - Object containing filters to be applied
|
|
||||||
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
|
|
||||||
*/
|
|
||||||
const processStudies = (studiesData, filters) => {
|
|
||||||
if (Array.isArray(studiesData) && studiesData.length > 0) {
|
|
||||||
// Map studies to new format, update metadata manager?
|
|
||||||
const studies = studiesData.map(study => {
|
|
||||||
const studyMetadata = new OHIFStudyMetadata(
|
|
||||||
study,
|
|
||||||
study.StudyInstanceUID
|
|
||||||
);
|
|
||||||
|
|
||||||
_updateStudyDisplaySets(study, studyMetadata);
|
|
||||||
_updateStudyMetadataManager(study, studyMetadata);
|
|
||||||
|
|
||||||
// Attempt to load remaning series if any
|
|
||||||
cancelableSeriesPromises[study.StudyInstanceUID] = makeCancelable(
|
|
||||||
loadRemainingSeries(studyMetadata)
|
|
||||||
)
|
|
||||||
.then(result => {
|
|
||||||
if (result && !result.isCanceled) {
|
|
||||||
studyDidLoad(study, studyMetadata, filters);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error && !error.isCanceled) {
|
|
||||||
setError(true);
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return study;
|
|
||||||
});
|
|
||||||
|
|
||||||
setStudies(studies);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const forceRerender = () => setStudies(studies => [...studies]);
|
|
||||||
|
|
||||||
const loadRemainingSeries = async studyMetadata => {
|
|
||||||
const { seriesLoader } = studyMetadata.getData();
|
|
||||||
if (!seriesLoader) return;
|
|
||||||
|
|
||||||
const loadNextSeries = async () => {
|
|
||||||
if (!seriesLoader.hasNext()) return;
|
|
||||||
const series = await seriesLoader.next();
|
|
||||||
_addSeriesToStudy(studyMetadata, series);
|
|
||||||
forceRerender();
|
|
||||||
return loadNextSeries();
|
|
||||||
};
|
|
||||||
|
|
||||||
const concurrentRequestsAllowed =
|
|
||||||
maxConcurrentMetadataRequests || studyMetadata.getSeriesCount();
|
|
||||||
const promises = Array(concurrentRequestsAllowed)
|
|
||||||
.fill(null)
|
|
||||||
.map(loadNextSeries);
|
|
||||||
|
|
||||||
return await Promise.all(promises);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadStudies = async () => {
|
|
||||||
try {
|
|
||||||
const filters = {};
|
|
||||||
// Use the first, discard others
|
|
||||||
const seriesInstanceUID = seriesInstanceUIDs && seriesInstanceUIDs[0];
|
|
||||||
|
|
||||||
const retrieveParams = [server, studyInstanceUIDs];
|
|
||||||
|
|
||||||
if (seriesInstanceUID) {
|
|
||||||
filters.seriesInstanceUID = seriesInstanceUID;
|
|
||||||
// Query param filtering controlled by appConfig property
|
|
||||||
if (isFilterStrategy) {
|
|
||||||
retrieveParams.push(filters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelableStudiesPromises[studyInstanceUIDs] = makeCancelable(
|
|
||||||
retrieveStudiesMetadata(...retrieveParams)
|
|
||||||
)
|
|
||||||
.then(result => {
|
|
||||||
if (result && !result.isCanceled) {
|
|
||||||
processStudies(result, filters);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error && !error.isCanceled) {
|
|
||||||
setError(true);
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error) {
|
|
||||||
setError(true);
|
|
||||||
log.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const purgeCancellablePromises = () => {
|
|
||||||
for (let studyInstanceUIDs in cancelableStudiesPromises) {
|
|
||||||
if ('cancel' in cancelableStudiesPromises[studyInstanceUIDs]) {
|
|
||||||
cancelableStudiesPromises[studyInstanceUIDs].cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let studyInstanceUIDs in cancelableSeriesPromises) {
|
|
||||||
if ('cancel' in cancelableSeriesPromises[studyInstanceUIDs]) {
|
|
||||||
cancelableSeriesPromises[studyInstanceUIDs].cancel();
|
|
||||||
deleteStudyMetadataPromise(studyInstanceUIDs);
|
|
||||||
studyMetadataManager.remove(studyInstanceUIDs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevStudyInstanceUIDs = usePrevious(studyInstanceUIDs);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const hasStudyInstanceUIDsChanged = !(
|
|
||||||
prevStudyInstanceUIDs &&
|
|
||||||
prevStudyInstanceUIDs.every(e => studyInstanceUIDs.includes(e))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasStudyInstanceUIDsChanged) {
|
|
||||||
studyMetadataManager.purge();
|
|
||||||
purgeCancellablePromises();
|
|
||||||
}
|
|
||||||
}, [studyInstanceUIDs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
cancelableSeriesPromises = {};
|
|
||||||
cancelableStudiesPromises = {};
|
|
||||||
loadStudies();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
purgeCancellablePromises();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <div>Error: {JSON.stringify(error)}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConnectedViewer
|
|
||||||
studies={studies}
|
|
||||||
isStudyLoaded={isStudyLoaded}
|
|
||||||
studyInstanceUIDs={studyInstanceUIDs}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewerRetrieveStudyData.propTypes = {
|
|
||||||
studyInstanceUIDs: PropTypes.array.isRequired,
|
|
||||||
seriesInstanceUIDs: PropTypes.array,
|
|
||||||
server: PropTypes.object,
|
|
||||||
clearViewportSpecificData: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewerRetrieveStudyData;
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* Finds displaySet by UID across all displaySets inside studyMetadata
|
|
||||||
* @param {Array} studyMetadata
|
|
||||||
* @param {string} displaySetInstanceUID
|
|
||||||
*/
|
|
||||||
export default function findDisplaySetByUID(
|
|
||||||
studyMetadata,
|
|
||||||
displaySetInstanceUID
|
|
||||||
) {
|
|
||||||
if (!Array.isArray(studyMetadata)) return null;
|
|
||||||
|
|
||||||
const allDisplaySets = studyMetadata.reduce((all, current) => {
|
|
||||||
let currentDisplaySet = [];
|
|
||||||
if (current && Array.isArray(current.displaySets)) {
|
|
||||||
currentDisplaySet = current.displaySets;
|
|
||||||
}
|
|
||||||
return all.concat(currentDisplaySet);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const bySetInstanceUID = ds =>
|
|
||||||
ds.displaySetInstanceUID === displaySetInstanceUID;
|
|
||||||
|
|
||||||
const displaySet = allDisplaySets.find(bySetInstanceUID);
|
|
||||||
return displaySet || null;
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import findDisplaySetByUID from './findDisplaySetByUID';
|
|
||||||
|
|
||||||
describe('findDisplaySetByUID', () => {
|
|
||||||
test('returns null when studyMetadata isnt an array', () => {
|
|
||||||
const result = findDisplaySetByUID(undefined, 'hello');
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns null when no match found', () => {
|
|
||||||
const result = findDisplaySetByUID([], 'no-match');
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it handles missing displaySet arrays', () => {
|
|
||||||
const expected = '9388-2291-a8fe';
|
|
||||||
const studyMetadata = [
|
|
||||||
{ displaySets: null },
|
|
||||||
{
|
|
||||||
displaySets: [{ displaySetInstanceUID: expected }],
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
7,
|
|
||||||
];
|
|
||||||
const result = findDisplaySetByUID(studyMetadata, expected);
|
|
||||||
expect(result.displaySetInstanceUID).toBe(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns correct displaySet by UID', () => {
|
|
||||||
const expected = '1234-5678';
|
|
||||||
const studyMetadata = [
|
|
||||||
{ displaySets: [{ displaySetInstanceUID: '0011-2239' }] },
|
|
||||||
{
|
|
||||||
displaySets: [
|
|
||||||
{ displaySetInstanceUID: '0392-2211' },
|
|
||||||
{ displaySetInstanceUID: expected },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ displaySets: [{ displaySetInstanceUID: '3384-9933' }] },
|
|
||||||
];
|
|
||||||
const result = findDisplaySetByUID(studyMetadata, expected);
|
|
||||||
expect(result.displaySetInstanceUID).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user