🧹🧹🧹 Don't mind me. Just moving things around
This commit is contained in:
1 parent
9a0d295ee6
commit
72cdebccf4
83 files changed
+479
-38
No files matched your search
+286
@@ -0,0 +1,286 @@
|
||||
import React, { Component } from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { createStore, combineReducers } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF from 'ohif-core';
|
||||
import './config';
|
||||
import ui from './redux/ui.js';
|
||||
import OHIFStandaloneViewer from './OHIFStandaloneViewer';
|
||||
import WhiteLabellingContext from './WhiteLabellingContext';
|
||||
import OHIFCornerstoneExtension from 'ohif-cornerstone-extension';
|
||||
import OHIFVTKExtension from 'ohif-vtk-extension';
|
||||
import OHIFDicomPDFExtension from 'ohif-dicom-pdf-extension';
|
||||
import OHIFDicomHtmlExtension from 'ohif-dicom-html-extension';
|
||||
import OHIFDicomMicroscopyExtension from 'ohif-dicom-microscopy-extension';
|
||||
import {
|
||||
loadUser,
|
||||
OidcProvider,
|
||||
createUserManager,
|
||||
reducer as oidcReducer
|
||||
} from 'redux-oidc';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
|
||||
const { ExtensionManager } = OHIF.extensions;
|
||||
|
||||
const Icons = 'icons.svg';
|
||||
|
||||
const { reducers, localStorage } = OHIF.redux;
|
||||
reducers.ui = ui;
|
||||
reducers.oidc = oidcReducer;
|
||||
|
||||
const combined = combineReducers(reducers);
|
||||
const store = createStore(combined, localStorage.loadState());
|
||||
|
||||
store.subscribe(() => {
|
||||
localStorage.saveState({
|
||||
preferences: store.getState().preferences
|
||||
});
|
||||
});
|
||||
|
||||
const defaultButtons = [
|
||||
{
|
||||
command: 'StackScroll',
|
||||
type: 'tool',
|
||||
text: 'Stack Scroll',
|
||||
svgUrl: `${Icons}#icon-tools-stack-scroll`,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
command: 'Zoom',
|
||||
type: 'tool',
|
||||
text: 'Zoom',
|
||||
svgUrl: `${Icons}#icon-tools-zoom`,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
command: 'Wwwc',
|
||||
type: 'tool',
|
||||
text: 'Levels',
|
||||
svgUrl: `${Icons}#icon-tools-levels`,
|
||||
active: true
|
||||
},
|
||||
{
|
||||
command: 'Pan',
|
||||
type: 'tool',
|
||||
text: 'Pan',
|
||||
svgUrl: `${Icons}#icon-tools-pan`,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
command: 'Length',
|
||||
type: 'tool',
|
||||
text: 'Length',
|
||||
svgUrl: `${Icons}#icon-tools-measure-temp`,
|
||||
active: false
|
||||
},
|
||||
/*{
|
||||
command: 'Annotate',
|
||||
type: 'tool',
|
||||
text: 'Annotate',
|
||||
svgUrl: `${Icons}#icon-tools-measure-non-target`,
|
||||
active: false
|
||||
},*/
|
||||
{
|
||||
command: 'Angle',
|
||||
type: 'tool',
|
||||
text: 'Angle',
|
||||
iconClasses: 'fa fa-angle-left',
|
||||
active: false
|
||||
},
|
||||
{
|
||||
command: 'Bidirectional',
|
||||
type: 'tool',
|
||||
text: 'Bidirectional',
|
||||
svgUrl: `${Icons}#icon-tools-measure-target`,
|
||||
active: false
|
||||
},
|
||||
{
|
||||
command: 'Brush',
|
||||
type: 'tool',
|
||||
text: 'Brush',
|
||||
iconClasses: 'fa fa-circle',
|
||||
active: false
|
||||
},
|
||||
{
|
||||
command: 'FreehandMouse',
|
||||
type: 'tool',
|
||||
text: 'Freehand',
|
||||
iconClasses: 'fa fa-star',
|
||||
active: false
|
||||
},
|
||||
{
|
||||
command: 'reset',
|
||||
type: 'command',
|
||||
text: 'Reset',
|
||||
svgUrl: `${Icons}#icon-tools-reset`,
|
||||
active: false
|
||||
}
|
||||
];
|
||||
|
||||
const buttonsAction = OHIF.redux.actions.setAvailableButtons(defaultButtons);
|
||||
|
||||
store.dispatch(buttonsAction);
|
||||
|
||||
const availableTools = [
|
||||
{ name: 'Pan', mouseButtonMasks: [1, 4] },
|
||||
{ name: 'Zoom', mouseButtonMasks: [1, 2] },
|
||||
{ name: 'Wwwc', mouseButtonMasks: [1] },
|
||||
{ name: 'Bidirectional', mouseButtonMasks: [1] },
|
||||
{ name: 'Length', mouseButtonMasks: [1] },
|
||||
{ name: 'Angle', mouseButtonMasks: [1] },
|
||||
{ name: 'StackScroll', mouseButtonMasks: [1] },
|
||||
{ name: 'Brush', mouseButtonMasks: [1] },
|
||||
{ name: 'FreehandMouse', mouseButtonMasks: [1] },
|
||||
{ name: 'PanMultiTouch' },
|
||||
{ name: 'ZoomTouchPinch' },
|
||||
{ name: 'StackScrollMouseWheel' },
|
||||
{ name: 'StackScrollMultiTouch' }
|
||||
];
|
||||
|
||||
const toolAction = OHIF.redux.actions.setExtensionData('cornerstone', {
|
||||
availableTools
|
||||
});
|
||||
|
||||
store.dispatch(toolAction);
|
||||
|
||||
/** TODO: extensions should be passed in as prop as soon as we have the extensions as separate packages and then registered by ExtensionsManager */
|
||||
const extensions = [
|
||||
new OHIFCornerstoneExtension(),
|
||||
new OHIFVTKExtension(),
|
||||
new OHIFDicomPDFExtension(),
|
||||
new OHIFDicomHtmlExtension(),
|
||||
new OHIFDicomMicroscopyExtension()
|
||||
];
|
||||
ExtensionManager.registerExtensions(store, extensions);
|
||||
|
||||
// TODO[react] Use a provider when the whole tree is React
|
||||
window.store = store;
|
||||
|
||||
function handleServers(servers) {
|
||||
if (servers) {
|
||||
OHIF.utils.addServers(servers, store);
|
||||
}
|
||||
}
|
||||
|
||||
function handleOIDC(oidc) {
|
||||
if (!oidc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oidcClient = oidc[0];
|
||||
|
||||
const settings = {
|
||||
authority: oidcClient.authServerUrl,
|
||||
client_id: oidcClient.clientId,
|
||||
redirect_uri: oidcClient.authRedirectUri,
|
||||
silent_redirect_uri: '/silent-refresh.html',
|
||||
post_logout_redirect_uri: oidcClient.postLogoutRedirectUri,
|
||||
response_type: oidcClient.responseType,
|
||||
scope: 'email profile openid', // Note: Request must have scope 'openid' to be considered an OpenID Connect request
|
||||
automaticSilentRenew: true,
|
||||
revokeAccessTokenOnSignout: true,
|
||||
filterProtocolClaims: true,
|
||||
loadUserInfo: true,
|
||||
extraQueryParams: oidcClient.extraQueryParams
|
||||
};
|
||||
|
||||
const userManager = createUserManager(settings);
|
||||
|
||||
loadUser(store, userManager);
|
||||
|
||||
return userManager;
|
||||
}
|
||||
|
||||
function handleWebWorkerInit(basename) {
|
||||
const config = {
|
||||
maxWebWorkers: Math.max(navigator.hardwareConcurrency - 1, 1),
|
||||
startWebWorkersOnDemand: true,
|
||||
webWorkerPath: basename + '/cornerstoneWADOImageLoaderWebWorker.min.js',
|
||||
taskConfiguration: {
|
||||
decodeTask: {
|
||||
loadCodecsOnStartup: true,
|
||||
initializeCodecsOnStartup: false,
|
||||
codecsPath: basename + '/cornerstoneWADOImageLoaderCodecs.min.js',
|
||||
usePDFJS: false,
|
||||
strict: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or update the HTML Base tag present on the document
|
||||
* to point to the provided ROOT Url.
|
||||
*
|
||||
* @param rootUrl
|
||||
*/
|
||||
function updateBaseTag(rootUrl) {
|
||||
const bases = document.getElementsByTagName('base');
|
||||
let baseHref = rootUrl;
|
||||
baseHref += baseHref.endsWith('/') ? '' : '/';
|
||||
|
||||
if (bases.length > 1) {
|
||||
throw new Error('For some reason there is more than one base tag present.');
|
||||
} else if (bases.length === 1) {
|
||||
bases[0].href = baseHref;
|
||||
} else {
|
||||
document.write(`<base href='${baseHref}'/>`);
|
||||
}
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
static propTypes = {
|
||||
servers: PropTypes.object,
|
||||
oidc: PropTypes.array,
|
||||
routerBasename: PropTypes.string,
|
||||
rootUrl: PropTypes.string,
|
||||
userManager: PropTypes.object,
|
||||
location: PropTypes.object
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
whiteLabelling: {}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.userManager = handleOIDC(this.props.oidc);
|
||||
handleServers(this.props.servers);
|
||||
handleWebWorkerInit(this.props.rootUrl);
|
||||
updateBaseTag(this.props.rootUrl);
|
||||
}
|
||||
|
||||
render() {
|
||||
const userManager = this.userManager;
|
||||
|
||||
if (userManager) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<OidcProvider store={store} userManager={userManager}>
|
||||
<BrowserRouter basename={this.props.routerBasename}>
|
||||
<WhiteLabellingContext.Provider value={this.props.whiteLabelling}>
|
||||
<OHIFStandaloneViewer userManager={userManager} />
|
||||
</WhiteLabellingContext.Provider>
|
||||
</BrowserRouter>
|
||||
</OidcProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<BrowserRouter basename={this.props.routerBasename}>
|
||||
<WhiteLabellingContext.Provider value={this.props.whiteLabelling}>
|
||||
<OHIFStandaloneViewer />
|
||||
</WhiteLabellingContext.Provider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CallbackComponent } from 'redux-oidc';
|
||||
|
||||
class CallbackPage extends Component {
|
||||
static propTypes = {
|
||||
userManager: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CallbackComponent
|
||||
userManager={this.props.userManager}
|
||||
successCallback={() => {
|
||||
const pathname = sessionStorage.getItem('ohif-redirect-to');
|
||||
|
||||
this.props.history.push(pathname);
|
||||
}}
|
||||
errorCallback={error => {
|
||||
//this.props.history.push("/");
|
||||
throw new Error(error);
|
||||
}}
|
||||
>
|
||||
<div>Redirecting...</div>
|
||||
</CallbackComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(CallbackPage);
|
||||
@@ -0,0 +1,17 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
background-color: black;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-family: Roboto,OpenSans,HelveticaNeue-Light,Helvetica Neue Light,Helvetica Neue,Helvetica,Arial,Lucida Grande,sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { ViewerbaseDragDropContext } from 'react-viewerbase';
|
||||
import ViewerRouting from './routes/ViewerRouting.js';
|
||||
import StudyListRouting from './studylist/StudyListRouting.js'
|
||||
import StandaloneRouting from './routes/StandaloneRouting.js';
|
||||
import IHEInvokeImageDisplay from './routes/IHEInvokeImageDisplay.js';
|
||||
import CallbackPage from './CallbackPage.js';
|
||||
|
||||
import './OHIFStandaloneViewer.css';
|
||||
import './variables.css';
|
||||
import './theme-tide.css';
|
||||
|
||||
const reload = () => window.location.reload();
|
||||
|
||||
class OHIFStandaloneViewer extends Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.unlisten = this.props.history.listen((location, action) => {
|
||||
if (this.props.setContext) {
|
||||
this.props.setContext(window.location.pathname);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { user, userManager } = this.props;
|
||||
|
||||
const userNotLoggedIn = userManager && (!user || user.expired);
|
||||
if (userNotLoggedIn) {
|
||||
const pathname = this.props.location.pathname;
|
||||
|
||||
if (pathname !== '/callback') {
|
||||
sessionStorage.setItem('ohif-redirect-to', pathname);
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/silent-refresh.html" onEnter={reload} />
|
||||
<Route exact path="/logout-redirect.html" onEnter={reload} />
|
||||
<Route
|
||||
path="/callback"
|
||||
render={() => <CallbackPage userManager={userManager} />}
|
||||
/>
|
||||
<Route
|
||||
component={() => {
|
||||
userManager.signinRedirect();
|
||||
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/silent-refresh.html" onEnter={reload} />
|
||||
<Route exact path="/logout-redirect.html" onEnter={reload} />
|
||||
<Route
|
||||
exact
|
||||
path="/studylist"
|
||||
component={StudyListRouting}
|
||||
/>
|
||||
<Route exact path="/" component={StudyListRouting} />
|
||||
<Route exact path="/viewer" component={StandaloneRouting} />
|
||||
<Route path="/viewer/:studyInstanceUids" component={ViewerRouting} />
|
||||
<Route
|
||||
path="/study/:studyInstanceUid/series/:seriesInstanceUids"
|
||||
component={ViewerRouting}
|
||||
/>
|
||||
<Route
|
||||
path="/IHEInvokeImageDisplay"
|
||||
component={IHEInvokeImageDisplay}
|
||||
/>
|
||||
<Route render={() => <div> Sorry, this page does not exist. </div>} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
user: state.oidc.user
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedOHIFStandaloneViewer = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(OHIFStandaloneViewer);
|
||||
|
||||
export default ViewerbaseDragDropContext(
|
||||
withRouter(ConnectedOHIFStandaloneViewer)
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import OHIFLogo from './components/OHIFLogo/OHIFLogo.js';
|
||||
import React from 'react';
|
||||
|
||||
const defaultContextValues = {
|
||||
logoComponent: OHIFLogo()
|
||||
};
|
||||
|
||||
const WhiteLabellingContext = React.createContext(defaultContextValues);
|
||||
|
||||
export default WhiteLabellingContext;
|
||||
@@ -0,0 +1,108 @@
|
||||
.entry-header {
|
||||
padding: 10px 10px 0;
|
||||
height: var(--top-bar-height);
|
||||
}
|
||||
|
||||
.entry-header.header-big {
|
||||
background: rgba(21,25,30,0.7);
|
||||
padding: 10px var(--study-list-padding);
|
||||
height: auto;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-left-box {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-viewerLink {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.header-brand {
|
||||
height: 30px;
|
||||
text-decoration: none;
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.entry-header.header-big .header-brand {
|
||||
height: auto;
|
||||
padding: 25px 0;
|
||||
}
|
||||
|
||||
|
||||
.header-logo-image {
|
||||
fill: transparent;
|
||||
height: 100%;
|
||||
margin: 0 8px 0 0;
|
||||
width: 30px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.entry-header.header-big .header-logo-image {
|
||||
margin: 0 20px 0 0;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.header-logo-text {
|
||||
display: inline-block;
|
||||
font-family: var(--logo-font-family);
|
||||
font-size: 14px;
|
||||
font-weight: var(--logo-font-weight);
|
||||
color: var(--text-primary-color);
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.entry-header.header-big .header-logo-text {
|
||||
font-size: 30px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
color: var(--text-secondary-color);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.header-btn:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.header-btn:active {
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.header-studyListLinkSection {
|
||||
border-left: var(--ui-border-thickness) solid var(--ui-border-color);
|
||||
margin: 3px 0 0 10px;
|
||||
padding: 0 0 0 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.header-menu {
|
||||
color: var(--text-primary-color);
|
||||
float: right;
|
||||
padding: 4px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
text-decoration: none;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.entry-header.header-big .header-menu {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.header-menu .research-use {
|
||||
float: left;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #91b9cd;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { Dropdown } from 'react-viewerbase';
|
||||
import './Header.css';
|
||||
import OHIFLogo from '../OHIFLogo/OHIFLogo.js';
|
||||
import ConnectedUserPreferencesModal from '../../connectedComponents/ConnectedUserPreferencesModal.js';
|
||||
|
||||
class Header extends Component {
|
||||
static propTypes = {
|
||||
home: PropTypes.bool.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
openUserPreferencesModal: PropTypes.func,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
home: true,
|
||||
children: OHIFLogo()
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
userPreferencesOpen: false
|
||||
};
|
||||
|
||||
this.options = [
|
||||
{
|
||||
title: 'Preferences ',
|
||||
icon: 'fa fa-user',
|
||||
onClick: this.props.openUserPreferencesModal
|
||||
},
|
||||
{
|
||||
title: 'About',
|
||||
icon: 'fa fa-info',
|
||||
link: 'http://ohif.org'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={`entry-header ${this.props.home ? 'header-big' : ''}`}>
|
||||
<div className="header-left-box">
|
||||
{this.props.location && this.props.location.studyLink && (
|
||||
<Link
|
||||
to={this.props.location.studyLink}
|
||||
className="header-btn header-viewerLink"
|
||||
>
|
||||
Back to Viewer
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{this.props.children}
|
||||
|
||||
{!this.props.home && (
|
||||
<Link
|
||||
className="header-btn header-studyListLinkSection"
|
||||
to={{
|
||||
pathname: '/',
|
||||
state: { studyLink: this.props.location.pathname }
|
||||
}}
|
||||
>
|
||||
Study list
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="header-menu">
|
||||
<span className="research-use">INVESTIGATIONAL USE ONLY</span>
|
||||
<Dropdown title="Options" list={this.options} align="right" />
|
||||
<ConnectedUserPreferencesModal />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Header);
|
||||
@@ -0,0 +1,3 @@
|
||||
import Header from './Header'
|
||||
|
||||
export default Header
|
||||
@@ -0,0 +1,23 @@
|
||||
.header-brand {
|
||||
height: 30px;
|
||||
text-decoration: none;
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.header-logo-text {
|
||||
display: inline-block;
|
||||
font-family: var(--logo-font-family);
|
||||
font-size: 14px;
|
||||
font-weight: var(--logo-font-weight);
|
||||
color: var(--text-primary-color);
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.header-logo-image {
|
||||
fill: transparent;
|
||||
height: 100%;
|
||||
margin: 0 8px 0 0;
|
||||
width: 30px;
|
||||
float: left;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import './OHIFLogo.css';
|
||||
|
||||
const Icons = 'icons.svg';
|
||||
|
||||
function OHIFLogo() {
|
||||
return (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="header-brand"
|
||||
href="http://ohif.org"
|
||||
>
|
||||
<svg className="header-logo-image">
|
||||
<use xlinkHref={`${Icons}#icon-ohif-logo`} />
|
||||
</svg>
|
||||
<div className="header-logo-text">Open Health Imaging Foundation</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default OHIFLogo;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
class StudyLoadingMonitor extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array.isRequired,
|
||||
setStudyLoadingProgress: PropTypes.func.isRequired,
|
||||
clearStudyLoadingProgress: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// TODO: This is pretty ugly. The thing is that the StudyLoadingListener
|
||||
// needs to update the Redux store, but shouldn't know that it exists.
|
||||
// I am therefore passing in some functions to update the store instead,
|
||||
// but this should definitely be cleaned up somehow.
|
||||
const options = {
|
||||
_setProgressData: (progressId, progressData) => {
|
||||
this.props.setStudyLoadingProgress(progressId, progressData)
|
||||
},
|
||||
_clearProgressById: (progressId) => {
|
||||
this.props.clearStudyLoadingProgress(progressId)
|
||||
}
|
||||
}
|
||||
|
||||
const { StudyLoadingListener } = OHIF.classes;
|
||||
this.studyLoadingListener = StudyLoadingListener.getInstance(options);
|
||||
this.studyLoadingListener.clear();
|
||||
this.studyLoadingListener.addStudies(this.props.studies);
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Destroy stack loading listeners when we close the viewer
|
||||
this.studyLoadingListener.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default StudyLoadingMonitor;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
const { StudyPrefetcher } = OHIF.classes;
|
||||
|
||||
class StudyPrefetcherComponent extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { studies } = this.props;
|
||||
|
||||
this.studyPrefetcher = StudyPrefetcher.getInstance();
|
||||
this.studyPrefetcher.setStudies(studies);
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Stop prefetching when we close the viewer
|
||||
this.studyPrefetcher.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default StudyPrefetcherComponent;
|
||||
@@ -0,0 +1,40 @@
|
||||
import dicomParser from 'dicom-parser';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import OHIF from 'ohif-core';
|
||||
import sha from './sha.js';
|
||||
import version from './version.js';
|
||||
import { homepage } from '../package.json';
|
||||
|
||||
window.info = {
|
||||
sha,
|
||||
version,
|
||||
homepage
|
||||
};
|
||||
|
||||
// For debugging
|
||||
window.cornerstone = cornerstone;
|
||||
window.cornerstoneWADOImageLoader = cornerstoneWADOImageLoader;
|
||||
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||||
|
||||
OHIF.user.getAccessToken = () => {
|
||||
// TODO: Get the Redux store from somewhere else
|
||||
const state = window.store.getState();
|
||||
if (!state.oidc || !state.oidc.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
return state.oidc.user.access_token;
|
||||
};
|
||||
|
||||
cornerstoneWADOImageLoader.configure({
|
||||
beforeSend: function(xhr) {
|
||||
const headers = OHIF.DICOMWeb.getAuthorizationHeader();
|
||||
|
||||
if (headers.Authorization) {
|
||||
xhr.setRequestHeader('Authorization', headers.Authorization);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { connect } from 'react-redux';
|
||||
import FlexboxLayout from './FlexboxLayout';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
leftSidebarOpen: state.ui.leftSidebarOpen,
|
||||
rightSidebarOpen: state.ui.rightSidebarOpen,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedFlexboxLayout = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(FlexboxLayout);
|
||||
|
||||
export default ConnectedFlexboxLayout;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Header from '../components/Header/Header.js';
|
||||
import { setUserPreferencesModalOpen } from '../redux/actions.js';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
isOpen: state.ui.userPreferencesModalOpen
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
openUserPreferencesModal: () => {
|
||||
dispatch(setUserPreferencesModalOpen(true));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedHeader = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Header);
|
||||
|
||||
export default ConnectedHeader;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { LayoutButton } from 'react-viewerbase';
|
||||
import OHIF from 'ohif-core'
|
||||
|
||||
const { setLayout } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
currentLayout: state.viewports.layout,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
// TODO: Change if layout switched becomes more complex
|
||||
onChange: selectedCell => {
|
||||
let viewports = [];
|
||||
const rows = selectedCell.row + 1;
|
||||
const columns = selectedCell.col + 1;
|
||||
const numViewports = rows * columns;
|
||||
for (let i = 0; i < numViewports; i++) {
|
||||
viewports.push({
|
||||
height: `${100 / rows}%`,
|
||||
width: `${100 / columns}%`
|
||||
});
|
||||
}
|
||||
const layout = {
|
||||
viewports
|
||||
};
|
||||
|
||||
dispatch(setLayout(layout))
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedLayoutButton = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LayoutButton);
|
||||
|
||||
export default ConnectedLayoutButton;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { LayoutManager } from 'react-viewerbase';
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const viewportPluginIds = state.plugins.availablePlugins.filter(plugin => plugin.type === OHIF.plugins.PLUGIN_TYPES.VIEWPORT).map(plugin => plugin.id);
|
||||
const availablePlugins = {}
|
||||
viewportPluginIds.forEach(id => {
|
||||
const plugin = OHIF.plugins.availablePlugins.find(plugin => plugin.id === id);
|
||||
if (plugin) {
|
||||
availablePlugins[id] = plugin.component;
|
||||
}
|
||||
});
|
||||
|
||||
// TODO Use something like state.plugins.defaultPlugin[OHIF.plugins.PLUGIN_TYPES.VIEWPORT]
|
||||
let defaultPlugin;
|
||||
if (viewportPluginIds && viewportPluginIds.length) {
|
||||
defaultPlugin = viewportPluginIds[0];
|
||||
}
|
||||
|
||||
return {
|
||||
layout: state.viewports.layout,
|
||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
||||
availablePlugins,
|
||||
defaultPlugin
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedLayoutManager = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(LayoutManager);
|
||||
|
||||
export default ConnectedLayoutManager;
|
||||
@@ -0,0 +1,137 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { MeasurementTable } from 'react-viewerbase';
|
||||
import OHIF from 'ohif-core';
|
||||
import moment from 'moment';
|
||||
|
||||
function groupBy(list, props) {
|
||||
return list.reduce((a, b) => {
|
||||
(a[b[props]] = a[b[props]] || []).push(b);
|
||||
return a;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getAllTools() {
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
let tools = [];
|
||||
config.measurementTools.forEach(
|
||||
toolGroup => (tools = tools.concat(toolGroup.childTools))
|
||||
);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
function getMeasurementText(measurementData) {
|
||||
const { location, description } = measurementData;
|
||||
let text = '...';
|
||||
if (location) {
|
||||
text = location;
|
||||
if (description) {
|
||||
text += `(${description})`;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function getDataForEachMeasurementNumber(
|
||||
measurementNumberList,
|
||||
timepoints,
|
||||
displayFunction
|
||||
) {
|
||||
const data = [];
|
||||
// on each measurement number we should get each measurement data by available timepoint
|
||||
measurementNumberList.forEach(measurement => {
|
||||
timepoints.forEach(timepoint => {
|
||||
const eachData = {
|
||||
displayText: '...'
|
||||
};
|
||||
if (measurement.timepointId === timepoint.timepointId) {
|
||||
eachData.displayText = displayFunction(measurement);
|
||||
}
|
||||
data.push(eachData);
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function convertMeasurementsToTableData(toolCollections, timepoints) {
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const toolGroups = config.measurementTools;
|
||||
const tools = getAllTools();
|
||||
|
||||
const tableMeasurements = toolGroups.map(toolGroup => {
|
||||
return {
|
||||
groupName: toolGroup.name,
|
||||
groupId: toolGroup.id,
|
||||
measurements: []
|
||||
};
|
||||
});
|
||||
|
||||
Object.keys(toolCollections).forEach(toolId => {
|
||||
const toolMeasurements = toolCollections[toolId];
|
||||
const tool = tools.find(tool => tool.id === toolId);
|
||||
const { displayFunction } = tool.options.measurementTable;
|
||||
|
||||
// Group by measurementNumber so we can display then all in the same line
|
||||
const groupedMeasurements = groupBy(toolMeasurements, 'measurementNumber');
|
||||
|
||||
Object.keys(groupedMeasurements).forEach(groupedMeasurementsIndex => {
|
||||
const measurementNumberList =
|
||||
groupedMeasurements[groupedMeasurementsIndex];
|
||||
//check if all measurements with same measurementNumber will have same LABEL
|
||||
const tableMeasurement = {
|
||||
measurementId: measurementNumberList[0]._id,
|
||||
label: getMeasurementText(measurementNumberList[0]),
|
||||
hasWarnings: false, //TODO
|
||||
warningTitle: '', //TODO
|
||||
isSplitLesion: false, //TODO
|
||||
warningList: [], //TODO
|
||||
data: getDataForEachMeasurementNumber(
|
||||
measurementNumberList,
|
||||
timepoints,
|
||||
displayFunction
|
||||
)
|
||||
};
|
||||
|
||||
// find the group object for the tool
|
||||
const toolGroupMeasurements = tableMeasurements.find(group => {
|
||||
return group.groupId === tool.toolGroup;
|
||||
});
|
||||
// inject the new measurement for this measurementNumer
|
||||
toolGroupMeasurements.measurements.push(tableMeasurement);
|
||||
});
|
||||
});
|
||||
|
||||
return tableMeasurements;
|
||||
}
|
||||
|
||||
function convertTimepointsToTableData(timepoints) {
|
||||
if (!timepoints || !timepoints.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Study date:',
|
||||
date: moment(timepoints[0].latestDate).format('DD-MMM-YY')
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { timepoints, measurements } = state.timepointManager;
|
||||
return {
|
||||
timepoints: convertTimepointsToTableData(timepoints),
|
||||
measurementCollection: convertMeasurementsToTableData(
|
||||
measurements,
|
||||
timepoints
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedMeasurementTable = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(MeasurementTable);
|
||||
|
||||
export default ConnectedMeasurementTable;
|
||||
@@ -0,0 +1,159 @@
|
||||
import { connect } from 'react-redux';
|
||||
import PluginSwitch from './PluginSwitch.js';
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
const { setLayout } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { activeViewportIndex, layout } = state.viewports;
|
||||
|
||||
return {
|
||||
activeViewportIndex,
|
||||
layout
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setLayout: data => {
|
||||
dispatch(setLayout(data));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function setSingleLayoutData(originalArray, viewportIndex, data) {
|
||||
const viewports = originalArray.slice();
|
||||
const layoutData = Object.assign({}, viewports[viewportIndex], data);
|
||||
|
||||
viewports[viewportIndex] = layoutData;
|
||||
|
||||
return viewports;
|
||||
}
|
||||
|
||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||
const { activeViewportIndex, layout } = propsFromState;
|
||||
const { setLayout } = propsFromDispatch;
|
||||
|
||||
// TODO: Do not display certain options if the current display set
|
||||
// cannot be displayed using these view types
|
||||
const Icons = 'icons.svg';
|
||||
const buttons = [
|
||||
{
|
||||
text: 'Acquired',
|
||||
type: 'command',
|
||||
iconClasses: 'fa fa-bars',
|
||||
active: false,
|
||||
onClick: click => {
|
||||
console.warn('Original Acquisition');
|
||||
|
||||
const layoutData = setSingleLayoutData(
|
||||
layout.viewports,
|
||||
activeViewportIndex,
|
||||
{ plugin: 'cornerstone' }
|
||||
);
|
||||
|
||||
setLayout({ viewports: layoutData });
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Axial',
|
||||
svgUrl: `${Icons}#cube`,
|
||||
svgClasses: 'icon-rotate-120',
|
||||
active: false,
|
||||
onClick: click => {
|
||||
console.warn('Axial');
|
||||
const data = {
|
||||
plugin: 'vtk',
|
||||
vtk: {
|
||||
mode: 'mpr',
|
||||
sliceNormal: [0, 0, 1]
|
||||
}
|
||||
};
|
||||
|
||||
const layoutData = setSingleLayoutData(
|
||||
layout.viewports,
|
||||
activeViewportIndex,
|
||||
data
|
||||
);
|
||||
|
||||
setLayout({ viewports: layoutData });
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Sagittal',
|
||||
svgUrl: `${Icons}#cube`,
|
||||
active: false,
|
||||
onClick: click => {
|
||||
console.warn('Sagittal');
|
||||
const data = {
|
||||
plugin: 'vtk',
|
||||
vtk: {
|
||||
mode: 'mpr',
|
||||
sliceNormal: [1, 0, 0]
|
||||
}
|
||||
};
|
||||
|
||||
const layoutData = setSingleLayoutData(
|
||||
layout.viewports,
|
||||
activeViewportIndex,
|
||||
data
|
||||
);
|
||||
|
||||
setLayout({ viewports: layoutData });
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Coronal',
|
||||
svgUrl: `${Icons}#cube`,
|
||||
svgClasses: 'fa-rotate-90 fa-flip-horizontal',
|
||||
active: false,
|
||||
onClick: click => {
|
||||
console.warn('Coronal');
|
||||
const data = {
|
||||
plugin: 'vtk',
|
||||
vtk: {
|
||||
mode: 'mpr',
|
||||
sliceNormal: [0, 1, 0]
|
||||
}
|
||||
};
|
||||
|
||||
const layoutData = setSingleLayoutData(
|
||||
layout.viewports,
|
||||
activeViewportIndex,
|
||||
data
|
||||
);
|
||||
|
||||
setLayout({ viewports: layoutData });
|
||||
}
|
||||
}
|
||||
/*{
|
||||
text: '3D',
|
||||
svgUrl: `${Icons}#cube`,
|
||||
onClick: (click) => {
|
||||
console.warn('3D Perspective');
|
||||
const data = {
|
||||
plugin: 'vtk',
|
||||
vtk: {
|
||||
mode: '3d',
|
||||
}
|
||||
};
|
||||
|
||||
const layoutData = setSingleLayoutData(layout.viewports, activeViewportIndex, data);
|
||||
|
||||
setLayout({ viewports: layoutData });
|
||||
}
|
||||
}*/
|
||||
];
|
||||
|
||||
return {
|
||||
buttons
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedPluginSwitch = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(PluginSwitch);
|
||||
|
||||
export default ConnectedPluginSwitch;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { StudyBrowser } from 'react-viewerbase';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
// 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 ConnectedStudyBrowser = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(StudyBrowser);
|
||||
|
||||
export default ConnectedStudyBrowser;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { connect } from 'react-redux';
|
||||
import StudyLoadingMonitor from '../components/StudyLoadingMonitor.js';
|
||||
import OHIF from 'ohif-core'
|
||||
|
||||
const { setStudyLoadingProgress, clearStudyLoadingProgress } = OHIF.redux.actions;
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setStudyLoadingProgress: (progressId, progressData) => {
|
||||
dispatch(setStudyLoadingProgress(progressId, progressData))
|
||||
},
|
||||
clearStudyLoadingProgress: progressId => {
|
||||
dispatch(clearStudyLoadingProgress(progressId))
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedStudyLoadingMonitor = connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(StudyLoadingMonitor);
|
||||
|
||||
export default ConnectedStudyLoadingMonitor;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ToolbarRow from './ToolbarRow';
|
||||
import { setLeftSidebarOpen, setRightSidebarOpen } from '../redux/actions.js';
|
||||
|
||||
const defaultPlugin = 'cornerstone';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { layout, viewportSpecificData, activeViewportIndex } = state.viewports;
|
||||
const pluginInLayout = layout.viewports[activeViewportIndex] && layout.viewports[activeViewportIndex].plugin;
|
||||
const pluginInViewportData = viewportSpecificData[activeViewportIndex] && viewportSpecificData[activeViewportIndex].plugin;
|
||||
const pluginInActiveViewport = pluginInLayout || pluginInViewportData || defaultPlugin;
|
||||
// const extensionData = state.extensions[pluginInActiveViewport];
|
||||
|
||||
return {
|
||||
pluginId: pluginInActiveViewport,
|
||||
leftSidebarOpen: state.ui.leftSidebarOpen,
|
||||
rightSidebarOpen: state.ui.rightSidebarOpen
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setLeftSidebarOpen: state => {
|
||||
dispatch(setLeftSidebarOpen(state))
|
||||
},
|
||||
setRightSidebarOpen: state => {
|
||||
dispatch(setRightSidebarOpen(state))
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedToolbarRow = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ToolbarRow);
|
||||
|
||||
export default ConnectedToolbarRow;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { ToolbarSection } from 'react-viewerbase';
|
||||
import OHIF from 'ohif-core'
|
||||
|
||||
const { setToolActive } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const activeButton = state.tools.buttons.find(tool => tool.active === true);
|
||||
|
||||
return {
|
||||
buttons: state.tools.buttons,
|
||||
activeCommand: activeButton && activeButton.command
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setToolActive: tool => {
|
||||
dispatch(setToolActive(tool.command))
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedToolbarSection = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ToolbarSection);
|
||||
|
||||
export default ConnectedToolbarSection;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { UserPreferencesModal } from 'react-viewerbase';
|
||||
import OHIF from 'ohif-core';
|
||||
import { setUserPreferencesModalOpen } from '../redux/actions.js';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
const { setUserPreferences } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const contextName = window.store.getState().commandContext.context;
|
||||
return {
|
||||
isOpen: state.ui.userPreferencesModalOpen,
|
||||
windowLevelData: state.preferences[contextName]
|
||||
? state.preferences[contextName].windowLevelData
|
||||
: {},
|
||||
hotKeysData: state.preferences[contextName]
|
||||
? state.preferences[contextName].hotKeysData
|
||||
: {}
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onCancel: () => {
|
||||
dispatch(setUserPreferencesModalOpen(false));
|
||||
},
|
||||
onSave: data => {
|
||||
const contextName = window.store.getState().commandContext.context;
|
||||
const preferences = cloneDeep(window.store.getState().preferences);
|
||||
preferences[contextName] = data;
|
||||
dispatch(setUserPreferences(preferences));
|
||||
dispatch(setUserPreferencesModalOpen(false));
|
||||
OHIF.hotkeysUtil.setHotkeys(data.hotKeysData);
|
||||
},
|
||||
onResetToDefaults: () => {
|
||||
dispatch(setUserPreferences());
|
||||
dispatch(setUserPreferencesModalOpen(false));
|
||||
OHIF.hotkeysUtil.setHotkeys();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedUserPreferencesModal = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(UserPreferencesModal);
|
||||
|
||||
export default ConnectedUserPreferencesModal;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Viewer from './Viewer.js';
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onTimepointsUpdated: timepoints => {
|
||||
dispatch(setTimepoints(timepoints));
|
||||
},
|
||||
onMeasurementsUpdated: measurements => {
|
||||
dispatch(setMeasurements(measurements));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedViewer = connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(Viewer);
|
||||
|
||||
export default ConnectedViewer;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js';
|
||||
|
||||
const isActive = (a) => a.active === true;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const activeServer = state.servers.servers.find(isActive);
|
||||
|
||||
return {
|
||||
server: activeServer,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedViewerRetrieveStudyData = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(ViewerRetrieveStudyData);
|
||||
|
||||
export default ConnectedViewerRetrieveStudyData;
|
||||
@@ -0,0 +1,48 @@
|
||||
.FlexboxLayout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row nowrap;
|
||||
align-items: stretch;
|
||||
height: calc(100% - var(--toolbar-height));
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
height: 100%;
|
||||
/* required transformation to make inner fixed elements relative to this one*/
|
||||
transform: scale(1);
|
||||
transition: var(--sidebar-transition);
|
||||
}
|
||||
|
||||
.sidebar-left {
|
||||
border-right: var(--ui-border-thickness) solid var(--ui-border-color);
|
||||
flex: 1;
|
||||
margin-left: calc(var(--left-sidebar-menu-width) * -1);
|
||||
max-width: var(--left-sidebar-menu-width);
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.sidebar-left.sidebar-open {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
order: 2;
|
||||
overflow: hidden;
|
||||
transition: var(--sidebar-transition);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-right {
|
||||
flex: 1;
|
||||
margin-right: calc(var(--right-sidebar-menu-width) * -1);
|
||||
max-width: var(--right-sidebar-menu-width);
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.sidebar-right.sidebar-open {
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||
import ViewerMain from './ViewerMain.js';
|
||||
import ConnectedMeasurementTable from './ConnectedMeasurementTable';
|
||||
import './FlexboxLayout.css';
|
||||
|
||||
class FlexboxLayout extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array.isRequired,
|
||||
leftSidebarOpen: PropTypes.bool.isRequired,
|
||||
rightSidebarOpen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
studiesForBrowser: []
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const studiesForBrowser = this.getStudiesForBrowser();
|
||||
|
||||
this.setState({
|
||||
studiesForBrowser
|
||||
});
|
||||
}
|
||||
|
||||
getStudiesForBrowser = () => {
|
||||
const { studies } = this.props;
|
||||
|
||||
// TODO[react]:
|
||||
// - Add sorting of display sets
|
||||
// - Add useMiddleSeriesInstanceAsThumbnail
|
||||
// - Add showStackLoadingProgressBar option
|
||||
return studies.map(study => {
|
||||
const { studyInstanceUid } = study;
|
||||
|
||||
const thumbnails = study.displaySets.map(displaySet => {
|
||||
const {
|
||||
displaySetInstanceUid,
|
||||
seriesDescription,
|
||||
seriesNumber,
|
||||
instanceNumber,
|
||||
numImageFrames
|
||||
} = displaySet;
|
||||
|
||||
let imageId;
|
||||
|
||||
if (displaySet.images && displaySet.images.length) {
|
||||
imageId = displaySet.images[0].getImageId();
|
||||
}
|
||||
|
||||
return {
|
||||
imageId,
|
||||
displaySetInstanceUid,
|
||||
seriesDescription,
|
||||
seriesNumber,
|
||||
instanceNumber,
|
||||
numImageFrames
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
thumbnails
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
let mainContentClassName = 'main-content';
|
||||
if (this.props.leftSidebarOpen) {
|
||||
mainContentClassName += ' sidebar-left-open';
|
||||
}
|
||||
|
||||
if (this.props.rightSidebarOpen) {
|
||||
mainContentClassName += ' sidebar-right-open';
|
||||
}
|
||||
|
||||
// TODO[react]: Make ConnectedMeasurementTable extension with state.timepointManager
|
||||
return (
|
||||
<div className="FlexboxLayout">
|
||||
<div
|
||||
className={
|
||||
this.props.leftSidebarOpen
|
||||
? 'sidebar-menu sidebar-left sidebar-open'
|
||||
: 'sidebar-menu sidebar-left'
|
||||
}
|
||||
>
|
||||
<ConnectedStudyBrowser studies={this.state.studiesForBrowser} />
|
||||
</div>
|
||||
<div className={mainContentClassName}>
|
||||
<ViewerMain studies={this.props.studies} />
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
this.props.rightSidebarOpen
|
||||
? 'sidebar-menu sidebar-right sidebar-open'
|
||||
: 'sidebar-menu sidebar-right'
|
||||
}
|
||||
>
|
||||
<ConnectedMeasurementTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FlexboxLayout;
|
||||
@@ -0,0 +1,3 @@
|
||||
.icon-rotate-120 {
|
||||
transform: rotate(120deg);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ExpandableToolMenu } from 'react-viewerbase';
|
||||
import './PluginSwitch.css';
|
||||
|
||||
class PluginSwitch extends Component {
|
||||
static propTypes = {
|
||||
buttons: PropTypes.array
|
||||
};
|
||||
|
||||
static defaultProps = {};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="PluginSwitch">
|
||||
<ExpandableToolMenu buttons={this.props.buttons} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginSwitch;
|
||||
@@ -0,0 +1,17 @@
|
||||
.ToolbarRow {
|
||||
border-bottom: var(--ui-border-thickness) solid var(--ui-border-color);
|
||||
flex: 0 0 auto;
|
||||
height: var(--toolbar-height);
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
position: relative;
|
||||
transition: height 300ms ease;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
ToolbarRow.expanded {
|
||||
height: calc(var(--toolbar-height) + var(--toolbar-drawer-height));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF from 'ohif-core';
|
||||
import { RoundedButtonGroup } from 'react-viewerbase';
|
||||
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
||||
import ConnectedPluginSwitch from './ConnectedPluginSwitch.js';
|
||||
import './ToolbarRow.css';
|
||||
|
||||
const Icons = 'icons.svg';
|
||||
|
||||
class ToolbarRow extends Component {
|
||||
static propTypes = {
|
||||
leftSidebarOpen: PropTypes.bool.isRequired,
|
||||
rightSidebarOpen: PropTypes.bool.isRequired,
|
||||
setLeftSidebarOpen: PropTypes.func,
|
||||
setRightSidebarOpen: PropTypes.func,
|
||||
pluginId: PropTypes.string
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
leftSidebarOpen: false,
|
||||
rightSidebarOpen: false
|
||||
};
|
||||
|
||||
onLeftSidebarValueChanged = value => {
|
||||
this.props.setLeftSidebarOpen(!!value);
|
||||
};
|
||||
|
||||
onRightSidebarValueChanged = value => {
|
||||
this.props.setRightSidebarOpen(!!value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const leftSidebarToggle = [
|
||||
{
|
||||
value: 'studies',
|
||||
svgLink: `${Icons}#icon-studies`,
|
||||
svgWidth: 15,
|
||||
svgHeight: 13,
|
||||
bottomLabel: 'Series'
|
||||
}
|
||||
];
|
||||
|
||||
const rightSidebarToggle = [
|
||||
{
|
||||
value: 'measurements',
|
||||
svgLink: `${Icons}#icon-measurements-lesions`,
|
||||
svgWidth: 15,
|
||||
svgHeight: 13,
|
||||
bottomLabel: 'Measurements'
|
||||
}
|
||||
];
|
||||
|
||||
const leftSidebarValue = this.props.leftSidebarOpen
|
||||
? leftSidebarToggle[0].value
|
||||
: null;
|
||||
|
||||
const rightSidebarValue = this.props.rightSidebarOpen
|
||||
? rightSidebarToggle[0].value
|
||||
: null;
|
||||
|
||||
const currentPluginId = this.props.pluginId;
|
||||
|
||||
const { PLUGIN_TYPES, availablePlugins } = OHIF.plugins;
|
||||
const plugin = availablePlugins.find(entry => {
|
||||
return (
|
||||
entry.type === PLUGIN_TYPES.TOOLBAR && entry.id === currentPluginId
|
||||
);
|
||||
});
|
||||
|
||||
let pluginComp;
|
||||
if (plugin) {
|
||||
const PluginComponent = plugin.component;
|
||||
|
||||
pluginComp = <PluginComponent />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ToolbarRow">
|
||||
<div className="pull-left m-t-1 p-y-1" style={{ padding: '10px' }}>
|
||||
<RoundedButtonGroup
|
||||
options={leftSidebarToggle}
|
||||
value={leftSidebarValue}
|
||||
onValueChanged={this.onLeftSidebarValueChanged}
|
||||
/>
|
||||
</div>
|
||||
{pluginComp}
|
||||
<ConnectedLayoutButton />
|
||||
<ConnectedPluginSwitch />
|
||||
<div className="pull-right m-t-1 rm-x-1" style={{ marginLeft: 'auto' }}>
|
||||
<RoundedButtonGroup
|
||||
options={rightSidebarToggle}
|
||||
value={rightSidebarValue}
|
||||
onValueChanged={this.onRightSidebarValueChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToolbarRow;
|
||||
@@ -0,0 +1,10 @@
|
||||
#viewer {
|
||||
background-color: black;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loadingTextDiv {
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
//import OHIF from 'ohif-core';
|
||||
//import { CineDialog } from 'react-viewerbase';
|
||||
|
||||
import OHIF from 'ohif-core';
|
||||
import moment from 'moment';
|
||||
import WhiteLabellingContext from '../WhiteLabellingContext.js';
|
||||
import ConnectedHeader from './ConnectedHeader.js';
|
||||
import ConnectedFlexboxLayout from './ConnectedFlexboxLayout.js';
|
||||
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
||||
import './Viewer.css';
|
||||
/**
|
||||
* Inits OHIF Hanging Protocol's onReady.
|
||||
* It waits for OHIF Hanging Protocol to be ready to instantiate the ProtocolEngine
|
||||
* Hanging Protocol will use OHIF LayoutManager to render viewports properly
|
||||
*/
|
||||
/*const initHangingProtocol = () => {
|
||||
// When Hanging Protocol is ready
|
||||
HP.ProtocolStore.onReady(() => {
|
||||
|
||||
// Gets all StudyMetadata objects: necessary for Hanging Protocol to access study metadata
|
||||
const studyMetadataList = OHIF.viewer.StudyMetadataList.all();
|
||||
|
||||
// Instantiate StudyMetadataSource: necessary for Hanging Protocol to get study metadata
|
||||
const studyMetadataSource = new OHIF.studies.classes.OHIFStudyMetadataSource();
|
||||
|
||||
// Get prior studies map
|
||||
const studyPriorsMap = OHIF.studylist.functions.getStudyPriorsMap(studyMetadataList);
|
||||
|
||||
// Creates Protocol Engine object with required arguments
|
||||
const ProtocolEngine = new HP.ProtocolEngine(layoutManager, studyMetadataList, studyPriorsMap, studyMetadataSource);
|
||||
|
||||
// Sets up Hanging Protocol engine
|
||||
HP.setEngine(ProtocolEngine);
|
||||
});
|
||||
};*/
|
||||
|
||||
/*const viewportUtils = OHIF.viewerbase.viewportUtils;
|
||||
|
||||
OHIF.viewer.functionList = {
|
||||
toggleCineDialog: viewportUtils.toggleCineDialog,
|
||||
toggleCinePlay: viewportUtils.toggleCinePlay,
|
||||
clearTools: viewportUtils.clearTools,
|
||||
resetViewport: viewportUtils.resetViewport,
|
||||
invert: viewportUtils.invert
|
||||
};*/
|
||||
|
||||
class Viewer extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array,
|
||||
studyInstanceUids: PropTypes.array,
|
||||
onTimepointsUpdated: PropTypes.func,
|
||||
onMeasurementsUpdated: PropTypes.func
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
OHIF.measurements.MeasurementApi.setConfiguration({
|
||||
dataExchange: {
|
||||
retrieve: this.retrieveMeasurements,
|
||||
store: this.storeMeasurements
|
||||
}
|
||||
});
|
||||
|
||||
OHIF.measurements.TimepointApi.setConfiguration({
|
||||
dataExchange: {
|
||||
retrieve: this.retrieveTimepoints,
|
||||
store: this.storeTimepoints,
|
||||
remove: this.removeTimepoint,
|
||||
update: this.updateTimepoint,
|
||||
disassociate: this.disassociateStudy
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
retrieveMeasurements = (patientId, timepointIds) => {
|
||||
OHIF.log.info('retrieveMeasurements');
|
||||
// TODO: Retrieve the measurements from the latest available SR
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
storeMeasurements = (measurementData, timepointIds) => {
|
||||
OHIF.log.info('storeMeasurements');
|
||||
// TODO: Store the measurements into a new SR sent to the active server
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
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 } = 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
|
||||
});
|
||||
|
||||
const patientId = studies[0] && studies[0].patientId;
|
||||
timepointApi.retrieveTimepoints({ patientId });
|
||||
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<WhiteLabellingContext.Consumer>
|
||||
{whiteLabelling => (
|
||||
<ConnectedHeader home={false}>
|
||||
{whiteLabelling.logoComponent}
|
||||
</ConnectedHeader>
|
||||
)}
|
||||
</WhiteLabellingContext.Consumer>
|
||||
<div id="viewer" className="Viewer">
|
||||
<ConnectedToolbarRow />
|
||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
||||
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
||||
<ConnectedFlexboxLayout studies={this.props.studies} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Viewer;
|
||||
@@ -0,0 +1,41 @@
|
||||
#viewer {
|
||||
height: calc(100% - var(--top-bar-height));
|
||||
}
|
||||
|
||||
#viewer > .loadingTextDiv {
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 30px;
|
||||
height: 100%;
|
||||
line-height: calc(100% - var(--top-bar-height));
|
||||
}
|
||||
|
||||
.ViewerMain {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.ViewerMain>div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ViewerMain #imageViewerViewports .viewportContainer {
|
||||
/*theme('border', '%s solid $uiBorderColorDark' % $viewportBorderThickness)*/
|
||||
/*float: left;
|
||||
position: relative;*/
|
||||
outline: 0; /* Prevent blue outline in Chrome */
|
||||
}
|
||||
|
||||
.ViewerMain #imageViewerViewports .viewportContainer:hover,
|
||||
.ViewerMain #imageViewerViewports .viewportContainer:active,
|
||||
.ViewerMain #imageViewerViewports .viewportContainer:hover.active {
|
||||
outline: 0; /* Prevent blue outline in Chrome */
|
||||
}
|
||||
|
||||
/* &:hover
|
||||
//theme('border', '%s solid $uiBorderColor' % $viewportBorderThickness)
|
||||
|
||||
&.active, &:hover.active
|
||||
//theme('border', '%s solid $uiBorderColorActive' % $viewportBorderThickness)
|
||||
*/
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { OHIF } from 'ohif-core';
|
||||
import ConnectedLayoutManager from './ConnectedLayoutManager.js';
|
||||
import './ViewerMain.css';
|
||||
|
||||
class ViewerMain extends Component {
|
||||
state = {
|
||||
viewportData: []
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
studies: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
OHIF.hotkeysUtil.setup('viewer');
|
||||
}
|
||||
|
||||
getDisplaySets(studies) {
|
||||
const displaySets = [];
|
||||
studies.forEach(study => {
|
||||
study.displaySets.forEach(dSet => {
|
||||
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
|
||||
const displaySets = this.getDisplaySets(this.props.studies);
|
||||
|
||||
this.setState({
|
||||
viewportData: displaySets
|
||||
});
|
||||
}
|
||||
|
||||
setViewportData = ({ viewportIndex, item }) => {
|
||||
// TODO: Replace this with mapDispatchToProps call
|
||||
// if we decide to put viewport info into redux
|
||||
|
||||
// Note: Use Slice because React does a shallow equality check. Mutating the array
|
||||
// would not trigger a re-render. We have to create a copy.
|
||||
const updatedViewportData = this.state.viewportData.slice(0);
|
||||
|
||||
const displaySet = this.findDisplaySet(
|
||||
this.props.studies,
|
||||
item.studyInstanceUid,
|
||||
item.displaySetInstanceUid
|
||||
);
|
||||
|
||||
updatedViewportData[viewportIndex] = Object.assign({}, displaySet);
|
||||
|
||||
this.setState({
|
||||
viewportData: updatedViewportData
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ViewerMain">
|
||||
<ConnectedLayoutManager
|
||||
studies={this.props.studies}
|
||||
viewportData={this.state.viewportData}
|
||||
setViewportData={this.setViewportData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// 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;
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF from 'ohif-core';
|
||||
import ConnectedViewer from './ConnectedViewer.js';
|
||||
|
||||
const { createDisplaySets } = OHIF.utils;
|
||||
|
||||
class ViewerRetrieveStudyData extends Component {
|
||||
static propTypes = {
|
||||
studyInstanceUids: PropTypes.array.isRequired,
|
||||
seriesInstanceUids: PropTypes.array,
|
||||
server: PropTypes.object
|
||||
};
|
||||
|
||||
state = {
|
||||
studies: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
// TODO: Avoid using timepoints here
|
||||
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
|
||||
const { studyInstanceUids, seriesInstanceUids, server } = this.props;
|
||||
const promise = OHIF.studies.retrieveStudiesMetadata(
|
||||
server,
|
||||
studyInstanceUids,
|
||||
seriesInstanceUids
|
||||
);
|
||||
|
||||
// Render the viewer when the data is ready
|
||||
promise
|
||||
.then(studies => {
|
||||
const updatedStudies = createDisplaySets(studies);
|
||||
|
||||
this.setState({
|
||||
studies: updatedStudies
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
error: true
|
||||
});
|
||||
|
||||
throw new Error(error);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||
} else if (!this.state.studies) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectedViewer
|
||||
studies={this.state.studies}
|
||||
studyInstanceUids={this.props.studyInstanceUids}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ViewerRetrieveStudyData;
|
||||
@@ -0,0 +1,15 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OHIF Viewer</title>
|
||||
<meta name="description" content="Open Health Imaging Foundation DICOM Viewer">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no">
|
||||
<link rel="shortcut icon" type="image/png" href="/images/logo.png" sizes="16x16 32x32 64x64">
|
||||
<!--Best display for mobile devices-->
|
||||
<meta http-equiv="cleartype" content="on">
|
||||
<meta name="MobileOptimized" content="320">
|
||||
<meta name="HandheldFriendly" content="True">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
@@ -0,0 +1,237 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<symbol id="icon-hud" viewBox="0 0 20 22">
|
||||
<title>HUD</title>
|
||||
<g id="icon-hud-group" fill="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path id="icon-hud-dashes" d="m14.5,2.75 2,0 a2.5 2.5 0 0 1 2.5 2.5 l0,13.25 a2.5 2.5 0 0 1 -2.5 2.5 l-11.5,0 a2.5 2.5 0 0 1 -2.5 -2.5 l0,-1" stroke-dasharray="1,3" />
|
||||
<path id="icon-hud-box" d="m0.5,2 0,14 a1.5 1.5 0 0 0 1.5 1.5 l12,0 a1.5 1.5 0 0 0 1.5 -1.5 l0,-14" />
|
||||
<path id="icon-hud-top" d="m2,2 12,0" stroke-width="4" />
|
||||
<path id="icon-hud-paralel" d="m4.4,7.5 7.2,0 m0,3 -7.2,0 m0,3 7.2,0" stroke-linecap="square" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-measurements-additional" viewBox="0 0 16 16">
|
||||
<title>Additional Measurements</title>
|
||||
<g id="icon-measurements-additional-group" stroke="none">
|
||||
<path id="icon-measurements-additional-boxes" d="M11,2 l1.85,-1.85 a1 1 0 0 1 1,0 l2,2 a1 1 0 0 1 0,1 l-1.85,1.85Z M10,3 l-10,10 0,3 3,0 10,-10Z M16,16 l0,-2 -9,0 -2,2Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-measurements-lesions" viewBox="0 0 18 10">
|
||||
<title>Lesions</title>
|
||||
<g id="icon-measurements-lesions-group" stroke="none">
|
||||
<path id="icon-measurements-lesions-boxes" d="M0,0 2,0 2,2 0,2Z M4,0 18,0 18,2 4,2Z M0,4 2,4 2,6 0,6Z M4,4 18,4 18,6 4,6Z M0,8 2,8 2,10 0,10Z M4,8 18,8 18,10 4,10Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-settings" viewBox="0 0 15 15">
|
||||
<title>Settings</title>
|
||||
<path d="M12.85 8.25C12.85 8.03 12.93 7.8 12.93 7.5 12.93 7.2 12.93 6.98 12.85 6.75L14.43 5.48C14.58 5.33 14.58 5.18 14.5 5.03L13 2.4C12.93 2.33 12.78 2.25 12.55 2.33L10.68 3.08C10.3 2.78 9.85 2.55 9.4 2.33L9.1 0.38C9.18 0.15 8.95 0 8.8 0L5.8 0C5.65 0 5.43 0.15 5.43 0.3L5.13 2.33C4.68 2.48 4.3 2.78 3.85 3.08L2.05 2.33C1.83 2.25 1.68 2.33 1.53 2.48L0.03 5.1C-0.05 5.18 0.03 5.4 0.18 5.55L1.75 6.75C1.75 6.98 1.68 7.2 1.68 7.5 1.68 7.8 1.68 8.03 1.75 8.25L0.18 9.53C0.03 9.68 0.03 9.82 0.1 9.98L1.6 12.6C1.68 12.68 1.83 12.75 2.05 12.68L3.93 11.93C4.3 12.23 4.75 12.45 5.2 12.68L5.5 14.63C5.5 14.78 5.65 14.93 5.88 14.93L8.88 14.93C9.03 14.93 9.25 14.78 9.25 14.63L9.55 12.68C10 12.45 10.45 12.23 10.83 11.93L12.7 12.68C12.85 12.75 13.08 12.68 13.15 12.53L14.65 9.9C14.73 9.75 14.73 9.53 14.58 9.45L12.85 8.25 12.85 8.25ZM7.3 10.13C5.88 10.13 4.68 8.93 4.68 7.5 4.68 6.08 5.88 4.88 7.3 4.88 8.73 4.88 9.93 6.08 9.93 7.5 9.93 8.93 8.73 10.13 7.3 10.13L7.3 10.13Z" id="Shape"/>
|
||||
</symbol>
|
||||
<symbol id="icon-status-complete" viewBox="0 0 27 27">
|
||||
<title>Complete</title>
|
||||
<g id="icon-status-complete-group" stroke="none">
|
||||
<path id="icon-status-complete-circle" d="M13.5,0 a13.5 13.5 0 0 1 0 27 a13.5 13.5 0 0 1 0 -27 M23,8 l-2,-2 -10,10, -5,-5 -2,2 7,7Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-status-lock" viewBox="0 0 16 21">
|
||||
<title>Locked</title>
|
||||
<path d="M14 7L13 7 13 5C13 2.2 10.8 0 8 0 5.2 0 3 2.2 3 5L3 7 2 7C0.9 7 0 7.9 0 9L0 19C0 20.1 0.9 21 2 21L14 21C15.1 21 16 20.1 16 19L16 9C16 7.9 15.1 7 14 7L14 7ZM11.1 7L4.9 7 4.9 5C4.9 3.3 6.3 1.9 8 1.9 9.7 1.9 11.1 3.3 11.1 5L11.1 7 11.1 7Z"/>
|
||||
</symbol>
|
||||
<symbol id="icon-studies" viewBox="0 0 15 13">
|
||||
<title>Studies</title>
|
||||
<g id="icon-studies-group" stroke="none">
|
||||
<path id="icon-studies-boxes" d="M0,0 7,0 7,6 0,6Z M8,0 15,0 15,6 8,6Z M0,7 7,7 7,13 0,13Z M8,7 15,7 15,13 8,13Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-levels" viewBox="0 0 18 18">
|
||||
<title>Window / Level</title>
|
||||
<g id="icon-tools-levels-group">
|
||||
<path id="icon-tools-levels-path" d="M14.5,3.5 a1 1 0 0 1 -11,11 Z" stroke="none" opacity="0.8" />
|
||||
<circle id="icon-tools-levels-circle" cx="9" cy="9" r="8" fill="none" stroke-width="2" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-link" viewBox="0 0 23 15">
|
||||
<title>Link</title>
|
||||
<g id="icon-tools-link-group" stroke="none">
|
||||
<circle id="icon-tools-link-circle-l" opacity="0.8" cx="7.5" cy="7.5" r="7.5" />
|
||||
<circle id="icon-tools-link-circle-r" opacity="0.6" cx="15.5" cy="7.5" r="7.5" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-measure-non-target" viewBox="0 0 24 24">
|
||||
<title>Non-Target Measurement</title>
|
||||
<g id="icon-tools-measure-non-target-group" fill="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle id="icon-tools-measure-non-target-circle" cx="6.5" cy="6.5" r="6" />
|
||||
<path id="icon-tools-measure-non-target-plus" d="M6.5,3 l0,7 M3,6.5 l7,0"></path>
|
||||
<path id="icon-tools-measure-non-target-arrow" d="M23,7 l-15,15 M7,17 l0,6 6,0" stroke-width="2" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-measure-target" viewBox="0 0 26 26">
|
||||
<title>Target Measurement</title>
|
||||
<g id="icon-tools-measure-target-group" stroke-width="0">
|
||||
<path id="icon-tools-measure-target-ruler" d="m9.5,26 2,-2 -1.5,-1.5 0.5,-0.5 1.5,1.5 1.5,-1.5 -2.5,-2.5 0.5,-0.5 2.5,2.5 1.5,-1.5 -1.5,-1.5 0.5,-0.5 1.5,1.5 1.5,-1.5 -2.5,-2.5 0.5,-0.5 2.5,2.5 1.5,-1.5 -1.5,-1.5 0.5,-0.5 1.5,1.5 1.5,-1.5 -2.5,-2.5 0.5,-0.5 2.5,2.5 1.5,-1.5 -1.5,-1.5 0.5,-0.5 1.5,1.5 2,-2 -4,-4 a2 2 0 0 0 -2.5 0 l-14 14 a2 2 0 0 0 0 2.5 l4,4 Z" />
|
||||
<path id="icon-tools-measure-target-plus" d="m6,0 a6 6 0 0 1 0 12 a6 6 0 0 1 0 -12 m0.5,3 a0.5 0.5 0 0 0 -1 0 l0,2.5 -2.5,0 a0.5 0.5 0 0 0 0 1 l2.5,0 0,2.5 a0.5 0.5 0 0 0 1 0 l0,-2.5 2.5,0 a0.5 0.5 0 0 0 0 -1 l-2.5,0 z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-measure-target-cr" viewBox="0 0 24 24">
|
||||
<title>Target CR Measurement</title>
|
||||
<g id="icon-tools-measure-target-cr-group" fill="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<text id="icon-tools-measure-target-cr-text" fill="#000000" x="0" y="8" style="font-size:10px; font-family:sans-serif">CR</text>
|
||||
<path id="icon-tools-measure-target-cr-arrow" d="M23,7 l-15,15 M7,17 l0,6 6,0" stroke-width="2" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-measure-target-un" viewBox="0 0 24 24">
|
||||
<title>Target UN Measurement</title>
|
||||
<g id="icon-tools-measure-target-un-group" fill="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<text id="icon-tools-measure-target-un-text" fill="#000000" x="0" y="8" style="font-size:10px; font-family:sans-serif">UN</text>
|
||||
<path id="icon-tools-measure-target-un-arrow" d="M23,7 l-15,15 M7,17 l0,6 6,0" stroke-width="2" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-measure-temp" viewBox="0 0 24 24">
|
||||
<title>Temporary Measurement</title>
|
||||
<g id="icon-tools-measure-temp-group" fill="none" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle id="icon-tools-measure-temp-circle" cx="6.5" cy="6.5" r="6" />
|
||||
<path id="icon-tools-measure-temp-plus" d="M6.5,3 l0,7 M3,6.5 l7,0"></path>
|
||||
<path id="icon-tools-measure-temp-dashes" d="m22.5,6 -16.5,16.5" stroke-width="3" stroke-dasharray="0.6666,5" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-more" viewBox="0 0 18 18">
|
||||
<title>More</title>
|
||||
<g id="icon-tools-more-group" stroke="none">
|
||||
<path id="icon-tools-more-circle" d="M9,0 a9 9 0 0 1 0 18 a9 9 0 0 1 0 -18 M4.5,8 a1.5 1.5 0 0 0 0 3 a1.5 1.5 0 0 0 0 -3 M9,8 a1.5 1.5 0 0 0 0 3 a1.5 1.5 0 0 0 0 -3 M13.5,8 a1.5 1.5 0 0 0 0 3 a1.5 1.5 0 0 0 0 -3" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-pan" viewBox="0 0 18 18">
|
||||
<title>Pan</title>
|
||||
<g id="icon-tools-pan-group" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path id="icon-tools-pan-line-v" d="M9,1 L9,17"></path>
|
||||
<path id="icon-tools-pan-line-h" d="M1,9 L17,9"></path>
|
||||
<polyline id="icon-tools-pan-caret-t" points="7 3 9 1 11 3"></polyline>
|
||||
<polyline id="icon-tools-pan-caret-r" points="15 11 17 9 15 7"></polyline>
|
||||
<polyline id="icon-tools-pan-caret-b" points="11 15 9 17 7 15"></polyline>
|
||||
<polyline id="icon-tools-pan-caret-l" points="3 7 1 9 3 11"></polyline>
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-zoom" viewBox="0 0 17 17">
|
||||
<title>Zoom</title>
|
||||
<g id="icon-tools-zoom-group" fill="none" stroke-width="2" stroke-linecap="round">
|
||||
<path id="icon-tools-zoom-path" d="m11.5,11.5 4.5,4.5" />
|
||||
<circle id="icon-tools-zoom-circle" cx="7" cy="7" r="6" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-invert" viewBox="0 0 24 28">
|
||||
<title>Invert</title>
|
||||
<path d="M12 22.5v-17c-4.688 0-8.5 3.813-8.5 8.5s3.813 8.5 8.5 8.5zM24 14c0 6.625-5.375 12-12 12s-12-5.375-12-12 5.375-12 12-12 12 5.375 12 12z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-stack-scroll" viewBox="0 0 24 28">
|
||||
<title>Stack Scroll</title>
|
||||
<path d="M24 21v2c0 0.547-0.453 1-1 1h-22c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h22c0.547 0 1 0.453 1 1zM24 13v2c0 0.547-0.453 1-1 1h-22c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h22c0.547 0 1 0.453 1 1zM24 5v2c0 0.547-0.453 1-1 1h-22c-0.547 0-1-0.453-1-1v-2c0-0.547 0.453-1 1-1h22c0.547 0 1 0.453 1 1z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-elliptical-roi" viewBox="0 0 24 28">
|
||||
<title>Elliptical ROI</title>
|
||||
<path d="M12 5.5c-4.688 0-8.5 3.813-8.5 8.5s3.813 8.5 8.5 8.5 8.5-3.813 8.5-8.5-3.813-8.5-8.5-8.5zM24 14c0 6.625-5.375 12-12 12s-12-5.375-12-12 5.375-12 12-12v0c6.625 0 12 5.375 12 12z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-magnify" viewBox="0 0 24 28">
|
||||
<title>Magnify</title>
|
||||
<path d="M24 14c0 6.625-5.375 12-12 12s-12-5.375-12-12 5.375-12 12-12 12 5.375 12 12z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-reset" viewBox="0 0 24 28">
|
||||
<title>Reset</title>
|
||||
<path d="M24 14c0 6.609-5.391 12-12 12-3.578 0-6.953-1.578-9.234-4.328-0.156-0.203-0.141-0.5 0.031-0.672l2.141-2.156c0.109-0.094 0.25-0.141 0.391-0.141 0.141 0.016 0.281 0.078 0.359 0.187 1.531 1.984 3.828 3.109 6.312 3.109 4.406 0 8-3.594 8-8s-3.594-8-8-8c-2.047 0-3.984 0.781-5.437 2.141l2.141 2.156c0.297 0.281 0.375 0.719 0.219 1.078-0.156 0.375-0.516 0.625-0.922 0.625h-7c-0.547 0-1-0.453-1-1v-7c0-0.406 0.25-0.766 0.625-0.922 0.359-0.156 0.797-0.078 1.078 0.219l2.031 2.016c2.203-2.078 5.187-3.313 8.266-3.313 6.609 0 12 5.391 12 12z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-rotate" viewBox="0 0 24 28">
|
||||
<title>Rotate</title>
|
||||
<path d="M24 4v7c0 0.547-0.453 1-1 1h-7c-0.406 0-0.766-0.25-0.922-0.625-0.156-0.359-0.078-0.797 0.219-1.078l2.156-2.156c-1.469-1.359-3.406-2.141-5.453-2.141-4.406 0-8 3.594-8 8s3.594 8 8 8c2.484 0 4.781-1.125 6.312-3.109 0.078-0.109 0.219-0.172 0.359-0.187 0.141 0 0.281 0.047 0.391 0.141l2.141 2.156c0.187 0.172 0.187 0.469 0.031 0.672-2.281 2.75-5.656 4.328-9.234 4.328-6.609 0-12-5.391-12-12s5.391-12 12-12c3.078 0 6.062 1.234 8.266 3.313l2.031-2.016c0.281-0.297 0.719-0.375 1.094-0.219 0.359 0.156 0.609 0.516 0.609 0.922z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-rotate-right" viewBox="0 0 24 24">
|
||||
<title>Rotate Right</title>
|
||||
<path d="M16.875 15.469c0.563-0.75 0.891-1.594 1.031-2.484h2.016c-0.188 1.406-0.703 2.719-1.594 3.891zM12.984 17.906c0.891-0.141 1.734-0.469 2.484-1.031l1.453 1.453c-1.172 0.891-2.531 1.406-3.938 1.594v-2.016zM19.922 11.016h-2.016c-0.141-0.891-0.469-1.734-1.031-2.484l1.453-1.406c0.891 1.172 1.406 2.484 1.594 3.891zM15.563 5.531l-4.547 4.453v-3.891c-2.859 0.469-5.016 2.953-5.016 5.906s2.156 5.438 5.016 5.906v2.016c-3.938-0.469-7.031-3.844-7.031-7.922s3.094-7.453 7.031-7.922v-3.094z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-cineplay-toggle" viewBox="0 0 28 28">
|
||||
<title>Cineplay Toggle</title>
|
||||
<path d="M11.109 17.625l7.562-3.906-7.562-3.953v7.859zM14 4.156c5.891 0 9.797 0.281 9.797 0.281 0.547 0.063 1.75 0.063 2.812 1.188 0 0 0.859 0.844 1.109 2.781 0.297 2.266 0.281 4.531 0.281 4.531v2.125s0.016 2.266-0.281 4.531c-0.25 1.922-1.109 2.781-1.109 2.781-1.062 1.109-2.266 1.109-2.812 1.172 0 0-3.906 0.297-9.797 0.297v0c-7.281-0.063-9.516-0.281-9.516-0.281-0.625-0.109-2.031-0.078-3.094-1.188 0 0-0.859-0.859-1.109-2.781-0.297-2.266-0.281-4.531-0.281-4.531v-2.125s-0.016-2.266 0.281-4.531c0.25-1.937 1.109-2.781 1.109-2.781 1.062-1.125 2.266-1.125 2.812-1.188 0 0 3.906-0.281 9.797-0.281v0z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-flip-vertical" viewBox="0 0 6 28">
|
||||
<title>Vertical</title>
|
||||
<path d="M6 19.5v3c0 0.828-0.672 1.5-1.5 1.5h-3c-0.828 0-1.5-0.672-1.5-1.5v-3c0-0.828 0.672-1.5 1.5-1.5h3c0.828 0 1.5 0.672 1.5 1.5zM6 11.5v3c0 0.828-0.672 1.5-1.5 1.5h-3c-0.828 0-1.5-0.672-1.5-1.5v-3c0-0.828 0.672-1.5 1.5-1.5h3c0.828 0 1.5 0.672 1.5 1.5zM6 3.5v3c0 0.828-0.672 1.5-1.5 1.5h-3c-0.828 0-1.5-0.672-1.5-1.5v-3c0-0.828 0.672-1.5 1.5-1.5h3c0.828 0 1.5 0.672 1.5 1.5z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-tools-flip-horizontal" viewBox="0 0 22 28">
|
||||
<title>Horizontal</title>
|
||||
<path d="M6 11.5v3c0 0.828-0.672 1.5-1.5 1.5h-3c-0.828 0-1.5-0.672-1.5-1.5v-3c0-0.828 0.672-1.5 1.5-1.5h3c0.828 0 1.5 0.672 1.5 1.5zM14 11.5v3c0 0.828-0.672 1.5-1.5 1.5h-3c-0.828 0-1.5-0.672-1.5-1.5v-3c0-0.828 0.672-1.5 1.5-1.5h3c0.828 0 1.5 0.672 1.5 1.5zM22 11.5v3c0 0.828-0.672 1.5-1.5 1.5h-3c-0.828 0-1.5-0.672-1.5-1.5v-3c0-0.828 0.672-1.5 1.5-1.5h3c0.828 0 1.5 0.672 1.5 1.5z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-trial-info" viewBox="0 0 20 20">
|
||||
<title>Trial Information</title>
|
||||
<g id="icon-trial-info-group" fill="none" stroke-width="2">
|
||||
<path id="icon-trial-info-circle" d="M10,1 a9 9 0 0 1 0 18 a9 9 0 0 1 0 -18 M10,5 l0,2 m0,2 0,6" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-ui-accordion-expand" viewBox="0 0 12 8">
|
||||
<title>Expand</title>
|
||||
<polygon transform="translate(6.000000, 3.700000) rotate(-270.000000) translate(-6.000000, -3.700000) " points="3.7 -2.3 2.3 -0.9 6.9 3.7 2.3 8.3 3.7 9.7 9.7 3.7"/>
|
||||
</symbol>
|
||||
<symbol id="icon-ui-add" viewBox="0 0 12 13">
|
||||
<title>Add</title>
|
||||
<path d="M6.5 7L6.5 12C6.5 12.28 6.28 12.5 6 12.5 5.72 12.5 5.5 12.28 5.5 12L5.5 7 0.5 7C0.22 7 0 6.78 0 6.5 0 6.22 0.22 6 0.5 6L5.5 6 5.5 1C5.5 0.72 5.72 0.5 6 0.5 6.28 0.5 6.5 0.72 6.5 1L6.5 6 11.5 6C11.78 6 12 6.22 12 6.5 12 6.78 11.78 7 11.5 7L6.5 7Z" id="Combined-Shape"/>
|
||||
</symbol>
|
||||
<symbol id="icon-ui-close" viewBox="0 0 14 14">
|
||||
<title>Close</title>
|
||||
<g id="icon-ui-close-group" stroke-width="1.75">
|
||||
<path id="icon-ui-close-path" d="M1,1 13,13 M1,13 13,1" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-create-comment" viewBox="0 0 37 34">
|
||||
<title>Comment</title>
|
||||
<path fill="#94a8b3" d="M22.32 14L35 14C36.1 14 37 14.9 37 16L37 28C37 29.1 36.1 30 35 30L21 30 17 34 17 20.39C19.45 18.97 21.36 16.71 22.32 14Z" id="Combined-Shape"/>
|
||||
<path d="M10 0C4.5 0 0 4.5 0 10 0 15.5 4.5 20 10 20 15.5 20 20 15.5 20 10 20 4.5 15.5 0 10 0L10 0ZM15 11L11 11 11 15 9 15 9 11 5 11 5 9 9 9 9 5 11 5 11 9 15 9 15 11 15 11Z" id="Shape"/>
|
||||
</symbol>
|
||||
<symbol id="icon-create-screen-capture" viewBox="0 0 37 35">
|
||||
<title>Capture Screen</title>
|
||||
<g id="camera-alt" transform="translate(17.000000, 17.000000)" fill="#94a8b3">
|
||||
<path d="M10 13C11.66 13 13 11.66 13 10 13 8.34 11.66 7 10 7 8.34 7 7 8.34 7 10 7 11.66 8.34 13 10 13Z" id="Oval"/>
|
||||
<path d="M7 0L5.2 2 2 2C0.9 2 0 2.9 0 4L0 16C0 17.1 0.9 18 2 18L18 18C19.1 18 20 17.1 20 16L20 4C20 2.9 19.1 2 18 2L14.8 2 13 0 7 0 7 0ZM10 15C7.2 15 5 12.8 5 10 5 7.2 7.2 5 10 5 12.8 5 15 7.2 15 10 15 12.8 12.8 15 10 15L10 15Z" id="Shape"/>
|
||||
</g>
|
||||
<path d="M10 0C4.5 0 0 4.5 0 10 0 15.5 4.5 20 10 20 15.5 20 20 15.5 20 10 20 4.5 15.5 0 10 0L10 0ZM15 11L11 11 11 15 9 15 9 11 5 11 5 9 9 9 9 5 11 5 11 9 15 9 15 11 15 11Z" id="Shape"/>
|
||||
</symbol>
|
||||
<symbol id="icon-ui-warning" viewBox="0 0 22 20">
|
||||
<title>Warning</title>
|
||||
<g id="icon-ui-warning-group" stroke="none">
|
||||
<path d="m11,0 11,20 -22,0Z m1,7 -2,0 0,6 2,0Z m0,8 -2,0 0,2 2,0Z" />
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-ohif-logo" viewBox="0 0 47 47">
|
||||
<title>Open Health Imaging Foundation</title>
|
||||
<g id="icon-ohif-logo" stroke="#fff" stroke-width="2.5" stroke-miterlimit="10">
|
||||
<rect x="1.25" y="1.25" width="18.8" height="18.8" rx="1" ry="1"/>
|
||||
<rect x="26" y="1.25" width="18.8" height="18.8" rx="1" ry="1"/>
|
||||
<rect x="1.25" y="26" width="18.8" height="18.8" rx="1" ry="1"/>
|
||||
<rect x="26" y="26" width="18.8" height="18.8" rx="1" ry="1"/>
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="icon-viewport-link" viewBox="0 0 32 32">
|
||||
<title>Viewport Link</title>
|
||||
<g id="icon-viewport-link-group" stroke="none">
|
||||
<path d="m5.364 28.917c1.047 1.046 2.422 1.57 3.797 1.57 1.376 0 2.751-0.523 3.798-1.572l7.552-7.554c1.016-1.014 1.574-2.361 1.574-3.797 -0.001-1.432-0.558-2.781-1.574-3.796l-2.297-2.298 -2.009 2.009 2.297 2.298c0.478 0.478 0.741 1.113 0.741 1.788 0 0.674-0.264 1.309-0.741 1.786l-7.553 7.555c-0.987 0.985-2.59 0.985-3.576 0l-2.297-2.298c-0.987-0.985-0.987-2.589 0-3.576l3.775-3.776 -2.009-2.009 -3.776 3.776c-2.094 2.096-2.092 5.502 0 7.595l2.299 2.3zM26.695 2.992"/>
|
||||
<path d="m26.695 2.992c-1.014-1.016-2.362-1.575-3.797-1.575 -0.001 0-0.001 0-0.002 0 -1.435 0-2.784 0.56-3.798 1.573l-7.551 7.553c-1.017 1.016-1.576 2.363-1.576 3.799 0 1.434 0.558 2.784 1.574 3.797l2.297 2.297 2.01-2.009 -2.298-2.297c-0.477-0.477-0.741-1.113-0.741-1.788 0-0.676 0.265-1.311 0.742-1.788l7.553-7.555c0.477-0.477 1.111-0.74 1.789-0.74 0 0 0 0 0.001 0 0.674 0 1.309 0.264 1.786 0.74l2.297 2.299c0.477 0.477 0.74 1.111 0.74 1.788 0 0.674-0.264 1.311-0.74 1.788l-3.776 3.777 2.009 2.009 3.776-3.777c1.014-1.013 1.573-2.363 1.574-3.797 0-1.435-0.56-2.784-1.574-3.797l-2.295-2.296z"/>
|
||||
</g>
|
||||
</symbol>
|
||||
<symbol id="theme" viewBox="0 0 24 24">
|
||||
<title>Theme</title>
|
||||
<path d="M17.484 12c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM14.484 8.016c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM9.516 8.016c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM6.516 12c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM12 3c4.969 0 9 3.609 9 8.016 0 2.766-2.25 4.969-5.016 4.969h-1.734c-0.844 0-1.5 0.656-1.5 1.5 0 0.375 0.141 0.703 0.375 0.984s0.375 0.656 0.375 1.031c0 0.844-0.656 1.5-1.5 1.5-4.969 0-9-4.031-9-9s4.031-9 9-9z"></path>
|
||||
</symbol>
|
||||
<symbol id="log" viewBox="0 0 24 24">
|
||||
<title>Log</title>
|
||||
<path d="M9.516 14.016c2.484 0 4.5-2.016 4.5-4.5s-2.016-4.5-4.5-4.5-4.5 2.016-4.5 4.5 2.016 4.5 4.5 4.5zM15.516 14.016l4.969 4.969-1.5 1.5-4.969-4.969v-0.797l-0.281-0.281c-1.125 0.984-2.625 1.547-4.219 1.547-3.609 0-6.516-2.859-6.516-6.469s2.906-6.516 6.516-6.516 6.469 2.906 6.469 6.516c0 1.594-0.563 3.094-1.547 4.219l0.281 0.281h0.797z"></path>
|
||||
</symbol>
|
||||
<symbol id="server" viewBox="0 0 24 28">
|
||||
<title>Server</title>
|
||||
<path d="M12 12c4.703 0 9.422-0.844 12-2.656v2.656c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2.656c2.578 1.813 7.297 2.656 12 2.656zM12 24c4.703 0 9.422-0.844 12-2.656v2.656c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2.656c2.578 1.813 7.297 2.656 12 2.656zM12 18c4.703 0 9.422-0.844 12-2.656v2.656c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2.656c2.578 1.813 7.297 2.656 12 2.656zM12 0c6.625 0 12 1.797 12 4v2c0 2.203-5.375 4-12 4s-12-1.797-12-4v-2c0-2.203 5.375-4 12-4z"></path>
|
||||
</symbol>
|
||||
<symbol id="study-list" viewBox="0 0 24 24">
|
||||
<title>Study List</title>
|
||||
<path d="M9 5.016h12v3.984h-12v-3.984zM9 18.984v-3.984h12v3.984h-12zM9 14.016v-4.031h12v4.031h-12zM3.984 9v-3.984h4.031v3.984h-4.031zM3.984 18.984v-3.984h4.031v3.984h-4.031zM3.984 14.016v-4.031h4.031v4.031h-4.031z"></path>
|
||||
</symbol>
|
||||
<symbol id="logout" viewBox="0 0 24 28">
|
||||
<title>Logout</title>
|
||||
<path d="M24 14c0 6.609-5.391 12-12 12s-12-5.391-12-12c0-3.797 1.75-7.297 4.797-9.578 0.891-0.672 2.141-0.5 2.797 0.391 0.672 0.875 0.484 2.141-0.391 2.797-2.031 1.531-3.203 3.859-3.203 6.391 0 4.406 3.594 8 8 8s8-3.594 8-8c0-2.531-1.172-4.859-3.203-6.391-0.875-0.656-1.062-1.922-0.391-2.797 0.656-0.891 1.922-1.062 2.797-0.391 3.047 2.281 4.797 5.781 4.797 9.578zM14 2v10c0 1.094-0.906 2-2 2s-2-0.906-2-2v-10c0-1.094 0.906-2 2-2s2 0.906 2 2z"></path>
|
||||
</symbol>
|
||||
<symbol id="password" viewBox="0 0 18 28">
|
||||
<title>Password</title>
|
||||
<path d="M5 12h8v-3c0-2.203-1.797-4-4-4s-4 1.797-4 4v3zM18 13.5v9c0 0.828-0.672 1.5-1.5 1.5h-15c-0.828 0-1.5-0.672-1.5-1.5v-9c0-0.828 0.672-1.5 1.5-1.5h0.5v-3c0-3.844 3.156-7 7-7s7 3.156 7 7v3h0.5c0.828 0 1.5 0.672 1.5 1.5z"></path>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 21 KiB |
+191
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Entry point index.js for development
|
||||
*/
|
||||
|
||||
import App from './App.js';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
export { App };
|
||||
|
||||
var rootUrl = 'http://localhost:5000';
|
||||
var routerBasename = '/';
|
||||
var props = {
|
||||
routerBasename: routerBasename,
|
||||
rootUrl: rootUrl
|
||||
};
|
||||
|
||||
/*
|
||||
Example config with OIDC
|
||||
*/
|
||||
// Uncomment the next two blocks, comment out the config without OIDC
|
||||
// Try going to:
|
||||
// http://localhost:5000/viewer/1.2.276.0.7230010.3.1.2.0.94237.1537373823.634387 //PDF
|
||||
// http://localhost:5000/viewer/1.3.6.1.4.1.25403.345050719074.3824.20170126082328.1
|
||||
// http://ohif-viewer-react.s3-website-us-east-1.amazonaws.com/viewer/1.3.6.1.4.1.25403.345050719074.3824.20170126082328.1
|
||||
/*props.servers = {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot:
|
||||
'https://cancer.crowds-cure.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||
qidoRoot: 'https://cancer.crowds-cure.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
wadoRoot: 'https://cancer.crowds-cure.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
requestOptions: {
|
||||
requestFromBrowser: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
props.oidc = [
|
||||
{
|
||||
authServerUrl: 'https://cancer.crowds-cure.org/auth/realms/dcm4che',
|
||||
authRedirectUri: rootUrl + '/callback',
|
||||
postLogoutRedirectUri: rootUrl + '/logout-redirect.html',
|
||||
clientId: 'crowds-cure-cancer',
|
||||
responseType: 'id_token token',
|
||||
scope: 'email profile openid',
|
||||
revokeAccessTokenOnSignout: true,
|
||||
extraQueryParams: {
|
||||
kc_idp_hint: 'crowds-cure-cancer-auth0-oidc',
|
||||
client_id: 'crowds-cure-cancer'
|
||||
}
|
||||
}
|
||||
];*/
|
||||
|
||||
/* props.servers = {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot:
|
||||
'https://k8s-testing.ohif.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||
qidoRoot: 'https://k8s-testing.ohif.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
wadoRoot: 'https://k8s-testing.ohif.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
requestOptions: {
|
||||
requestFromBrowser: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
props.oidc = [
|
||||
{
|
||||
authServerUrl: 'https://k8s-testing.ohif.org/auth/realms/dcm4che',
|
||||
authRedirectUri: rootUrl + '/callback',
|
||||
postLogoutRedirectUri: rootUrl + '/logout-redirect.html',
|
||||
clientId: 'ohif-viewer',
|
||||
responseType: 'id_token token',
|
||||
scope: 'email profile openid',
|
||||
revokeAccessTokenOnSignout: true
|
||||
}
|
||||
]; */
|
||||
|
||||
/* Example config without OIDC */
|
||||
props.servers = {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
requestOptions: {
|
||||
requestFromBrowser: true
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/*props.servers = {
|
||||
"dicomWeb": [
|
||||
{
|
||||
"name": "DCM4CHEE",
|
||||
"wadoUriRoot": "http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/wado",
|
||||
"qidoRoot": "http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs",
|
||||
"wadoRoot": "http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs",
|
||||
"qidoSupportsIncludeField": true,
|
||||
"imageRendering": "wadors",
|
||||
"thumbnailRendering": "wadors",
|
||||
"requestOptions": {
|
||||
"requestFromBrowser": true,
|
||||
"auth": "admin:admin"
|
||||
}
|
||||
}
|
||||
]
|
||||
};*/
|
||||
|
||||
/*
|
||||
UI settings
|
||||
Plugins
|
||||
- Custom tools / buttons
|
||||
- Custom Sidebar module thing
|
||||
- Custom Viewports
|
||||
- Custom Sop Class Interpreters
|
||||
*/
|
||||
|
||||
/*"PUBLIC_SETTINGS": {
|
||||
"ui": {
|
||||
"studyListFunctionsEnabled": true,
|
||||
"leftSidebarOpen": false,
|
||||
"displaySetNavigationLoopOverSeries": false,
|
||||
"displaySetNavigationMultipleViewports": true,
|
||||
"autoPositionMeasurementsTextCallOuts": "TRLB"
|
||||
},
|
||||
"prefetch": {
|
||||
"order": "topdown",
|
||||
"displaySetCount": 1
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
White labelling example
|
||||
|
||||
// Note that you can't write JSX here, so you can use the online JSX compiler:
|
||||
// https://babeljs.io/repl/#?babili=false&browsers=&build=&builtIns=false&spec=false&loose=false&code_lz=GYVwdgxgLglg9mABACwKYBt1wBQEpEDeAUIogE6pQhlIA8AholPWQOaUC8ARAPoBG6emADWXchm5g4cAA6owqMoikVgiimTERBAZx0A5egFtU3NPQAmigLR8yQi2OSqzUKDIBcAei_2LMCHp0GCN6VhgwVgA6CDgjLgA-ElIU1LS02hDWRB0yCA4AcmQ3Tx8_AKCQsIjo2KMvAHcZa1iwKHkoLxAZLEsdLwAmAAYARgB2LxGRrwgyWTkLFrmZBetwMGNURYGomUiCxC8k9JOU2i96BIBuIgBfIiA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=react&prettier=false&targets=&version=6.26.0&envVersion=
|
||||
*/
|
||||
/*function RadicalImagingLogo() {
|
||||
return React.createElement(
|
||||
'a',
|
||||
{
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer',
|
||||
className: 'header-brand',
|
||||
href: 'http://radicalimaging.com'
|
||||
},
|
||||
React.createElement('h5', {}, 'RADICAL IMAGING')
|
||||
);
|
||||
}
|
||||
|
||||
props.whiteLabelling = {
|
||||
logoComponent: RadicalImagingLogo()
|
||||
};*/
|
||||
|
||||
// Note: Run your build like this:
|
||||
// REACT_APP_CONFIG=$(cat ../config-react/ccc.json) yarn start
|
||||
//
|
||||
// If you change the JSON config, you need to re-run the command!
|
||||
const configString = process.env && process.env && process.env.REACT_APP_CONFIG;
|
||||
if (configString) {
|
||||
const configJSON = JSON.parse(configString);
|
||||
if (configJSON.servers) {
|
||||
props.servers = configJSON.servers;
|
||||
}
|
||||
|
||||
if (configJSON.oidc) {
|
||||
props.oidc = configJSON.oidc;
|
||||
}
|
||||
}
|
||||
|
||||
var app = React.createElement(App, props, null);
|
||||
|
||||
ReactDOM.render(app, document.getElementById('root'));
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Entry point index.js for rollup packaging
|
||||
*/
|
||||
|
||||
import App from './App.js';
|
||||
export { App };
|
||||
@@ -0,0 +1,6 @@
|
||||
import loglevel from 'loglevel';
|
||||
|
||||
const log = loglevel.getLogger('OHIFViewer');
|
||||
log.setLevel('info');
|
||||
|
||||
export default log;
|
||||
@@ -0,0 +1,22 @@
|
||||
export const setLeftSidebarOpen = state => ({
|
||||
type: 'SET_LEFT_SIDEBAR_OPEN',
|
||||
state
|
||||
});
|
||||
|
||||
export const setRightSidebarOpen = state => ({
|
||||
type: 'SET_RIGHT_SIDEBAR_OPEN',
|
||||
state
|
||||
});
|
||||
|
||||
export const setUserPreferencesModalOpen = state => ({
|
||||
type: 'SET_USER_PREFERENCES_MODAL_OPEN',
|
||||
state
|
||||
});
|
||||
|
||||
const actions = {
|
||||
setLeftSidebarOpen,
|
||||
setRightSidebarOpen,
|
||||
setUserPreferencesModalOpen,
|
||||
};
|
||||
|
||||
export default actions;
|
||||
@@ -0,0 +1,22 @@
|
||||
const defaultState = {
|
||||
leftSidebarOpen: true,
|
||||
rightSidebarOpen: false,
|
||||
userPreferencesModalOpen: false
|
||||
};
|
||||
|
||||
const ui = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_LEFT_SIDEBAR_OPEN':
|
||||
return Object.assign({}, state, { leftSidebarOpen: action.state });
|
||||
case 'SET_RIGHT_SIDEBAR_OPEN':
|
||||
return Object.assign({}, state, { rightSidebarOpen: action.state });
|
||||
case 'SET_USER_PREFERENCES_MODAL_OPEN':
|
||||
return Object.assign({}, state, {
|
||||
userPreferencesModalOpen: action.state
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default ui;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import ViewerRetrieveStudyData from "../connectedComponents/ViewerRetrieveStudyData.js";
|
||||
|
||||
function IHEInvokeImageDisplay({ match }) {
|
||||
const requestType = match.params.query.requestType;
|
||||
let studyInstanceUids;
|
||||
//let patientUids;
|
||||
let displayStudyList = false;
|
||||
|
||||
if (requestType === "STUDY") {
|
||||
studyInstanceUids = match.params.query.studyUID.split(';');
|
||||
} else if (requestType === "STUDYBASE64") {
|
||||
const uids = this.params.query.studyUID;
|
||||
const decodedData = window.atob(uids);
|
||||
studyInstanceUids = decodedData.split(';');
|
||||
} else if (requestType === "PATIENT") {
|
||||
//patientUidspatientUids = this.params.query.patientID.split(';');
|
||||
displayStudyList = true
|
||||
} else {
|
||||
displayStudyList = true
|
||||
}
|
||||
|
||||
if (displayStudyList) {
|
||||
return ('')//<StudyList patientUids={patientUids}/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewerRetrieveStudyData studyInstanceUids={studyInstanceUids}/>
|
||||
);
|
||||
}
|
||||
|
||||
IHEInvokeImageDisplay.propTypes = {
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
query: PropTypes.shape({
|
||||
requestType: PropTypes.string.isRequired,
|
||||
studyUID: PropTypes.string,
|
||||
patientID: PropTypes.string
|
||||
})
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
export default IHEInvokeImageDisplay;
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import qs from 'query-string'
|
||||
import Viewer from "../connectedComponents/Viewer";
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
const { createDisplaySets } = OHIF.utils;
|
||||
|
||||
class StandaloneRouting extends Component {
|
||||
state = {
|
||||
studies: null,
|
||||
error: null
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
location: PropTypes.object,
|
||||
store: PropTypes.object
|
||||
};
|
||||
|
||||
static parseQueryAndFetchStudies(query) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = query.url;
|
||||
|
||||
if (!url) {
|
||||
reject(new Error('No URL was specified. Use ?url=$yourURL'));
|
||||
}
|
||||
|
||||
// Define a request to the server to retrieve the study data
|
||||
// as JSON, given a URL that was in the Route
|
||||
const oReq = new XMLHttpRequest();
|
||||
|
||||
// Add event listeners for request failure
|
||||
oReq.addEventListener('error', (error) => {
|
||||
OHIF.log.warn('An error occurred while retrieving the JSON data');
|
||||
reject(error)
|
||||
});
|
||||
|
||||
// When the JSON has been returned, parse it into a JavaScript Object
|
||||
// and render the OHIF Viewer with this data
|
||||
oReq.addEventListener('load', () => {
|
||||
// Parse the response content
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseText
|
||||
if (!oReq.responseText) {
|
||||
OHIF.log.warn('Response was undefined');
|
||||
reject(new Error('Response was undefined'));
|
||||
}
|
||||
|
||||
OHIF.log.info(JSON.stringify(oReq.responseText, null, 2));
|
||||
|
||||
const data = JSON.parse(oReq.responseText);
|
||||
if (data.servers && query.studyInstanceUids) {
|
||||
const server = data.servers.dicomWeb[0];
|
||||
server.type = 'dicomWeb';
|
||||
|
||||
const studyInstanceUids = query.studyInstanceUids.split(';');
|
||||
const seriesInstanceUids = [];
|
||||
|
||||
OHIF.studies.retrieveStudiesMetadata(server, studyInstanceUids, seriesInstanceUids).then(studies => {
|
||||
resolve(studies);
|
||||
}, error => {
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
resolve(data.studies);
|
||||
}
|
||||
});
|
||||
|
||||
// Open the Request to the server for the JSON data
|
||||
// In this case we have a server-side route called /api/
|
||||
// which responds to GET requests with the study data
|
||||
OHIF.log.info(`Sending Request to: ${url}`);
|
||||
oReq.open('GET', url);
|
||||
oReq.setRequestHeader('Accept', 'application/json')
|
||||
|
||||
// Fire the request to the server
|
||||
oReq.send();
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const query = qs.parse(this.props.location.search);
|
||||
StandaloneRouting.parseQueryAndFetchStudies(query).then(studies => {
|
||||
const updatedStudies = createDisplaySets(studies);
|
||||
|
||||
this.setState({ studies: updatedStudies });
|
||||
}, error => {
|
||||
this.setState({ error });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (<div>Error: {JSON.stringify(this.state.error)}</div>);
|
||||
} else if (!this.state.studies) {
|
||||
return (<div>Loading...</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<Viewer studies={this.state.studies}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StandaloneRouting;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import ConnectedViewerRetrieveStudyData from "../connectedComponents/ConnectedViewerRetrieveStudyData";
|
||||
|
||||
function ViewerRouting({ match }) {
|
||||
const { studyInstanceUids, seriesInstanceUids } = match.params;
|
||||
|
||||
let studyUIDs;
|
||||
let seriesUIDs;
|
||||
|
||||
if (studyInstanceUids && !seriesInstanceUids) {
|
||||
studyUIDs = studyInstanceUids.split(';');
|
||||
} else if (studyInstanceUids && seriesInstanceUids) {
|
||||
studyUIDs = [match.params.studyInstanceUid];
|
||||
seriesUIDs = match.params.seriesInstanceUids.split(';');
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectedViewerRetrieveStudyData studyInstanceUids={studyUIDs} seriesInstanceUids={seriesUIDs}/>
|
||||
);
|
||||
}
|
||||
|
||||
ViewerRouting.propTypes = {
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
studyInstanceUids: PropTypes.string.isRequired,
|
||||
seriesInstanceUids: PropTypes.string
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
export default ViewerRouting;
|
||||
@@ -0,0 +1,17 @@
|
||||
import IHEInvokeImageDisplay from './IHEInvokeImageDisplay.js';
|
||||
import StandaloneRouting from './StandaloneRouting.js';
|
||||
import ViewerRouting from './ViewerRouting.js';
|
||||
|
||||
export {
|
||||
IHEInvokeImageDisplay,
|
||||
ViewerRouting,
|
||||
StandaloneRouting
|
||||
};
|
||||
|
||||
const routes = {
|
||||
IHEInvokeImageDisplay,
|
||||
ViewerRouting,
|
||||
StandaloneRouting
|
||||
};
|
||||
|
||||
export default routes;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import StudyListWithData from './StudyListWithData.js';
|
||||
|
||||
const isActive = (a) => a.active === true;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const activeServer = state.servers.servers.find(isActive);
|
||||
|
||||
return {
|
||||
server: activeServer,
|
||||
user: state.oidc.user,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedStudyList = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(StudyListWithData);
|
||||
|
||||
export default ConnectedStudyList;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import ConnectedStudyList from "./ConnectedStudyList";
|
||||
|
||||
// TODO: Move to react-viewerbase
|
||||
|
||||
function StudyListRouting({ match }) {
|
||||
// TODO: Figure out which filters we want to pass in via a URL
|
||||
//const { patientId } = match.params;
|
||||
|
||||
return (
|
||||
<ConnectedStudyList/>
|
||||
);
|
||||
}
|
||||
|
||||
StudyListRouting.propTypes = {
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
patientIds: PropTypes.string,
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
export default StudyListRouting;
|
||||
@@ -0,0 +1,151 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF from 'ohif-core';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { StudyList } from 'react-viewerbase';
|
||||
import ConnectedHeader from '../connectedComponents/ConnectedHeader.js';
|
||||
import moment from 'moment';
|
||||
|
||||
class StudyListWithData extends Component {
|
||||
state = {
|
||||
searchData: {},
|
||||
studies: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
patientId: PropTypes.string,
|
||||
server: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
history: PropTypes.object
|
||||
};
|
||||
|
||||
static rowsPerPage = 25;
|
||||
static defaultSort = { field: 'patientName', order: 'desc' };
|
||||
|
||||
static studyListDateFilterNumDays = 25000; // TODO: put this in the settings
|
||||
static defaultStudyDateFrom = moment()
|
||||
.subtract(StudyListWithData.studyListDateFilterNumDays, 'days')
|
||||
.toDate();
|
||||
static defaultStudyDateTo = new Date();
|
||||
|
||||
componentDidMount() {
|
||||
// TODO: Avoid using timepoints here
|
||||
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
|
||||
|
||||
this.searchForStudies();
|
||||
}
|
||||
|
||||
searchForStudies = (
|
||||
searchData = {
|
||||
currentPage: 0,
|
||||
rowsPerPage: StudyListWithData.rowsPerPage,
|
||||
studyDateFrom: StudyListWithData.defaultStudyDateFrom,
|
||||
studyDateTo: StudyListWithData.defaultStudyDateTo,
|
||||
sortData: StudyListWithData.defaultSort
|
||||
}
|
||||
) => {
|
||||
const { server } = this.props;
|
||||
const filter = {
|
||||
patientId: searchData.patientId,
|
||||
patientName: searchData.patientName,
|
||||
accessionNumber: searchData.accessionNumber,
|
||||
studyDescription: searchData.studyDescription,
|
||||
modalitiesInStudy: searchData.modalities,
|
||||
studyDateFrom: searchData.studyDateFrom,
|
||||
studyDateTo: searchData.studyDateTo,
|
||||
limit: searchData.rowsPerPage,
|
||||
offset: searchData.currentPage * searchData.rowsPerPage
|
||||
};
|
||||
|
||||
// TODO: add sorting
|
||||
const promise = OHIF.studies.searchStudies(server, filter);
|
||||
|
||||
// Render the viewer when the data is ready
|
||||
promise
|
||||
.then(studies => {
|
||||
if (!studies) {
|
||||
studies = [];
|
||||
}
|
||||
|
||||
const { field, order } = searchData.sortData;
|
||||
const sortedStudies = studies
|
||||
.sort(function(a, b) {
|
||||
if (order === 'desc') {
|
||||
if (a[field] < b[field]) {
|
||||
return -1;
|
||||
}
|
||||
if (a[field] > b[field]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
if (a[field] > b[field]) {
|
||||
return -1;
|
||||
}
|
||||
if (a[field] < b[field]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
})
|
||||
.map(study => {
|
||||
study.studyDate = moment(study.studyDate, 'YYYYMMDD').format(
|
||||
'MMM DD, YYYY'
|
||||
);
|
||||
return study;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
studies: sortedStudies
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
error: true
|
||||
});
|
||||
|
||||
throw new Error(error);
|
||||
});
|
||||
};
|
||||
|
||||
onImport = () => {
|
||||
//console.log('onImport');
|
||||
};
|
||||
|
||||
onSelectItem = studyInstanceUID => {
|
||||
this.props.history.push(`/viewer/${studyInstanceUID}`);
|
||||
};
|
||||
|
||||
onSearch = searchData => {
|
||||
this.searchForStudies(searchData);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||
} else if (this.state.studies === null) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectedHeader home={true} user={this.props.user} />
|
||||
<StudyList
|
||||
studies={this.state.studies}
|
||||
studyListFunctionsEnabled={false}
|
||||
onImport={this.onImport}
|
||||
onSelectItem={this.onSelectItem}
|
||||
pageSize={this.rowsPerPage}
|
||||
defaultSort={StudyListWithData.defaultSort}
|
||||
studyListDateFilterNumDays={
|
||||
StudyListWithData.studyListDateFilterNumDays
|
||||
}
|
||||
onSearch={this.onSearch}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(StudyListWithData);
|
||||
@@ -0,0 +1,52 @@
|
||||
/* "Tide" theme */
|
||||
/* TODO:
|
||||
- Convert the other themes to CSS Variables
|
||||
- Allow user to set default CSS theme in JS config
|
||||
- Export the themes as identical JS objects
|
||||
- For theme switcher, update root-level CSS properties via JS (https://medium.com/@_bengarrison/accessing-and-modifying-css-variables-with-javascript-2ccb735bbff0)
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Interface UI Colors */
|
||||
--default-color: #9CCEF9;
|
||||
--hover-color: #ffffff;
|
||||
--active-color: #20A5D6;
|
||||
--ui-border-color: #44626F;
|
||||
--ui-border-color-dark: #3C5D80;
|
||||
--ui-border-color-active: #00a4d9;
|
||||
--primary-background-color: #000000;
|
||||
--box-background-color: #3E5975;
|
||||
|
||||
/* Common palette */
|
||||
--ui-yellow: #E29E4A;
|
||||
--ui-sky-blue: #6FBDE2;
|
||||
/* State palette */
|
||||
--ui-state-error: #FFCCCC;
|
||||
--ui-state-error-border: #993333;
|
||||
--ui-state-error-text: #661111;
|
||||
--ui-gray-light: #516873;
|
||||
--ui-gray: #263340;
|
||||
--ui-gray-dark: #16202B;
|
||||
--ui-gray-darker: #151A1F;
|
||||
--ui-gray-darkest: #14202A;
|
||||
|
||||
/* Text Colors */
|
||||
--text-color-active: #000000;
|
||||
--text-primary-color: #ffffff;
|
||||
--text-secondary-color: #91b9cd;
|
||||
--large-numbers-color: #6fbde2;
|
||||
--text-disabled-color: #878787;
|
||||
|
||||
--input-background-color: #2c363f;
|
||||
--input-placeholder-color: #d3d3d3;
|
||||
|
||||
--table-hover-color: #2c363f;
|
||||
--table-text-primary-color: #ffffff;
|
||||
--table-text-secondary-color: #91b9cd;
|
||||
|
||||
--large-numbers-color:#6FBDE2;
|
||||
|
||||
--state-error: #FFCCCC;
|
||||
--state-error-border: #FFCCCC;
|
||||
--state-error-text: #FFCCCC;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/* Sizes */
|
||||
:root {
|
||||
--top-bar-height: 40px;
|
||||
--top-bar-expanded-height: 160px;
|
||||
--toolbar-height: 78px;
|
||||
--toolbar-drawer-height: 62px;
|
||||
--left-sidebar-menu-width: 307px;
|
||||
--right-sidebar-menu-width: 323px;
|
||||
--study-list-padding: 8%;
|
||||
--study-list-padding-medium-screen: 10px;
|
||||
}
|
||||
|
||||
/* Fonts */
|
||||
:root {
|
||||
--logo-font-family: "Sanchez";
|
||||
--logo-font-weight: 300;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
:root {
|
||||
--transition-duration: 0.3s;
|
||||
--transition-effect: ease;
|
||||
--sidebar-transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Thicknesses */
|
||||
:root {
|
||||
--viewport-border-thickness: 1px;
|
||||
--ui-border-thickness: 1px;
|
||||
}
|
||||
Reference in new issue
Block a user