🧹🧹🧹 Don't mind me. Just moving things around

This commit is contained in:
dannyrb committed 2019-04-15 16:44:36 -04:00
1 parent 9a0d295ee6
commit 72cdebccf4
83 files changed
+479 -38

No files matched your search

+286
View File
@@ -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;
+32
View File
@@ -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);
+17
View File
@@ -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;
}
+106
View File
@@ -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)
);
+10
View File
@@ -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;
+108
View File
@@ -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;
}
+81
View File
@@ -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);
+3
View File
@@ -0,0 +1,3 @@
import Header from './Header'
export default Header
+23
View File
@@ -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;
}
+22
View File
@@ -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;
+43
View File
@@ -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;
+30
View File
@@ -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;
+40
View File
@@ -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;
+48
View File
@@ -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;
}
+108
View File
@@ -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;
+3
View File
@@ -0,0 +1,3 @@
.icon-rotate-120 {
transform: rotate(120deg);
}
+22
View File
@@ -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;
+17
View File
@@ -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));
}
+102
View File
@@ -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;
+10
View File
@@ -0,0 +1,10 @@
#viewer {
background-color: black;
width: 100%;
}
.loadingTextDiv {
color: var(--text-secondary-color);
font-size: 30px;
}
+193
View File
@@ -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;
+41
View File
@@ -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)
*/
+110
View File
@@ -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;
+15
View File
@@ -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>
+237
View File
@@ -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
View File
@@ -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'));
+6
View File
@@ -0,0 +1,6 @@
/**
* Entry point index.js for rollup packaging
*/
import App from './App.js';
export { App };
+6
View File
@@ -0,0 +1,6 @@
import loglevel from 'loglevel';
const log = loglevel.getLogger('OHIFViewer');
log.setLevel('info');
export default log;
+22
View File
@@ -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;
+22
View File
@@ -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;
+45
View File
@@ -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;
+104
View File
@@ -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;
+32
View File
@@ -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;
+17
View File
@@ -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;
+21
View File
@@ -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;
+24
View File
@@ -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;
+151
View File
@@ -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);
+52
View File
@@ -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;
}
+30
View File
@@ -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;
}