Chore/kill yalc with fire (#629)
* init * more shifting * Move @babel dependencies up to workspace root * Begin updating dependent libraries to use sync'd webpack builds w/ watches * Commit changes before a long weekend * Split packages into platform and extensions * Consolidate how/where we specify file/module type entrypoints * Make it possible to pass in extensions as App props * Resolve viewer's modules * Target for UMD bundle * Add Webpack Stylus loader * Provide WebPack build options for microscopy, vtk, and ui * Better entrypoint for extensions * Push changes up to switch PCs * Fix ESM symlink build for Viewers * Shift docs to root * Specify additional lerna config props * Begin providing basic readme info * Rename example extension folder * Shift links to bottom of doc * First attempt at a netlify deploy preview * Attempt to fix deploy preview build * push workspace enabled to initial command; remove second yarn install; use npx to call gitbook cli commands * Try to use global gitbook-cli * Export node_modules path * More agressive with modifying PATH * Follow deploy log output advice * try again to set bin path * Try swapping bash with a new shell * Try bash -l instead of exec bash * Annd let's try again * and again * We may have figured it out johnny, boy * We need to pull cornerstone-wado-image-loader files from hoisted node_modules * Attempt to fix ticks/escapes * Change copy syntax; try running tests on viewers from root for circleci * Lower version to match circleci image * Try alternative jest-canvas-mock location and version file syntax * Run version command instead of calling node directly * Try alternative jest transform * Set path and enable workspaces * Add codecov flags * Use cpx so our copying finishes? * Get all platform unit tests to run * Run and report individually and in parallel * Try once relying on codecov yaml to split w/ flags * Add projects to split by flags * Generate separate reports * Trigger codecov after everything has finished running; these may not support flags * Use recommended flags from issue comments for codecov * Fix typo * Remove clear flag * Shift back to calling codecov from root * Generate example for codecov issue * Try fixing paths * Simplify * Try using codecov orb * Bump circleci version * Upload core and viewer * Use full string paths * Remove individual codecov calls * Fix dir * Combine files before upload * Escape anchors * tryfix syntax * Escape string literal * Combine lines to reduce path * long paths * Use home alias * Fix filename * Try to see the contents of our cat'd file * Also upload `core` * Try running with aliased folder * Fix path; split PR and Merge unit tests into separate jobs * Set default threshold * Fix typo * Changing to scoped package names * Adding note * Catch more updates * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.0 - @ohif/extension-dicom-html@0.0.4-alpha.0 - @ohif/extension-dicom-microscopy@0.0.9-alpha.0 - @ohif/extension-dicom-pdf@0.0.8-alpha.0 - @ohif/extension-vtk@0.1.4-alpha.0 - @ohif/core@0.11.1-alpha.0 - @ohif/i18n@0.2.3-alpha.0 - @ohif/ui@0.2.18-alpha.0 - @ohif/viewer@0.0.22-alpha.0 * Clean up primary readme * Support for dev and dev:* commands * Shift build command; satisfy default PWA build * Remove old scripts * Clean up UI to set webpack scripts * Tidy up project links * Clean duplicate code in UI project * Update UI project's docs * Clean up ui and i18n config * Reduce duplicate code in `@ohif/core` * Fix @ohif/ui versions * bump @ohif/core versions * Redux testkit dep * Remove unused/duplicate config files for projects/packages * ohif-core --> @ohif/core * Safer publish command * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.1 - @ohif/extension-dicom-html@0.0.4-alpha.1 - @ohif/extension-dicom-microscopy@0.0.9-alpha.1 - @ohif/extension-dicom-pdf@0.0.8-alpha.1 - @ohif/extension-vtk@0.1.4-alpha.1 - @ohif/core@0.11.1-alpha.1 - @ohif/i18n@0.2.3-alpha.1 - @ohif/ui@0.2.18-alpha.1 - @ohif/viewer@0.0.22-alpha.1 * test version bump * Add global prettier config * chore(release): publish %s [skip ci] - @ohif/viewer@0.0.22-alpha.2 * test lerna publish * Update package.jsons * Fix build scripts * catch changes * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.2 - @ohif/extension-dicom-html@0.0.4-alpha.2 - @ohif/extension-dicom-microscopy@0.0.9-alpha.2 - @ohif/extension-dicom-pdf@0.0.8-alpha.2 - @ohif/extension-vtk@0.1.4-alpha.2 - @ohif/core@0.11.1-alpha.2 - @ohif/i18n@0.2.3-alpha.2 - @ohif/ui@0.2.18-alpha.2 - @ohif/viewer@0.0.22-alpha.3 * fix eslint issues caused by eslint 6 and our react-app config * Publish with canary tag * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.3 - @ohif/extension-dicom-html@0.0.4-alpha.3 - @ohif/extension-dicom-microscopy@0.0.9-alpha.3 - @ohif/extension-dicom-pdf@0.0.8-alpha.3 - @ohif/extension-vtk@0.1.4-alpha.3 - @ohif/core@0.11.1-alpha.3 - @ohif/i18n@0.2.3-alpha.3 - @ohif/ui@0.2.18-alpha.3 - @ohif/viewer@0.0.22-alpha.4 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.4 - @ohif/extension-dicom-html@0.0.4-alpha.4 - @ohif/extension-dicom-microscopy@0.0.9-alpha.4 - @ohif/extension-dicom-pdf@0.0.8-alpha.4 - @ohif/extension-vtk@0.1.4-alpha.4 - @ohif/core@0.11.1-alpha.4 - @ohif/i18n@0.2.3-alpha.4 - @ohif/ui@0.2.18-alpha.4 - @ohif/viewer@0.0.22-alpha.5 * Simplify build * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.5 - @ohif/extension-dicom-html@0.0.4-alpha.5 - @ohif/extension-dicom-microscopy@0.0.9-alpha.5 - @ohif/extension-dicom-pdf@0.0.8-alpha.5 - @ohif/extension-vtk@0.1.4-alpha.5 - @ohif/core@0.11.1-alpha.5 - @ohif/i18n@0.2.3-alpha.5 - @ohif/ui@0.2.18-alpha.5 - @ohif/viewer@0.0.22-alpha.6 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.6 - @ohif/extension-dicom-html@0.0.4-alpha.6 - @ohif/extension-dicom-microscopy@0.0.9-alpha.6 - @ohif/extension-dicom-pdf@0.0.8-alpha.6 - @ohif/extension-vtk@0.1.4-alpha.6 - @ohif/core@0.11.1-alpha.6 - @ohif/i18n@0.2.3-alpha.6 - @ohif/ui@0.2.18-alpha.6 - @ohif/viewer@0.0.22-alpha.7 * Split version and publish * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.7 - @ohif/extension-dicom-html@0.0.4-alpha.7 - @ohif/extension-dicom-microscopy@0.0.9-alpha.7 - @ohif/extension-dicom-pdf@0.0.8-alpha.7 - @ohif/extension-vtk@0.1.4-alpha.7 - @ohif/core@0.11.1-alpha.7 - @ohif/i18n@0.2.3-alpha.7 - @ohif/ui@0.2.18-alpha.7 - @ohif/viewer@0.0.22-alpha.8 * Try simplify netlify build * react-viewerbase --> @ohif/ui * add test for stylus loader * Try new way to specify publish path * Use an output folder that won't be ignored * Try move command * fiddle with paths * Dig for some information * Make sure we're in root for our commands * Remove discovery * Setup script-tag example * Fix typo * Move config folder * Update cornerstone extension to use shared/root webpack configs * capture changes * Update extension webpack configs * Don't check in docz output * Fix babel runtime resolution issue * Simplify ui build steps * Push in progress * Shift directories * Get viewer dev path to work * Fix viewer commonjs build * Fix build pwa config for viewers * Make sure our package specific command can be called from root * Update notes * Fix imports and misc. merge notes * Bump wado-image-loader version * Switch up commands to remove webworkers copy * Fix CI config * Fix more broken package references * Add react-dropzone * Persist so e2e test can use * Fix lockfile * Use cypress orb to simplify CI * Fix config * Try again * Install yarn latest for orb * Install yarn a different way * Wait on correct port * Run tests in viewer project * Fix cypress config * Make Cypress happier - use default location * Simplify e2e testing * Capture UI changes since fork * Catch core up * Grab latest deps after merge * Fix broken import * Make sure cornerstone-wado-image-loader resolves to 3+ * Note regarding orbs * Fix refresh on 404 routes * Fix odd error for cornerstone-wado-image-loader * Different target for UMD * Simplify dev script * Try to get fancy with netlify output * Let's do it again * Try to fix measurementTools issue * See if we can get our lerna magic to work on merge to monorepo branch
This commit is contained in:
1 parent
cac911f08e
commit
08ff214ea6
919 files changed
+45652
-52097
No files matched your search
@@ -0,0 +1,198 @@
|
||||
import './config';
|
||||
// Polyfills
|
||||
// PWA Only?
|
||||
import 'core-js/features/array/flat';
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import {
|
||||
CommandsManager,
|
||||
ExtensionManager,
|
||||
HotkeysManager,
|
||||
utils,
|
||||
} from '@ohif/core';
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
getUserManagerForOpenIdConnectClient,
|
||||
initWebWorkers,
|
||||
} from './utils/index.js';
|
||||
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
|
||||
// ~~ EXTENSIONS
|
||||
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions';
|
||||
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
||||
import OHIFStandaloneViewer from './OHIFStandaloneViewer';
|
||||
import { OidcProvider } from 'redux-oidc';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import WhiteLabellingContext from './WhiteLabellingContext';
|
||||
import { getActiveContexts } from './store/layout/selectors.js';
|
||||
import i18n from '@ohif/i18n';
|
||||
import setupTools from './setupTools.js';
|
||||
import store from './store';
|
||||
import UserManagerContext from './UserManagerContext';
|
||||
|
||||
// ~~~~ APP SETUP
|
||||
initCornerstoneTools({
|
||||
globalToolSyncEnabled: true,
|
||||
showSVGCursors: true,
|
||||
});
|
||||
|
||||
const commandsManagerConfig = {
|
||||
getAppState: () => store.getState(),
|
||||
getActiveContexts: () => getActiveContexts(store.getState()),
|
||||
};
|
||||
|
||||
const commandsManager = new CommandsManager(commandsManagerConfig);
|
||||
const hotkeysManager = new HotkeysManager(commandsManager);
|
||||
const extensionManager = new ExtensionManager({ commandsManager });
|
||||
|
||||
// CornerstoneTools and labeling/measurements?
|
||||
setupTools(store);
|
||||
// ~~~~ END APP SETUP
|
||||
|
||||
// TODO[react] Use a provider when the whole tree is React
|
||||
window.store = store;
|
||||
|
||||
class App extends Component {
|
||||
static propTypes = {
|
||||
routerBasename: PropTypes.string.isRequired,
|
||||
servers: PropTypes.object.isRequired,
|
||||
//
|
||||
oidc: PropTypes.array,
|
||||
whiteLabelling: PropTypes.object,
|
||||
extensions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
whiteLabelling: {},
|
||||
oidc: [],
|
||||
extensions: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (this.props.oidc.length) {
|
||||
const firstOpenIdClient = this.props.oidc[0];
|
||||
|
||||
const { protocol, host } = window.location;
|
||||
const { routerBasename } = this.props;
|
||||
const baseUri = `${protocol}//${host}${routerBasename}`;
|
||||
|
||||
const redirect_uri = firstOpenIdClient.redirect_uri || '/callback';
|
||||
const silent_redirect_uri =
|
||||
firstOpenIdClient.silent_redirect_uri || '/silent-refresh.html';
|
||||
const post_logout_redirect_uri =
|
||||
firstOpenIdClient.post_logout_redirect_uri || '/';
|
||||
|
||||
const openIdConnectConfiguration = Object.assign({}, firstOpenIdClient, {
|
||||
redirect_uri: _makeAbsoluteIfNecessary(redirect_uri, baseUri),
|
||||
silent_redirect_uri: _makeAbsoluteIfNecessary(
|
||||
silent_redirect_uri,
|
||||
baseUri
|
||||
),
|
||||
post_logout_redirect_uri: _makeAbsoluteIfNecessary(
|
||||
post_logout_redirect_uri,
|
||||
baseUri
|
||||
),
|
||||
});
|
||||
|
||||
this.userManager = getUserManagerForOpenIdConnectClient(
|
||||
store,
|
||||
openIdConnectConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
_initExtensions(this.props.extensions);
|
||||
_initServers(this.props.servers);
|
||||
initWebWorkers();
|
||||
}
|
||||
|
||||
render() {
|
||||
const userManager = this.userManager;
|
||||
|
||||
if (userManager) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<OidcProvider store={store} userManager={userManager}>
|
||||
<UserManagerContext.Provider value={userManager}>
|
||||
<Router basename={this.props.routerBasename}>
|
||||
<WhiteLabellingContext.Provider
|
||||
value={this.props.whiteLabelling}
|
||||
>
|
||||
<OHIFStandaloneViewer userManager={userManager} />
|
||||
</WhiteLabellingContext.Provider>
|
||||
</Router>
|
||||
</UserManagerContext.Provider>
|
||||
</OidcProvider>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Router basename={this.props.routerBasename}>
|
||||
<WhiteLabellingContext.Provider value={this.props.whiteLabelling}>
|
||||
<OHIFStandaloneViewer />
|
||||
</WhiteLabellingContext.Provider>
|
||||
</Router>
|
||||
</I18nextProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param
|
||||
*/
|
||||
function _initExtensions(extensions) {
|
||||
const defaultExtensions = [
|
||||
GenericViewerCommands,
|
||||
MeasurementsPanel,
|
||||
OHIFCornerstoneExtension,
|
||||
];
|
||||
const mergedExtensions = defaultExtensions.concat(extensions);
|
||||
extensionManager.registerExtensions(mergedExtensions);
|
||||
|
||||
// Must run after extension commands are registered
|
||||
if (window.config.hotkeys) {
|
||||
hotkeysManager.setHotkeys(window.config.hotkeys, true);
|
||||
}
|
||||
}
|
||||
|
||||
function _initServers(servers) {
|
||||
if (servers) {
|
||||
utils.addServers(servers, store);
|
||||
}
|
||||
}
|
||||
|
||||
function _isAbsoluteUrl(url) {
|
||||
return url.includes('http://') || url.includes('https://');
|
||||
}
|
||||
|
||||
function _makeAbsoluteIfNecessary(url, base_url) {
|
||||
if (_isAbsoluteUrl(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Make sure base_url and url are not duplicating slashes
|
||||
if (base_url[base_url.length - 1] === '/') {
|
||||
base_url = base_url.slice(0, base_url.length - 1);
|
||||
}
|
||||
|
||||
return base_url + url;
|
||||
}
|
||||
|
||||
export default App;
|
||||
export { commandsManager, extensionManager, hotkeysManager };
|
||||
@@ -0,0 +1,37 @@
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
background-color: black;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-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;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
}
|
||||
|
||||
#root .fade-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#root .fade-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 200ms;
|
||||
}
|
||||
|
||||
#root .fade-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#root .fade-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms;
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter, matchPath } from 'react-router';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { NProgress } from '@tanem/react-nprogress';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import { connect } from 'react-redux';
|
||||
import { ViewerbaseDragDropContext } from '@ohif/ui';
|
||||
import { SignoutCallbackComponent } from 'redux-oidc';
|
||||
// import asyncComponent from './components/AsyncComponent.js'
|
||||
import IHEInvokeImageDisplay from './routes/IHEInvokeImageDisplay.js';
|
||||
import ViewerRouting from './routes/ViewerRouting.js';
|
||||
import ViewerLocalFileData from './connectedComponents/ViewerLocalFileData.js';
|
||||
import StudyListRouting from './studylist/StudyListRouting.js';
|
||||
import StandaloneRouting from './routes/StandaloneRouting.js';
|
||||
import CallbackPage from './routes/CallbackPage.js';
|
||||
import NotFound from './routes/NotFound.js';
|
||||
import { Bar, Container } from './components/LoadingBar/';
|
||||
import './OHIFStandaloneViewer.css';
|
||||
import './variables.css';
|
||||
import './theme-tide.css';
|
||||
|
||||
// Dynamic Import Routes (CodeSplitting)
|
||||
// const IHEInvokeImageDisplay = asyncComponent(() =>
|
||||
// import('./routes/IHEInvokeImageDisplay.js')
|
||||
// )
|
||||
// const ViewerRouting = asyncComponent(() => import('./routes/ViewerRouting.js'))
|
||||
// const StudyListRouting = asyncComponent(() =>
|
||||
// import('./studylist/StudyListRouting.js')
|
||||
// )
|
||||
// const StandaloneRouting = asyncComponent(() =>
|
||||
// import('./routes/StandaloneRouting.js')
|
||||
// )
|
||||
// const CallbackPage = asyncComponent(() => import('./CallbackPage.js'))
|
||||
//
|
||||
|
||||
const reload = () => window.location.reload();
|
||||
|
||||
class OHIFStandaloneViewer extends Component {
|
||||
state = {
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
user: PropTypes.object,
|
||||
setContext: PropTypes.func,
|
||||
userManager: PropTypes.object,
|
||||
location: 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"
|
||||
render={() => (
|
||||
<SignoutCallbackComponent
|
||||
userManager={userManager}
|
||||
successCallback={() => console.log('Signout successful')}
|
||||
errorCallback={error => {
|
||||
console.warn(error);
|
||||
console.warn('Signout failed');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/callback"
|
||||
render={() => <CallbackPage userManager={userManager} />}
|
||||
/>
|
||||
<Route
|
||||
component={() => {
|
||||
userManager.getUser().then(user => {
|
||||
if (user) {
|
||||
userManager.signinSilent();
|
||||
} else {
|
||||
userManager.signinRedirect();
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this approach for routing is caused by the conflict between
|
||||
* react-transition-group and react-router's <Switch> component.
|
||||
*
|
||||
* See http://reactcommunity.org/react-transition-group/with-react-router/
|
||||
*/
|
||||
const routes = [
|
||||
{
|
||||
path: '/local',
|
||||
Component: ViewerLocalFileData,
|
||||
},
|
||||
{
|
||||
path: '/viewer',
|
||||
Component: StandaloneRouting,
|
||||
},
|
||||
{
|
||||
path: '/viewer/:studyInstanceUids',
|
||||
Component: ViewerRouting,
|
||||
},
|
||||
{
|
||||
path: '/study/:studyInstanceUids/series/:seriesInstanceUids',
|
||||
Component: ViewerRouting,
|
||||
},
|
||||
{
|
||||
path: '/IHEInvokeImageDisplay',
|
||||
Component: IHEInvokeImageDisplay,
|
||||
},
|
||||
];
|
||||
|
||||
const showStudyList =
|
||||
window.config && window.config.showStudyList !== undefined
|
||||
? window.config.showStudyList
|
||||
: true;
|
||||
if (showStudyList) {
|
||||
routes.push({
|
||||
path: '/studylist',
|
||||
Component: StudyListRouting,
|
||||
});
|
||||
routes.push({
|
||||
path: '/',
|
||||
Component: StudyListRouting,
|
||||
});
|
||||
}
|
||||
|
||||
const currentPath = this.props.location.pathname;
|
||||
const noMatchingRoutes = !routes.find(r =>
|
||||
matchPath(currentPath, {
|
||||
path: r.path,
|
||||
exact: true,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NProgress isAnimating={this.state.isLoading}>
|
||||
{({ isFinished, progress, animationDuration }) => (
|
||||
<Container
|
||||
isFinished={isFinished}
|
||||
animationDuration={animationDuration}
|
||||
>
|
||||
<Bar progress={progress} animationDuration={animationDuration} />
|
||||
</Container>
|
||||
)}
|
||||
</NProgress>
|
||||
<Route exact path="/silent-refresh.html" onEnter={reload} />
|
||||
<Route exact path="/logout-redirect.html" onEnter={reload} />
|
||||
{!noMatchingRoutes &&
|
||||
routes.map(({ path, Component }) => (
|
||||
<Route key={path} exact path={path}>
|
||||
{({ match }) => (
|
||||
<CSSTransition
|
||||
in={match !== null}
|
||||
timeout={300}
|
||||
classNames="fade"
|
||||
unmountOnExit
|
||||
onEnter={() => {
|
||||
this.setState({ isLoading: true });
|
||||
}}
|
||||
onEntered={() => {
|
||||
this.setState({ isLoading: false });
|
||||
}}
|
||||
>
|
||||
{match === null ? (
|
||||
<></>
|
||||
) : (
|
||||
<Component match={match} location={this.props.location} />
|
||||
)}
|
||||
</CSSTransition>
|
||||
)}
|
||||
</Route>
|
||||
))}
|
||||
{noMatchingRoutes && <NotFound />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
user: state.oidc.user,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedOHIFStandaloneViewer = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(OHIFStandaloneViewer);
|
||||
|
||||
export default ViewerbaseDragDropContext(
|
||||
withRouter(ConnectedOHIFStandaloneViewer)
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const UserManagerContext = React.createContext();
|
||||
|
||||
export default UserManagerContext;
|
||||
@@ -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,5 @@
|
||||
export default {
|
||||
webWorkerManager: {
|
||||
initialize: jest.fn(),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
// https://jestjs.io/docs/en/webpack#handling-static-assets
|
||||
|
||||
module.exports = 'test-file-stub';
|
||||
@@ -0,0 +1,4 @@
|
||||
const createUserManager = () => jest.fn().mockReturnValue({});
|
||||
const loadUser = () => jest.fn();
|
||||
|
||||
export { createUserManager, loadUser };
|
||||
@@ -0,0 +1,9 @@
|
||||
const _ = require('lodash');
|
||||
const originalConsoleError = console.error;
|
||||
|
||||
// JSDom's CSS Parser has limited support for certain features
|
||||
// This supresses error warnings caused by it
|
||||
console.error = function(msg) {
|
||||
if (_.startsWith(msg, 'Error: Could not parse CSS stylesheet')) return;
|
||||
originalConsoleError(msg);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { redux } from "@ohif/core";
|
||||
import store from "./../../store";
|
||||
const { setViewportActive } = redux.actions;
|
||||
|
||||
const actions = {
|
||||
updateViewportDisplaySet: ({ direction }) => {
|
||||
// TODO
|
||||
// console.warn('updateDisplaySet: ', direction);
|
||||
},
|
||||
updateActiveViewport: ({ viewports, direction }) => {
|
||||
const { viewportSpecificData, activeViewportIndex } = viewports;
|
||||
const maxIndex = Object.keys(viewportSpecificData).length - 1;
|
||||
|
||||
let newIndex = activeViewportIndex + direction;
|
||||
newIndex = newIndex > maxIndex ? 0 : newIndex;
|
||||
newIndex = newIndex < 0 ? maxIndex : newIndex;
|
||||
|
||||
store.dispatch(setViewportActive(newIndex));
|
||||
}
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
// Next/Previous active viewport
|
||||
incrementActiveViewport: {
|
||||
commandFn: actions.updateActiveViewport,
|
||||
storeContexts: ["viewports"],
|
||||
options: { direction: 1 }
|
||||
},
|
||||
decrementActiveViewport: {
|
||||
commandFn: actions.updateActiveViewport,
|
||||
storeContexts: ["viewports"],
|
||||
options: { direction: -1 }
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: "VIEWER"
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import commandsModule from './commandsModule.js';
|
||||
|
||||
export default {
|
||||
id: 'generic-viewer-commands',
|
||||
getCommandsModule() {
|
||||
return commandsModule;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,356 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { MeasurementTable } from '@ohif/ui';
|
||||
import OHIF from '@ohif/core';
|
||||
import moment from 'moment';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
//
|
||||
import jumpToRowItem from './jumpToRowItem.js';
|
||||
import getMeasurementLocationCallback from './../../lib/getMeasurementLocationCallback';
|
||||
|
||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||
const { MeasurementApi } = OHIF.measurements;
|
||||
|
||||
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];
|
||||
const measurementData = measurementNumberList[0];
|
||||
const {
|
||||
measurementNumber,
|
||||
lesionNamingNumber,
|
||||
toolType,
|
||||
} = measurementData;
|
||||
const measurementId = measurementData._id;
|
||||
|
||||
//check if all measurements with same measurementNumber will have same LABEL
|
||||
const tableMeasurement = {
|
||||
itemNumber: lesionNamingNumber,
|
||||
label: getMeasurementText(measurementData),
|
||||
measurementId,
|
||||
measurementNumber,
|
||||
lesionNamingNumber,
|
||||
toolType,
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Sort measurements by lesion naming number
|
||||
tableMeasurements.forEach(tm => {
|
||||
tm.measurements.sort((m1, m2) =>
|
||||
m1.lesionNamingNumber > m2.lesionNamingNumber ? 1 : -1
|
||||
);
|
||||
});
|
||||
|
||||
return tableMeasurements;
|
||||
}
|
||||
|
||||
function convertTimepointsToTableData(timepoints) {
|
||||
if (!timepoints || !timepoints.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Study date:',
|
||||
key: 'StudyDate',
|
||||
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
|
||||
),
|
||||
timepointManager: state.timepointManager,
|
||||
viewports: state.viewports,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
dispatchRelabel: (event, measurementData, viewportsState) => {
|
||||
const activeViewportIndex =
|
||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
||||
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
|
||||
OHIF.log.error('Failed to find the enabled element');
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = enabledElements[activeViewportIndex];
|
||||
|
||||
const eventData = {
|
||||
event: {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
},
|
||||
element,
|
||||
};
|
||||
|
||||
const { toolType, measurementId } = measurementData;
|
||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
||||
return measurement._id === measurementId;
|
||||
});
|
||||
|
||||
const options = {
|
||||
skipAddLabelButton: true,
|
||||
editLocation: true,
|
||||
};
|
||||
|
||||
// Clone the tool not to set empty location initially
|
||||
const toolForLocation = Object.assign({}, tool, { location: null });
|
||||
getMeasurementLocationCallback(eventData, toolForLocation, options);
|
||||
},
|
||||
dispatchEditDescription: (event, measurementData, viewportsState) => {
|
||||
const activeViewportIndex =
|
||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
||||
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
|
||||
OHIF.log.error('Failed to find the enabled element');
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = enabledElements[activeViewportIndex];
|
||||
|
||||
const eventData = {
|
||||
event: {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
},
|
||||
element,
|
||||
};
|
||||
|
||||
const { toolType, measurementId } = measurementData;
|
||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
||||
return measurement._id === measurementId;
|
||||
});
|
||||
|
||||
const options = {
|
||||
editDescriptionOnDialog: true,
|
||||
};
|
||||
|
||||
getMeasurementLocationCallback(eventData, tool, options);
|
||||
},
|
||||
dispatchJumpToRowItem: (
|
||||
measurementData,
|
||||
viewportsState,
|
||||
timepointManagerState,
|
||||
options
|
||||
) => {
|
||||
const actionData = jumpToRowItem(
|
||||
measurementData,
|
||||
viewportsState,
|
||||
timepointManagerState,
|
||||
dispatch,
|
||||
options
|
||||
);
|
||||
|
||||
actionData.viewportSpecificData.forEach(viewportSpecificData => {
|
||||
const { viewportIndex, displaySet } = viewportSpecificData;
|
||||
|
||||
dispatch(setViewportSpecificData(viewportIndex, displaySet));
|
||||
});
|
||||
|
||||
const { toolType, measurementNumber } = measurementData;
|
||||
const measurementApi = MeasurementApi.Instance;
|
||||
|
||||
Object.keys(measurementApi.tools).forEach(toolType => {
|
||||
const measurements = measurementApi.tools[toolType];
|
||||
|
||||
measurements.forEach(measurement => {
|
||||
measurement.active = false;
|
||||
});
|
||||
});
|
||||
|
||||
const measurementsToActive = measurementApi.tools[toolType].filter(
|
||||
measurement => {
|
||||
return measurement.measurementNumber === measurementNumber;
|
||||
}
|
||||
);
|
||||
|
||||
measurementsToActive.forEach(measurementToActive => {
|
||||
measurementToActive.active = true;
|
||||
});
|
||||
|
||||
measurementApi.syncMeasurementsAndToolData();
|
||||
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
|
||||
// Needs to update viewports.layout state to set layout
|
||||
//const layout = actionData.layout;
|
||||
//dispatch(setLayout(layout))
|
||||
|
||||
// Needs to update viewports.activeViewportIndex to the first updated viewport
|
||||
//const viewportIndex = actionData.viewportIndex;
|
||||
//dispatch(setViewportActive(viewportIndex));
|
||||
|
||||
// Needs to update timepointsManager.measurements state to set active measurementId
|
||||
// TODO: Not yet implemented
|
||||
//dispatch(setActiveMeasurement(measurementData.measurementId))
|
||||
|
||||
// (later): Needs to set some property on state.extensions.cornerstone to synchronize viewport scrolling
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||
return {
|
||||
timepoints: propsFromState.timepoints,
|
||||
measurementCollection: propsFromState.measurementCollection,
|
||||
selectedMeasurementNumber: ownProps.selectedMeasurementNumber,
|
||||
...propsFromDispatch,
|
||||
onItemClick: (event, measurementData) => {
|
||||
// TODO: Add timepointId to .data for measurementData?
|
||||
// TODO: Tooltype should be on the level below? This should
|
||||
// provide the entire row item?
|
||||
|
||||
const viewportsState = propsFromState.viewports;
|
||||
const timepointManagerState = propsFromState.timepointManager;
|
||||
|
||||
// TODO: invertViewportTimepointsOrder should be stored in / read from user preferences
|
||||
// TODO: childToolKey should come from the measurement table when it supports child tools
|
||||
const options = {
|
||||
invertViewportTimepointsOrder: false,
|
||||
childToolKey: null,
|
||||
};
|
||||
|
||||
propsFromDispatch.dispatchJumpToRowItem(
|
||||
measurementData,
|
||||
viewportsState,
|
||||
timepointManagerState,
|
||||
options
|
||||
);
|
||||
},
|
||||
onRelabelClick: (event, measurementData) => {
|
||||
const viewportsState = propsFromState.viewports;
|
||||
propsFromDispatch.dispatchRelabel(event, measurementData, viewportsState);
|
||||
},
|
||||
onEditDescriptionClick: (event, measurementData) => {
|
||||
const viewportsState = propsFromState.viewports;
|
||||
propsFromDispatch.dispatchEditDescription(
|
||||
event,
|
||||
measurementData,
|
||||
viewportsState
|
||||
);
|
||||
},
|
||||
onDeleteClick: (event, measurementData) => {
|
||||
const { MeasurementHandlers } = OHIF.measurements;
|
||||
|
||||
MeasurementHandlers.onRemoved({
|
||||
detail: {
|
||||
toolType: measurementData.toolType,
|
||||
measurementData: {
|
||||
_id: measurementData.measurementId,
|
||||
lesionNamingNumber: measurementData.lesionNamingNumber,
|
||||
measurementNumber: measurementData.measurementNumber,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedMeasurementTable = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(MeasurementTable);
|
||||
|
||||
export default ConnectedMeasurementTable;
|
||||
@@ -0,0 +1,23 @@
|
||||
import ConnectedMeasurementTable from './ConnectedMeasurementTable.js';
|
||||
|
||||
export default {
|
||||
id: 'measurements-table',
|
||||
getPanelModule() {
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'Measurements',
|
||||
target: 'measurement-panel',
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'measurement-panel',
|
||||
component: ConnectedMeasurementTable,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER'],
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,111 @@
|
||||
import { measurements, utils } from "@ohif/core";
|
||||
|
||||
const { MeasurementApi } = measurements;
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
// TODO: Move this function to OHIF itself so we can use it on the OHIF measurment table (when it is finished)
|
||||
|
||||
/**
|
||||
* Activates a set of measurements
|
||||
*
|
||||
* @param measurementData
|
||||
* @param viewportsState
|
||||
* @param timepointManagerState
|
||||
* @param options
|
||||
*/
|
||||
export default function jumpToRowItem(
|
||||
measurementData,
|
||||
viewportsState,
|
||||
timepointManagerState,
|
||||
options = { invertViewportTimepointsOrder: false, childToolKey: null }
|
||||
) {
|
||||
const numViewports = viewportsState.layout.viewports.length;
|
||||
const numTimepoints = timepointManagerState.timepoints.length;
|
||||
const { measurements, timepoints } = timepointManagerState;
|
||||
const numViewportsToUpdate = Math.min(numTimepoints, numViewports);
|
||||
const { toolType, measurementNumber } = measurementData;
|
||||
|
||||
if (options.invertViewportTimepointsOrder) {
|
||||
timepoints.reverse();
|
||||
}
|
||||
|
||||
const measurementsForToolGroup = measurements[toolType];
|
||||
|
||||
// Retrieve the measurements data
|
||||
const measurementsToJumpTo = [];
|
||||
for (let i = 0; i < numViewportsToUpdate; i++) {
|
||||
const { timepointId } = timepoints[i];
|
||||
|
||||
const dataAtThisTimepoint = measurementsForToolGroup.find(entry => {
|
||||
return (
|
||||
entry.timepointId === timepointId &&
|
||||
entry.measurementNumber === measurementNumber
|
||||
);
|
||||
});
|
||||
|
||||
if (!dataAtThisTimepoint) {
|
||||
measurementsToJumpTo.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
let measurement = dataAtThisTimepoint;
|
||||
|
||||
const { tool } = MeasurementApi.getToolConfiguration(toolType);
|
||||
if (options.childToolKey) {
|
||||
measurement = dataAtThisTimepoint[options.childToolKey];
|
||||
} else if (Array.isArray(tool.childTools)) {
|
||||
const key = tool.childTools.find(key => !!dataAtThisTimepoint[key]);
|
||||
measurement = dataAtThisTimepoint[key];
|
||||
}
|
||||
|
||||
measurementsToJumpTo.push(measurement);
|
||||
}
|
||||
|
||||
// TODO: Add a single viewports state action which allows
|
||||
// - viewportData to be set
|
||||
// - layout to be set
|
||||
// - activeViewport to be set
|
||||
|
||||
// Needs to update viewports.viewportData state to set image set data
|
||||
|
||||
const displaySetContainsSopInstance = (displaySet, sopInstanceUid) =>
|
||||
displaySet.images.find(
|
||||
image => image.getSOPInstanceUID() === sopInstanceUid
|
||||
);
|
||||
|
||||
const viewportSpecificData = [];
|
||||
measurementsToJumpTo.forEach((data, viewportIndex) => {
|
||||
// Skip if there is no measurement to jump
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const study = studyMetadataManager.get(data.studyInstanceUid);
|
||||
if (!study) {
|
||||
throw new Error("Study not found.");
|
||||
}
|
||||
|
||||
const displaySet = study.findDisplaySet(displaySet => {
|
||||
return displaySetContainsSopInstance(displaySet, data.sopInstanceUid);
|
||||
});
|
||||
|
||||
if (!displaySet) {
|
||||
throw new Error("Display set not found.");
|
||||
}
|
||||
|
||||
displaySet.sopInstanceUid = data.sopInstanceUid;
|
||||
if (data.frameIndex) {
|
||||
displaySet.frameIndex = data.frameIndex;
|
||||
}
|
||||
|
||||
viewportSpecificData.push({
|
||||
viewportIndex,
|
||||
displaySet
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
viewportSpecificData,
|
||||
layout: [] // TODO: if we need to change layout, we should return this here
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import GenericViewerCommands from './GenericViewerCommands/index.js';
|
||||
import MeasurementsPanel from './MeasurementsPanel/index.js';
|
||||
|
||||
export { GenericViewerCommands, MeasurementsPanel };
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* We use this component to leverage "Code Splitting"
|
||||
*
|
||||
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export default function asyncComponent(importComponent) {
|
||||
class AsyncComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
component: null,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Add dynamically loaded component to state
|
||||
const { default: component } = await importComponent();
|
||||
|
||||
this.setState({
|
||||
component: component,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const C = this.state.component;
|
||||
|
||||
// Render the loaded component, or null
|
||||
return C ? <C {...this.props} /> : null;
|
||||
}
|
||||
}
|
||||
|
||||
return AsyncComponent;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.editDescriptionDialog {
|
||||
position: absolute;
|
||||
z-index: 300;
|
||||
width: 320px;
|
||||
transition: all 300ms linear;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SimpleDialog from '../SimpleDialog/SimpleDialog.js';
|
||||
|
||||
import bounding from '../../lib/utils/bounding.js';
|
||||
import { getDialogStyle } from './../Labelling/labellingPositionUtils.js';
|
||||
|
||||
import './EditDescriptionDialog.css';
|
||||
|
||||
export default class EditDescriptionDialog extends Component {
|
||||
static defaultProps = {
|
||||
componentRef: React.createRef(),
|
||||
componentStyle: {},
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
measurementData: PropTypes.object.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
componentRef: PropTypes.object,
|
||||
componentStyle: PropTypes.object,
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
description: props.measurementData.description || '',
|
||||
};
|
||||
|
||||
this.mainElement = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
bounding(this.mainElement);
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.description !== prevProps.description) {
|
||||
this.setState({
|
||||
description: this.props.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = getDialogStyle(this.props.componentStyle);
|
||||
|
||||
return (
|
||||
<SimpleDialog
|
||||
headerTitle="Edit Description"
|
||||
onClose={this.onClose}
|
||||
onConfirm={this.onConfirm}
|
||||
rootClass="editDescriptionDialog"
|
||||
componentRef={this.mainElement}
|
||||
componentStyle={style}
|
||||
>
|
||||
<input
|
||||
value={this.state.description}
|
||||
className="simpleDialogInput"
|
||||
id="description"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</SimpleDialog>
|
||||
);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
onConfirm = e => {
|
||||
e.preventDefault();
|
||||
this.props.onUpdate(this.state.description);
|
||||
};
|
||||
|
||||
handleChange = event => {
|
||||
this.setState({ description: event.target.value });
|
||||
};
|
||||
}
|
||||
@@ -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,133 @@
|
||||
import './Header.css';
|
||||
import './Header.css';
|
||||
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Dropdown } from '@ohif/ui';
|
||||
import OHIFLogo from '../OHIFLogo/OHIFLogo.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { AboutModal } from '@ohif/ui';
|
||||
import { hotkeysManager } from './../../App.js';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
class Header extends Component {
|
||||
static propTypes = {
|
||||
home: PropTypes.bool.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
children: PropTypes.node,
|
||||
t: PropTypes.func.isRequired,
|
||||
userManager: PropTypes.object
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
home: true,
|
||||
children: OHIFLogo(),
|
||||
};
|
||||
|
||||
// onSave: data => {
|
||||
// const contextName = store.getState().commandContext.context;
|
||||
// const preferences = cloneDeep(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();
|
||||
// },
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isUserPreferencesOpen: false, isOpen: false };
|
||||
|
||||
this.loadOptions();
|
||||
}
|
||||
|
||||
loadOptions() {
|
||||
const { t } = this.props;
|
||||
this.options = [
|
||||
{
|
||||
title: t('About'),
|
||||
icon: { name: 'info' },
|
||||
onClick: () => {
|
||||
this.setState({
|
||||
isOpen: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (this.props.user && this.props.userManager) {
|
||||
this.options.push({
|
||||
title: t('Logout'),
|
||||
icon: { name: 'power-off' },
|
||||
onClick: () => {
|
||||
this.props.userManager.signoutRedirect();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.hotKeysData = hotkeysManager.hotkeyDefinitions;
|
||||
}
|
||||
|
||||
onUserPreferencesSave({ windowLevelData, hotKeysData }) {
|
||||
// console.log(windowLevelData);
|
||||
// console.log(hotKeysData);
|
||||
// TODO: Update hotkeysManager
|
||||
// TODO: reset `this.hotKeysData`
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const showStudyList =
|
||||
window.config.showStudyList !== undefined
|
||||
? window.config.showStudyList
|
||||
: true;
|
||||
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"
|
||||
>
|
||||
{t('Back to Viewer')}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{this.props.children}
|
||||
|
||||
{showStudyList && !this.props.home && (
|
||||
<Link
|
||||
className="header-btn header-studyListLinkSection"
|
||||
to={{
|
||||
pathname: '/',
|
||||
state: { studyLink: this.props.location.pathname },
|
||||
}}
|
||||
>
|
||||
{t('Study list')}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="header-menu">
|
||||
<span className="research-use">{t('INVESTIGATIONAL USE ONLY')}</span>
|
||||
<Dropdown title={t('Options')} list={this.options} align="right" />
|
||||
<AboutModal
|
||||
{...this.state}
|
||||
onCancel={() =>
|
||||
this.setState({
|
||||
isOpen: false,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Header')(withRouter(Header));
|
||||
@@ -0,0 +1,3 @@
|
||||
import Header from './Header';
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,317 @@
|
||||
import { Icon, SelectTree } from '@ohif/ui';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import LabellingTransition from './LabellingTransition.js';
|
||||
import OHIFLabellingData from './OHIFLabellingData.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import bounding from '../../lib/utils/bounding.js';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import { getAddLabelButtonStyle } from './labellingPositionUtils.js';
|
||||
|
||||
export default class LabellingFlow extends Component {
|
||||
static propTypes = {
|
||||
eventData: PropTypes.object.isRequired,
|
||||
measurementData: PropTypes.object.isRequired,
|
||||
labellingDoneCallback: PropTypes.func.isRequired,
|
||||
updateLabelling: PropTypes.func.isRequired,
|
||||
|
||||
initialTopDistance: PropTypes.number,
|
||||
skipAddLabelButton: PropTypes.bool,
|
||||
editLocation: PropTypes.bool,
|
||||
editDescription: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const { location, locationLabel, description } = props.measurementData;
|
||||
|
||||
let style = props.componentStyle;
|
||||
if (!props.skipAddLabelButton) {
|
||||
style = getAddLabelButtonStyle(props.measurementData, props.eventData);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
location,
|
||||
locationLabel,
|
||||
description,
|
||||
skipAddLabelButton: props.skipAddLabelButton,
|
||||
editDescription: props.editDescription,
|
||||
editLocation: props.editLocation,
|
||||
componentStyle: style,
|
||||
confirmationState: false,
|
||||
displayComponent: true,
|
||||
};
|
||||
|
||||
this.mainElement = React.createRef();
|
||||
this.descriptionInput = React.createRef();
|
||||
|
||||
this.initialItems = OHIFLabellingData;
|
||||
this.currentItems = cloneDeep(this.initialItems);
|
||||
}
|
||||
|
||||
componentDidUpdate = () => {
|
||||
this.repositionComponent();
|
||||
if (this.state.editDescription) {
|
||||
this.descriptionInput.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
let mainElementClassName = 'labellingComponent';
|
||||
if (this.state.editDescription) {
|
||||
mainElementClassName += ' editDescription';
|
||||
}
|
||||
|
||||
const style = Object.assign({}, this.state.componentStyle);
|
||||
if (this.state.skipAddLabelButton) {
|
||||
if (style.left - 160 < 0) {
|
||||
style.left = 0;
|
||||
} else {
|
||||
style.left -= 160;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.editLocation) {
|
||||
style.maxHeight = '70vh';
|
||||
if (!this.initialTopDistance) {
|
||||
this.initialTopDistance = window.innerHeight - window.innerHeight * 0.3;
|
||||
style.top = `${this.state.componentStyle.top -
|
||||
this.initialTopDistance / 2}px`;
|
||||
} else {
|
||||
style.top = `${this.state.componentStyle.top}px`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<LabellingTransition
|
||||
displayComponent={this.state.displayComponent}
|
||||
onTransitionExit={this.props.labellingDoneCallback}
|
||||
>
|
||||
<>
|
||||
<div className="labellingComponent-overlay"></div>
|
||||
<div
|
||||
className={mainElementClassName}
|
||||
style={style}
|
||||
ref={this.mainElement}
|
||||
onMouseLeave={this.fadeOutAndLeave}
|
||||
onMouseEnter={this.clearFadeOutTimer}
|
||||
>
|
||||
{this.labellingStateFragment()}
|
||||
</div>
|
||||
</>
|
||||
</LabellingTransition>
|
||||
);
|
||||
}
|
||||
|
||||
labellingStateFragment = () => {
|
||||
const {
|
||||
skipAddLabelButton,
|
||||
editLocation,
|
||||
description,
|
||||
locationLabel,
|
||||
} = this.state;
|
||||
|
||||
if (!skipAddLabelButton) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="addLabelButton"
|
||||
onClick={this.showLabelling}
|
||||
>
|
||||
{this.state.location ? 'Edit' : 'Add'} Label
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
if (editLocation) {
|
||||
return (
|
||||
<SelectTree
|
||||
items={this.currentItems}
|
||||
columns={1}
|
||||
onSelected={this.selectTreeSelectCalback}
|
||||
selectTreeFirstTitle="Assign Label"
|
||||
onComponentChange={this.repositionComponent}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="checkIconWrapper"
|
||||
onClick={this.fadeOutAndLeaveFast}
|
||||
>
|
||||
<Icon name="check" className="checkIcon" />
|
||||
</div>
|
||||
<div className="locationDescriptionWrapper">
|
||||
<div className="location">{locationLabel}</div>
|
||||
<div className="description">
|
||||
<input
|
||||
id="descriptionInput"
|
||||
ref={this.descriptionInput}
|
||||
defaultValue={description || ''}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="commonButtons">
|
||||
<button
|
||||
type="button"
|
||||
className="commonButton left"
|
||||
onClick={this.relabel}
|
||||
>
|
||||
Relabel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="commonButton right"
|
||||
onClick={this.setDescriptionUpdateMode}
|
||||
>
|
||||
{description ? 'Edit ' : 'Add '}
|
||||
Description
|
||||
</button>
|
||||
</div>
|
||||
<div className="editDescriptionButtons">
|
||||
<button
|
||||
type="button"
|
||||
className="commonButton left"
|
||||
onClick={this.descriptionCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="commonButton right"
|
||||
onClick={this.descriptionSave}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
relabel = event => {
|
||||
const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop;
|
||||
const componentStyle = {
|
||||
top: event.nativeEvent.y - viewportTopPosition - 55,
|
||||
left: event.nativeEvent.x,
|
||||
};
|
||||
this.setState({
|
||||
editLocation: true,
|
||||
componentStyle,
|
||||
});
|
||||
};
|
||||
|
||||
setDescriptionUpdateMode = () => {
|
||||
this.descriptionInput.current.focus();
|
||||
|
||||
this.setState({
|
||||
editDescription: true,
|
||||
});
|
||||
};
|
||||
|
||||
descriptionCancel = () => {
|
||||
const { description = '' } = cloneDeep(this.state);
|
||||
this.descriptionInput.current.value = description;
|
||||
|
||||
this.setState({
|
||||
editDescription: false,
|
||||
});
|
||||
};
|
||||
|
||||
descriptionSave = () => {
|
||||
const description = this.descriptionInput.current.value;
|
||||
this.props.updateLabelling({ description });
|
||||
|
||||
this.setState({
|
||||
description,
|
||||
editDescription: false,
|
||||
});
|
||||
};
|
||||
|
||||
selectTreeSelectCalback = (event, itemSelected) => {
|
||||
const location = itemSelected.value;
|
||||
this.props.updateLabelling({ location });
|
||||
|
||||
const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop;
|
||||
const componentStyle = {
|
||||
top: event.nativeEvent.y - viewportTopPosition - 25,
|
||||
left: event.nativeEvent.x,
|
||||
};
|
||||
|
||||
this.setState({
|
||||
editLocation: false,
|
||||
confirmationState: true,
|
||||
location: itemSelected.value,
|
||||
locationLabel: itemSelected.label,
|
||||
componentStyle,
|
||||
});
|
||||
|
||||
if (this.isTouchScreen) {
|
||||
this.setTimeout = setTimeout(() => {
|
||||
this.setState({
|
||||
displayComponent: false,
|
||||
});
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
showLabelling = () => {
|
||||
this.setState({
|
||||
skipAddLabelButton: true,
|
||||
editLocation: false,
|
||||
});
|
||||
};
|
||||
|
||||
fadeOutAndLeave = () => {
|
||||
// Wait for 1 sec to dismiss the labelling component
|
||||
this.fadeOutTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
displayComponent: false,
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
fadeOutAndLeaveFast = () => {
|
||||
this.setState({
|
||||
displayComponent: false,
|
||||
});
|
||||
};
|
||||
|
||||
clearFadeOutTimer = () => {
|
||||
if (!this.fadeOutTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.fadeOutTimer);
|
||||
};
|
||||
|
||||
calculateTopDistance = () => {
|
||||
const height = window.innerHeight - window.innerHeight * 0.3;
|
||||
let top = this.state.componentStyle.top - height / 2 + 55;
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
} else {
|
||||
if (top + height > window.innerHeight) {
|
||||
top -= top + height - window.innerHeight;
|
||||
}
|
||||
}
|
||||
return top;
|
||||
};
|
||||
|
||||
repositionComponent = () => {
|
||||
// SetTimeout for the css animation to end.
|
||||
setTimeout(() => {
|
||||
bounding(this.mainElement);
|
||||
if (this.state.editLocation) {
|
||||
this.mainElement.current.style.maxHeight = '70vh';
|
||||
const top = this.calculateTopDistance();
|
||||
this.mainElement.current.style.top = `${top}px`;
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
.labellingComponent-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.labellingComponent {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
z-index: 999;
|
||||
transition: all 200ms linear;
|
||||
}
|
||||
|
||||
.labellingComponent .selectedLabel,
|
||||
.labellingComponent .selectedDescription {
|
||||
padding: 5px;
|
||||
background-color: white;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.labellingComponent .addLabelButton {
|
||||
color: #000000;
|
||||
background-color: #20a5d6;
|
||||
border: 2px solid #44626f;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
opacity: 1;
|
||||
padding: 0 14px;
|
||||
transition: opacity 0.3s ease;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.labellingComponent .commonButton {
|
||||
border: 1px solid #44626f;
|
||||
color: #ffffff;
|
||||
background-color: #000000;
|
||||
border-radius: 16px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
line-height: 26px;
|
||||
padding: 0 12px;
|
||||
margin: 10px 5px 0 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.labellingComponent .locationDescriptionWrapper {
|
||||
background-color: #ffffff;
|
||||
line-height: 46px;
|
||||
height: 46px;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
width: 290px;
|
||||
min-width: 260px;
|
||||
padding: 0 12px;
|
||||
margin: 0 auto;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.labellingComponent .locationDescriptionWrapper .location {
|
||||
transition: all 300ms linear;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.labellingComponent.editDescription .locationDescriptionWrapper .location {
|
||||
bottom: 38px;
|
||||
}
|
||||
|
||||
.labellingComponent .locationDescriptionWrapper #descriptionInput {
|
||||
transition-delay: all 300ms linear;
|
||||
visibility: hidden;
|
||||
outline: none;
|
||||
height: 46px;
|
||||
width: 100%;
|
||||
line-height: 20px;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.labellingComponent.editDescription
|
||||
.locationDescriptionWrapper
|
||||
#descriptionInput {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.labellingComponent.editDescription .location {
|
||||
color: #337ab7;
|
||||
}
|
||||
|
||||
.labellingComponent .commonButtons,
|
||||
.labellingComponent.editDescription .editDescriptionButtons {
|
||||
display: block;
|
||||
margin-left: 55px;
|
||||
}
|
||||
|
||||
.labellingComponent.editDescription .commonButtons,
|
||||
.labellingComponent .editDescriptionButtons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.labellingComponent .commonButtons {
|
||||
text-align: center;
|
||||
margin-left: 55px;
|
||||
}
|
||||
|
||||
.labellingComponent .commonButton.left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.labellingComponent .commonButton.right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.labellingComponent .checkIconWrapper {
|
||||
display: inline-block;
|
||||
background-color: #337ab7;
|
||||
border-radius: 46px;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
margin-right: 10px;
|
||||
vertical-align: bottom;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.labellingComponent .checkIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: 13px;
|
||||
fill: black;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js';
|
||||
import LabellingFlow from './LabellingFlow.js';
|
||||
|
||||
import './LabellingManager.css';
|
||||
|
||||
export default class LabellingManager extends Component {
|
||||
static propTypes = {
|
||||
eventData: PropTypes.object.isRequired,
|
||||
measurementData: PropTypes.object.isRequired,
|
||||
|
||||
labellingDoneCallback: PropTypes.func.isRequired,
|
||||
updateLabelling: PropTypes.func.isRequired,
|
||||
|
||||
skipAddLabelButton: PropTypes.bool,
|
||||
editLocation: PropTypes.bool,
|
||||
editDescription: PropTypes.bool,
|
||||
editDescriptionOnDialog: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
skipAddLabelButton: false,
|
||||
editLocation: false,
|
||||
editDescription: false,
|
||||
editDescriptionOnDialog: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const measurementData = cloneDeep(props.measurementData);
|
||||
this.treatMeasurementData(measurementData);
|
||||
|
||||
let editLocation = props.editLocation;
|
||||
if (!props.editDescription && !props.editLocation) {
|
||||
editLocation = true;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
componentStyle: getComponentPosition(props.eventData),
|
||||
skipAddLabelButton: props.skipAddLabelButton,
|
||||
editLocation: editLocation,
|
||||
editDescription: props.editDescription,
|
||||
editDescriptionOnDialog: props.editDescriptionOnDialog,
|
||||
measurementData: measurementData,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
document.addEventListener('touchstart', this.onTouchStart);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
document.removeEventListener('touchstart', this.onTouchStart);
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.getRenderComponent();
|
||||
}
|
||||
|
||||
getRenderComponent = () => {
|
||||
const {
|
||||
editLocation,
|
||||
editDescription,
|
||||
editDescriptionOnDialog,
|
||||
measurementData,
|
||||
} = this.state;
|
||||
|
||||
if (editDescriptionOnDialog) {
|
||||
return (
|
||||
<EditDescriptionDialog
|
||||
onCancel={this.props.labellingDoneCallback}
|
||||
onUpdate={this.descriptionDialogUpdate}
|
||||
componentRef={this.editDescriptionDialog}
|
||||
componentStyle={this.state.componentStyle}
|
||||
measurementData={measurementData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (editLocation || editDescription) {
|
||||
return (
|
||||
<LabellingFlow
|
||||
{...this.props}
|
||||
componentStyle={this.state.componentStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
treatMeasurementData = measurementData => {
|
||||
const { editDescription, editLocation } = this.props;
|
||||
|
||||
if (editDescription) {
|
||||
measurementData.description = undefined;
|
||||
}
|
||||
if (editLocation) {
|
||||
measurementData.location = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
responseDialogUpdate = response => {
|
||||
this.props.updateLabelling({
|
||||
response,
|
||||
});
|
||||
this.props.labellingDoneCallback();
|
||||
};
|
||||
|
||||
descriptionDialogUpdate = description => {
|
||||
this.props.updateLabelling({
|
||||
description,
|
||||
});
|
||||
this.props.labellingDoneCallback();
|
||||
};
|
||||
}
|
||||
|
||||
function getComponentPosition(eventData) {
|
||||
const {
|
||||
event: { clientX: left, clientY: top },
|
||||
} = eventData;
|
||||
|
||||
return {
|
||||
left,
|
||||
top,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.labelling-appear {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.labelling-appear.labelling-appear-active {
|
||||
opacity: 1;
|
||||
transition: opacity 500ms linear;
|
||||
}
|
||||
|
||||
.labelling-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.labelling-exit.labelling-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 500ms linear;
|
||||
}
|
||||
|
||||
.labelling-exit-done {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
import './LabellingTransition.css';
|
||||
|
||||
// If these variables changes, CSS must be updated
|
||||
const transitionDuration = 500;
|
||||
const transitionClassName = 'labelling';
|
||||
const transitionOnAppear = true;
|
||||
|
||||
export default class LabellingTransition extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
displayComponent: PropTypes.bool.isRequired,
|
||||
onTransitionExit: PropTypes.func.isRequired,
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<CSSTransition
|
||||
in={this.props.displayComponent}
|
||||
appear={transitionOnAppear}
|
||||
timeout={transitionDuration}
|
||||
classNames={transitionClassName}
|
||||
onExited={this.props.onTransitionExit}
|
||||
>
|
||||
{this.props.children}
|
||||
</CSSTransition>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
const items = [
|
||||
'Abdomen/Chest Wall',
|
||||
'Adrenal',
|
||||
'Bladder',
|
||||
'Bone',
|
||||
'Brain',
|
||||
'Breast',
|
||||
'Colon',
|
||||
'Esophagus',
|
||||
'Extremities',
|
||||
'Gallbladder',
|
||||
'Kidney',
|
||||
'Liver',
|
||||
'Lung',
|
||||
'Lymph Node',
|
||||
'Mediastinum/Hilum',
|
||||
'Muscle',
|
||||
'Neck',
|
||||
'Other Soft Tissue',
|
||||
'Ovary',
|
||||
'Pancreas',
|
||||
'Pelvis',
|
||||
'Peritoneum/Omentum',
|
||||
'Prostate',
|
||||
'Retroperitoneum',
|
||||
'Small Bowel',
|
||||
'Spleen',
|
||||
'Stomach',
|
||||
'Subcutaneous',
|
||||
];
|
||||
|
||||
const OHIFLabellingData = items.map(item => {
|
||||
return {
|
||||
label: item,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
|
||||
export default OHIFLabellingData;
|
||||
@@ -0,0 +1,53 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
const buttonSize = {
|
||||
width: 96,
|
||||
height: 28,
|
||||
};
|
||||
|
||||
export function getAddLabelButtonStyle(measurementData, eventData) {
|
||||
const { start, end } = measurementData.handles;
|
||||
const { client } = eventData.currentPoints;
|
||||
const clientStart = cornerstone.pixelToCanvas(eventData.element, start);
|
||||
const clientEnd = cornerstone.pixelToCanvas(eventData.element, end);
|
||||
const canvasOffSetLeft = client.x - clientStart.x;
|
||||
const canvasOffSetTop = client.y - clientStart.y;
|
||||
const position = {
|
||||
left: clientEnd.x + canvasOffSetLeft,
|
||||
top: clientEnd.y + canvasOffSetTop,
|
||||
};
|
||||
|
||||
if (start.y > end.y) {
|
||||
position.top -= buttonSize.height;
|
||||
}
|
||||
if (start.x > end.x) {
|
||||
position.left -= buttonSize.width;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
export function getDialogStyle(componentStyle) {
|
||||
const style = Object.assign({}, componentStyle);
|
||||
const dialogProps = {
|
||||
width: 320,
|
||||
height: 230,
|
||||
};
|
||||
|
||||
// Get max values to avoid position out of the screen
|
||||
const maxLeft = window.innerWidth - dialogProps.width;
|
||||
const maxTop = window.innerHeight - dialogProps.height;
|
||||
|
||||
// Positioning the dialog with its center on the click event
|
||||
style.left -= dialogProps.width / 2;
|
||||
style.top -= dialogProps.height / 2;
|
||||
|
||||
if (style.left > maxLeft) {
|
||||
style.left = maxLeft;
|
||||
}
|
||||
if (style.top > maxTop) {
|
||||
style.top = maxTop;
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
const Bar = ({ progress, animationDuration }) => (
|
||||
<div
|
||||
style={{
|
||||
background: '#29d',
|
||||
height: 2,
|
||||
left: 0,
|
||||
marginLeft: `${(-1 + progress) * 100}%`,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
transition: `margin-left ${animationDuration}ms linear`,
|
||||
width: '100%',
|
||||
zIndex: 1031,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
boxShadow: '0 0 10px #29d, 0 0 5px #29d',
|
||||
display: 'block',
|
||||
height: '100%',
|
||||
opacity: 1,
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
transform: 'rotate(3deg) translate(0px, -4px)',
|
||||
width: 100,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Bar;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
const Container = ({ children, isFinished, animationDuration }) => (
|
||||
<div
|
||||
style={{
|
||||
opacity: isFinished ? 0 : 1,
|
||||
pointerEvents: 'none',
|
||||
transition: `opacity ${animationDuration}ms linear`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Container;
|
||||
@@ -0,0 +1,4 @@
|
||||
import Bar from './Bar.js';
|
||||
import Container from './Container.js';
|
||||
|
||||
export { Bar, Container };
|
||||
@@ -0,0 +1,29 @@
|
||||
.header-brand {
|
||||
height: 30px;
|
||||
text-decoration: none;
|
||||
text-decoration-color: none;
|
||||
|
||||
display: flex;
|
||||
color: var(--text-primary-color);
|
||||
font-size: 14px;
|
||||
font-weight: var(--logo-font-weight);
|
||||
font-family: var(--logo-font-family);
|
||||
}
|
||||
|
||||
.header-brand:hover,
|
||||
.header-brand:active,
|
||||
.header-band:visited {
|
||||
color: var(--text-primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.header-logo-text {
|
||||
line-height: 30px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.header-logo-image {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
font-size: 30px;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import './OHIFLogo.css';
|
||||
|
||||
import { Icon } from '@ohif/ui';
|
||||
import React from 'react';
|
||||
|
||||
function OHIFLogo() {
|
||||
return (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="header-brand"
|
||||
href="http://ohif.org"
|
||||
>
|
||||
<Icon name="ohif-logo" className="header-logo-image" />
|
||||
<div className="header-logo-text">Open Health Imaging Foundation</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default OHIFLogo;
|
||||
@@ -0,0 +1,40 @@
|
||||
.FlexboxLayout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row nowrap;
|
||||
align-items: stretch;
|
||||
height: calc(100% - var(--toolbar-height) - var(--top-bar-height));
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidepanel {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
transition: var(--sidepanel-transition);
|
||||
}
|
||||
|
||||
.from-left {
|
||||
border-right: var(--ui-border-thickness) solid var(--ui-border-color);
|
||||
margin-left: calc(var(--left-sidepanel-menu-width) * -1);
|
||||
max-width: var(--left-sidepanel-menu-width);
|
||||
}
|
||||
|
||||
.from-right {
|
||||
border-left: var(--ui-border-thickness) solid var(--ui-border-color);
|
||||
margin-right: calc(var(--right-sidepanel-menu-width) * -1);
|
||||
max-width: var(--right-sidepanel-menu-width);
|
||||
}
|
||||
|
||||
.sidepanel.is-open {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: var(--sidepanel-transition);
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import './SidePanel.css';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
class SidePanel extends Component {
|
||||
static propTypes = {
|
||||
from: PropTypes.string.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node,
|
||||
width: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const fromSideClass =
|
||||
this.props.from === 'right' ? 'from-right' : 'from-left';
|
||||
|
||||
const styles = this.props.width
|
||||
? {
|
||||
maxWidth: this.props.width,
|
||||
marginRight: this.props.isOpen
|
||||
? '0'
|
||||
: Number.parseInt(this.props.width) * -1,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<section
|
||||
style={styles}
|
||||
className={classNames('sidepanel', fromSideClass, {
|
||||
'is-open': this.props.isOpen,
|
||||
})}
|
||||
>
|
||||
{this.props.children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SidePanel;
|
||||
@@ -0,0 +1,133 @@
|
||||
.simpleDialog {
|
||||
position: fixed;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background-color: #151a1f;
|
||||
}
|
||||
.simpleDialog .header {
|
||||
border-bottom-width: 3px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: #000;
|
||||
padding: 19px 22px 17px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.simpleDialog .header .title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
padding-right: 40px;
|
||||
color: #91b9cd;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.simpleDialog .header .closeBtn {
|
||||
height: 20px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
width: 20px;
|
||||
color: #91b9cd;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 21px;
|
||||
top: 50%;
|
||||
-webkit-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.simpleDialog .header .closeIcon {
|
||||
color: transparent;
|
||||
display: block;
|
||||
font-size: 0;
|
||||
height: 100%;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.simpleDialog .header .closeIcon:after,
|
||||
.simpleDialog .header .closeIcon:before {
|
||||
content: ' ';
|
||||
display: block;
|
||||
height: 2px;
|
||||
transition: background-color 0.3s ease;
|
||||
width: 19px;
|
||||
background-color: #91b9cd;
|
||||
}
|
||||
|
||||
.simpleDialog .header .closeIcon:before {
|
||||
left: 1px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
-webkit-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform-origin: 1px 50%;
|
||||
transform-origin: 1px 50%;
|
||||
}
|
||||
|
||||
.simpleDialog .header .closeIcon:after {
|
||||
right: 1px;
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
-webkit-transform-origin: calc(100% - 1px) 50%;
|
||||
transform-origin: calc(100% - 1px) 50%;
|
||||
}
|
||||
|
||||
.simpleDialog .content {
|
||||
padding: 16px 22px 25px;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.simpleDialog .content .simpleDialogSelect,
|
||||
.simpleDialog .content .simpleDialogInput {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
line-height: 16px;
|
||||
padding: 8px 9px 6px;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.simpleDialog .content .simpleDialogInputLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.simpleDialog .footer {
|
||||
padding: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.simpleDialog .footer .btn {
|
||||
transition: background-color 0.3s ease;
|
||||
color: #000;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
height: 37px;
|
||||
line-height: 37px;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 0;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.simpleDialog .footer .btn-confirm {
|
||||
color: #fff;
|
||||
background-color: #337ab7;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './SimpleDialog.css';
|
||||
|
||||
class SimpleDialog extends Component {
|
||||
static defaultProps = {
|
||||
componentStyle: {},
|
||||
rootClass: '',
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={`simpleDialog ${this.props.rootClass}`}
|
||||
ref={this.props.componentRef}
|
||||
style={this.props.componentStyle}
|
||||
>
|
||||
<form onSubmit={this.props.onConfirm}>
|
||||
<div className="header">
|
||||
<span className="closeBtn" onClick={this.props.onClose}>
|
||||
<span className="closeIcon">x</span>
|
||||
</span>
|
||||
<h4 className="title">{this.props.headerTitle}</h4>
|
||||
</div>
|
||||
<div className="content">{this.props.children}</div>
|
||||
<div className="footer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-cancel"
|
||||
onClick={this.props.onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-confirm"
|
||||
onClick={this.props.onConfirm}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SimpleDialog.propTypes = {
|
||||
headerTitle: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SimpleDialog;
|
||||
@@ -0,0 +1,42 @@
|
||||
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,29 @@
|
||||
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,45 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import version from './version.js';
|
||||
|
||||
let homepage;
|
||||
const { process } = window;
|
||||
if (process && process.env && process.env.PUBLIC_URL) {
|
||||
homepage = process.env.PUBLIC_URL;
|
||||
}
|
||||
|
||||
window.info = {
|
||||
version,
|
||||
homepage,
|
||||
};
|
||||
|
||||
// For debugging
|
||||
//if (process.env.node_env === 'development') {
|
||||
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,102 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { CineDialog } from '@ohif/ui';
|
||||
import OHIF from '@ohif/core';
|
||||
import csTools from 'cornerstone-tools';
|
||||
// Our target output kills the `as` and "import" throws a keyword error
|
||||
// import { import as toolImport, getToolState } from 'cornerstone-tools';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
const toolImport = csTools.import;
|
||||
const scrollToIndex = toolImport('util/scrollToIndex');
|
||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||
|
||||
// Why do I need or care about any of this info?
|
||||
// A dispatch action should be able to pull this at the time of an event?
|
||||
// `isPlaying` and `cineFrameRate` might matter, but I think we can prop pass for those.
|
||||
const mapStateToProps = state => {
|
||||
// Get activeViewport's `cine` and `stack`
|
||||
const { viewportSpecificData, activeViewportIndex } = state.viewports;
|
||||
const { cine, dom } = viewportSpecificData[activeViewportIndex] || {};
|
||||
|
||||
const cineData = cine || {
|
||||
isPlaying: false,
|
||||
cineFrameRate: 24,
|
||||
};
|
||||
|
||||
// New props we're creating?
|
||||
return {
|
||||
activeEnabledElement: dom,
|
||||
activeViewportCineData: cineData,
|
||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
dispatchSetViewportSpecificData: (viewportIndex, data) => {
|
||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||
const {
|
||||
activeEnabledElement,
|
||||
activeViewportCineData,
|
||||
activeViewportIndex,
|
||||
} = propsFromState;
|
||||
|
||||
return {
|
||||
cineFrameRate: activeViewportCineData.cineFrameRate,
|
||||
isPlaying: activeViewportCineData.isPlaying,
|
||||
onPlayPauseChanged: isPlaying => {
|
||||
const cine = cloneDeep(activeViewportCineData);
|
||||
cine.isPlaying = !cine.isPlaying;
|
||||
|
||||
propsFromDispatch.dispatchSetViewportSpecificData(activeViewportIndex, {
|
||||
cine,
|
||||
});
|
||||
},
|
||||
onFrameRateChanged: frameRate => {
|
||||
const cine = cloneDeep(activeViewportCineData);
|
||||
cine.cineFrameRate = frameRate;
|
||||
|
||||
propsFromDispatch.dispatchSetViewportSpecificData(activeViewportIndex, {
|
||||
cine,
|
||||
});
|
||||
},
|
||||
onClickNextButton: () => {
|
||||
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
|
||||
if (!stackData || !stackData.data || !stackData.data.length) return;
|
||||
const { currentImageIdIndex, imageIds } = stackData.data[0];
|
||||
if (currentImageIdIndex >= imageIds.length - 1) return;
|
||||
scrollToIndex(activeEnabledElement, currentImageIdIndex + 1);
|
||||
},
|
||||
onClickBackButton: () => {
|
||||
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
|
||||
if (!stackData || !stackData.data || !stackData.data.length) return;
|
||||
const { currentImageIdIndex } = stackData.data[0];
|
||||
if (currentImageIdIndex === 0) return;
|
||||
scrollToIndex(activeEnabledElement, currentImageIdIndex - 1);
|
||||
},
|
||||
onClickSkipToStart: () => {
|
||||
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
|
||||
if (!stackData || !stackData.data || !stackData.data.length) return;
|
||||
scrollToIndex(activeEnabledElement, 0);
|
||||
},
|
||||
onClickSkipToEnd: () => {
|
||||
const stackData = csTools.getToolState(activeEnabledElement, 'stack');
|
||||
if (!stackData || !stackData.data || !stackData.data.length) return;
|
||||
const lastIndex = stackData.data[0].imageIds.length - 1;
|
||||
scrollToIndex(activeEnabledElement, lastIndex);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedCineDialog = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(CineDialog);
|
||||
|
||||
export default ConnectedCineDialog;
|
||||
@@ -0,0 +1,13 @@
|
||||
import Header from '../components/Header/Header.js';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
user: state.oidc && state.oidc.user,
|
||||
isOpen: state.ui.userPreferencesModalOpen,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedHeader = connect(mapStateToProps)(Header);
|
||||
|
||||
export default ConnectedHeader;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import LabellingOverlay from './LabellingOverlay';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
if (!state.ui || !state.ui.labelling) {
|
||||
return {
|
||||
visible: false,
|
||||
};
|
||||
}
|
||||
|
||||
const labellingFlowData = state.ui.labelling;
|
||||
|
||||
return {
|
||||
visible: false,
|
||||
...labellingFlowData,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedLabellingOverlay = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(LabellingOverlay);
|
||||
|
||||
export default ConnectedLabellingOverlay;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { LayoutButton } from '@ohif/ui';
|
||||
import OHIF from '@ohif/core';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
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,36 @@
|
||||
import { LayoutManager } from '@ohif/ui';
|
||||
import { MODULE_TYPES } from '@ohif/core';
|
||||
import { connect } from 'react-redux';
|
||||
import { extensionManager } from './../App.js';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const availableViewportModules = {};
|
||||
const viewportModules = extensionManager.modules[MODULE_TYPES.VIEWPORT];
|
||||
|
||||
viewportModules.forEach(moduleDefinition => {
|
||||
availableViewportModules[moduleDefinition.extensionId] =
|
||||
moduleDefinition.module;
|
||||
});
|
||||
|
||||
// TODO: Use something like state.plugins.defaultPlugin[MODULE_TYPES.VIEWPORT]
|
||||
let defaultPlugin;
|
||||
if (viewportModules.length) {
|
||||
defaultPlugin = viewportModules[0].extensionId;
|
||||
}
|
||||
|
||||
return {
|
||||
layout: state.viewports.layout,
|
||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
||||
// TODO: rename `availableViewportModules`
|
||||
availablePlugins: availableViewportModules,
|
||||
// TODO: rename `defaultViewportModule`
|
||||
defaultPlugin,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedLayoutManager = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(LayoutManager);
|
||||
|
||||
export default ConnectedLayoutManager;
|
||||
@@ -0,0 +1,103 @@
|
||||
import OHIF from "@ohif/core";
|
||||
import PluginSwitch from "./PluginSwitch.js";
|
||||
import { commandsManager } from "./../App.js";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
const { setLayout } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
|
||||
|
||||
return {
|
||||
activeViewportIndex,
|
||||
viewportSpecificData,
|
||||
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 buttons = [
|
||||
/*{
|
||||
text: 'Acquired',
|
||||
type: 'command',
|
||||
icon: 'bars',
|
||||
active: false,
|
||||
onClick: () => {
|
||||
console.warn('Original Acquisition');
|
||||
|
||||
const layoutData = setSingleLayoutData(
|
||||
layout.viewports,
|
||||
activeViewportIndex,
|
||||
{ plugin: 'cornerstone' }
|
||||
);
|
||||
|
||||
setLayout({ viewports: layoutData });
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Axial',
|
||||
icon: 'cube',
|
||||
active: false,
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('axial');
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Sagittal',
|
||||
icon: 'cube',
|
||||
active: false,
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('sagittal');
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Coronal',
|
||||
icon: 'cube',
|
||||
active: false,
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('coronal');
|
||||
},
|
||||
},*/
|
||||
{
|
||||
label: "2D MPR",
|
||||
icon: "cube",
|
||||
onClick: () => {
|
||||
commandsManager.runCommand("mpr2d");
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
buttons
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedPluginSwitch = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(PluginSwitch);
|
||||
|
||||
export default ConnectedPluginSwitch;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { StudyBrowser } from '@ohif/ui';
|
||||
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,26 @@
|
||||
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,34 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ToolContextMenu from './ToolContextMenu';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
if (!state.ui || !state.ui.contextMenu) {
|
||||
return {
|
||||
visible: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { viewportIndex } = ownProps;
|
||||
const toolContextMenuData = state.ui.contextMenu[viewportIndex];
|
||||
let availableTools;
|
||||
|
||||
if (
|
||||
state.extensions &&
|
||||
state.extensions.cornerstone &&
|
||||
state.extensions.cornerstone.availableTools
|
||||
) {
|
||||
availableTools = state.extensions.cornerstone.availableTools;
|
||||
}
|
||||
|
||||
return {
|
||||
...toolContextMenuData,
|
||||
availableTools,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedToolContextMenu = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(ToolContextMenu);
|
||||
|
||||
export default ConnectedToolContextMenu;
|
||||
@@ -0,0 +1,15 @@
|
||||
// TODO: REPLACE THIS WITH A CONTEXT PROVIDER
|
||||
// EVERYTHING IN `VIEWER.JS` COULD USE THIS FOR APPROPRIATE CONTEXT
|
||||
import ToolbarRow from './ToolbarRow';
|
||||
import { connect } from 'react-redux';
|
||||
import { getActiveContexts } from './../store/layout/selectors.js';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
activeContexts: getActiveContexts(state),
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedToolbarRow = connect(mapStateToProps)(ToolbarRow);
|
||||
|
||||
export default ConnectedToolbarRow;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { connect } from "react-redux";
|
||||
import Viewer from "./Viewer.js";
|
||||
import OHIF from "@ohif/core";
|
||||
|
||||
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
viewports: state.viewports.viewportSpecificData,
|
||||
activeViewportIndex: state.viewports.activeViewportIndex
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onTimepointsUpdated: timepoints => {
|
||||
dispatch(setTimepoints(timepoints));
|
||||
},
|
||||
onMeasurementsUpdated: measurements => {
|
||||
dispatch(setMeasurements(measurements));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedViewer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Viewer);
|
||||
|
||||
export default ConnectedViewer;
|
||||
@@ -0,0 +1,37 @@
|
||||
import OHIF from "@ohif/core";
|
||||
import ViewerMain from "./ViewerMain";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
const {
|
||||
setViewportSpecificData,
|
||||
clearViewportSpecificData
|
||||
} = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
|
||||
|
||||
return {
|
||||
activeViewportIndex,
|
||||
layout,
|
||||
viewportSpecificData,
|
||||
viewports: state.viewports
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setViewportSpecificData: (viewportIndex, data) => {
|
||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
||||
},
|
||||
clearViewportSpecificData: () => {
|
||||
dispatch(clearViewportSpecificData());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedViewerMain = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ViewerMain);
|
||||
|
||||
export default ConnectedViewerMain;
|
||||
@@ -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,23 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LabellingManager from '../components/Labelling/LabellingManager';
|
||||
|
||||
class LabellingOverlay extends Component {
|
||||
static propTypes = {
|
||||
visible: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
visible: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <LabellingManager {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default LabellingOverlay;
|
||||
@@ -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 '@ohif/ui';
|
||||
import './PluginSwitch.css';
|
||||
|
||||
class PluginSwitch extends Component {
|
||||
static propTypes = {
|
||||
buttons: PropTypes.array,
|
||||
};
|
||||
|
||||
static defaultProps = {};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="PluginSwitch">
|
||||
<ExpandableToolMenu buttons={this.props.buttons} text={'View'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginSwitch;
|
||||
@@ -0,0 +1,34 @@
|
||||
.ToolContextMenu {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border: 1px solid white;
|
||||
border-radius: 5px;
|
||||
z-index: 1000;
|
||||
display: block;
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.ToolContextMenu > ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ToolContextMenu > ul > li > button {
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
color: #516873;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.ToolContextMenu > ul > li > button:hover {
|
||||
color: #16202b;
|
||||
}
|
||||
|
||||
.ToolContextMenu > ul > li > button:active {
|
||||
color: #79f9fe;
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import getMeasurementLocationCallback from '../lib/getMeasurementLocationCallback';
|
||||
|
||||
import './ToolContextMenu.css';
|
||||
|
||||
const toolTypes = [
|
||||
'Angle',
|
||||
'Bidirectional',
|
||||
'Length',
|
||||
'FreehandMouse',
|
||||
'EllipticalRoi',
|
||||
'CircleRoi',
|
||||
'RectangleRoi',
|
||||
];
|
||||
|
||||
let defaultDropdownItems = [
|
||||
{
|
||||
actionType: 'Delete',
|
||||
action: ({ nearbyToolData, eventData }) => {
|
||||
const element = eventData.element;
|
||||
|
||||
cornerstoneTools.removeToolState(
|
||||
element,
|
||||
nearbyToolData.toolType,
|
||||
nearbyToolData.tool
|
||||
);
|
||||
cornerstone.updateImage(element);
|
||||
},
|
||||
},
|
||||
{
|
||||
actionType: 'setLabel',
|
||||
action: ({ nearbyToolData, eventData }) => {
|
||||
const { tool } = nearbyToolData;
|
||||
|
||||
const options = {
|
||||
skipAddLabelButton: true,
|
||||
editLocation: true,
|
||||
};
|
||||
|
||||
getMeasurementLocationCallback(eventData, tool, options);
|
||||
},
|
||||
},
|
||||
{
|
||||
actionType: 'setDescription',
|
||||
action: ({ nearbyToolData, eventData }) => {
|
||||
const { tool } = nearbyToolData;
|
||||
|
||||
const options = {
|
||||
editDescriptionOnDialog: true,
|
||||
};
|
||||
|
||||
getMeasurementLocationCallback(eventData, tool, options);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function getNearbyToolData(element, coords, toolTypes) {
|
||||
const nearbyTool = {};
|
||||
let pointNearTool = false;
|
||||
|
||||
toolTypes.forEach(toolType => {
|
||||
const toolData = cornerstoneTools.getToolState(element, toolType);
|
||||
if (!toolData) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolData.data.forEach(function(data, index) {
|
||||
// TODO: Fix this, it's ugly
|
||||
let toolInterface = cornerstoneTools.getToolForElement(element, toolType);
|
||||
if (!toolInterface) {
|
||||
toolInterface = cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
`${toolType}Tool`
|
||||
);
|
||||
}
|
||||
|
||||
if (!toolInterface) {
|
||||
throw new Error('Tool not found.');
|
||||
}
|
||||
|
||||
if (toolInterface.pointNearTool(element, data, coords)) {
|
||||
pointNearTool = true;
|
||||
nearbyTool.tool = data;
|
||||
nearbyTool.index = index;
|
||||
nearbyTool.toolType = toolType;
|
||||
}
|
||||
});
|
||||
|
||||
if (pointNearTool) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return pointNearTool ? nearbyTool : undefined;
|
||||
}
|
||||
|
||||
function getDropdownItems(eventData, isTouchEvent = false, availableTools) {
|
||||
const nearbyToolData = getNearbyToolData(
|
||||
eventData.element,
|
||||
eventData.currentPoints.canvas,
|
||||
toolTypes,
|
||||
availableTools
|
||||
);
|
||||
|
||||
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
|
||||
if (
|
||||
isTouchEvent &&
|
||||
nearbyToolData &&
|
||||
nearbyToolData.toolType === 'arrowAnnotate'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dropdownItems = [];
|
||||
if (nearbyToolData) {
|
||||
defaultDropdownItems.forEach(function(item) {
|
||||
item.params = {
|
||||
eventData,
|
||||
nearbyToolData,
|
||||
};
|
||||
|
||||
if (item.actionType === 'Delete') {
|
||||
item.text = 'Delete measurement';
|
||||
}
|
||||
|
||||
if (item.actionType === 'setLabel') {
|
||||
item.text = 'Relabel';
|
||||
}
|
||||
|
||||
if (item.actionType === 'setDescription') {
|
||||
item.text = `${
|
||||
nearbyToolData.tool.description ? 'Edit' : 'Add'
|
||||
} Description`;
|
||||
}
|
||||
|
||||
dropdownItems.push(item);
|
||||
});
|
||||
}
|
||||
|
||||
return dropdownItems;
|
||||
}
|
||||
|
||||
class ToolContextMenu extends Component {
|
||||
static propTypes = {
|
||||
isTouchEvent: PropTypes.bool.isRequired,
|
||||
eventData: PropTypes.object,
|
||||
onClose: PropTypes.func,
|
||||
availableTools: PropTypes.array,
|
||||
visible: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
visible: true,
|
||||
isTouchEvent: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.mainElement = React.createRef();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.eventData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isTouchEvent, eventData, availableTools } = this.props;
|
||||
const dropdownItems = getDropdownItems(
|
||||
eventData,
|
||||
isTouchEvent,
|
||||
availableTools
|
||||
);
|
||||
|
||||
// Skip if there is no dropdown item
|
||||
if (!dropdownItems.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dropdownComponents = dropdownItems.map(item => {
|
||||
const itemOnClick = event => {
|
||||
item.action(item.params);
|
||||
if (this.props.onClose) {
|
||||
this.props.onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<li key={item.actionType}>
|
||||
<button className="form-action" onClick={itemOnClick}>
|
||||
<span key={item.actionType}>{item.text}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
const position = {
|
||||
top: `${eventData.currentPoints.canvas.y}px`,
|
||||
left: `${eventData.currentPoints.canvas.x}px`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ToolContextMenu" style={position} ref={this.mainElement}>
|
||||
<ul className="bounded">{dropdownComponents}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
if (this.mainElement.current) {
|
||||
this.updateElementPosition();
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate = () => {
|
||||
if (this.mainElement.current) {
|
||||
this.updateElementPosition();
|
||||
}
|
||||
};
|
||||
|
||||
updateElementPosition = () => {
|
||||
const {
|
||||
offsetParent,
|
||||
offsetTop,
|
||||
offsetHeight,
|
||||
offsetWidth,
|
||||
offsetLeft,
|
||||
} = this.mainElement.current;
|
||||
|
||||
const { eventData } = this.props;
|
||||
|
||||
if (offsetTop + offsetHeight > offsetParent.offsetHeight) {
|
||||
const offBoundPixels =
|
||||
offsetTop + offsetHeight - offsetParent.offsetHeight;
|
||||
const top = eventData.currentPoints.canvas.y - offBoundPixels;
|
||||
|
||||
this.mainElement.current.style.top = `${top > 0 ? top : 0}px`;
|
||||
}
|
||||
|
||||
if (offsetLeft + offsetWidth > offsetParent.offsetWidth) {
|
||||
const offBoundPixels =
|
||||
offsetLeft + offsetWidth - offsetParent.offsetWidth;
|
||||
const left = eventData.currentPoints.canvas.x - offBoundPixels;
|
||||
|
||||
this.mainElement.current.style.left = `${left > 0 ? left : 0}px`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default ToolContextMenu;
|
||||
@@ -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,250 @@
|
||||
import './ToolbarRow.css';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import {
|
||||
ExpandableToolMenu,
|
||||
RoundedButtonGroup,
|
||||
ToolbarButton,
|
||||
} from '@ohif/ui';
|
||||
import { commandsManager, extensionManager } from './../App.js';
|
||||
|
||||
import ConnectedCineDialog from './ConnectedCineDialog';
|
||||
import ConnectedLayoutButton from './ConnectedLayoutButton';
|
||||
import ConnectedPluginSwitch from './ConnectedPluginSwitch.js';
|
||||
import { MODULE_TYPES } from '@ohif/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
class ToolbarRow extends Component {
|
||||
// TODO: Simplify these? isOpen can be computed if we say "any" value for selected,
|
||||
// closed if selected is null/undefined
|
||||
static propTypes = {
|
||||
isLeftSidePanelOpen: PropTypes.bool.isRequired,
|
||||
isRightSidePanelOpen: PropTypes.bool.isRequired,
|
||||
selectedLeftSidePanel: PropTypes.string.isRequired,
|
||||
selectedRightSidePanel: PropTypes.string.isRequired,
|
||||
handleSidePanelChange: PropTypes.func,
|
||||
activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const toolbarButtonDefinitions = _getVisibleToolbarButtons.call(this);
|
||||
// TODO:
|
||||
// If it's a tool that can be active... Mark it as active?
|
||||
// - Tools that are on/off?
|
||||
// - Tools that can be bound to multiple buttons?
|
||||
|
||||
// Normal ToolbarButtons...
|
||||
// Just how high do we need to hoist this state?
|
||||
// Why ToolbarRow instead of just Toolbar? Do we have any others?
|
||||
this.state = {
|
||||
toolbarButtons: toolbarButtonDefinitions,
|
||||
activeButtons: [],
|
||||
isCineDialogOpen: false,
|
||||
};
|
||||
|
||||
this._handleBuiltIn = _handleBuiltIn.bind(this);
|
||||
|
||||
const panelModules = extensionManager.modules[MODULE_TYPES.PANEL];
|
||||
this.buttonGroups = {
|
||||
left: [
|
||||
// TODO: This should come from extensions, instead of being baked in
|
||||
{
|
||||
value: 'studies',
|
||||
icon: 'th-large',
|
||||
bottomLabel: this.props.t('Series'),
|
||||
},
|
||||
],
|
||||
right: [],
|
||||
};
|
||||
|
||||
panelModules.forEach(panelExtension => {
|
||||
const panelModule = panelExtension.module;
|
||||
const defaultContexts = Array.from(panelModule.defaultContext);
|
||||
|
||||
// MENU OPTIONS
|
||||
panelModule.menuOptions.forEach(menuOption => {
|
||||
const contexts = Array.from(menuOption.context || defaultContexts);
|
||||
|
||||
const activeContextIncludesAnyPanelContexts = this.props.activeContexts.some(
|
||||
actx => contexts.includes(actx)
|
||||
);
|
||||
if (activeContextIncludesAnyPanelContexts) {
|
||||
const menuOptionEntry = {
|
||||
value: menuOption.target,
|
||||
icon: menuOption.icon,
|
||||
bottomLabel: menuOption.label,
|
||||
};
|
||||
const from = menuOption.from || 'right';
|
||||
|
||||
this.buttonGroups[from].push(menuOptionEntry);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const activeContextsChanged =
|
||||
prevProps.activeContexts !== this.props.activeContexts;
|
||||
|
||||
if (activeContextsChanged) {
|
||||
this.setState({
|
||||
toolbarButtons: _getVisibleToolbarButtons.call(this),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const buttonComponents = _getButtonComponents.call(
|
||||
this,
|
||||
this.state.toolbarButtons,
|
||||
this.state.activeButtons
|
||||
);
|
||||
|
||||
const cineDialogContainerStyle = {
|
||||
display: this.state.isCineDialogOpen ? 'block' : 'none',
|
||||
position: 'absolute',
|
||||
top: '82px',
|
||||
zIndex: 999,
|
||||
};
|
||||
|
||||
const onPress = (side, value) => {
|
||||
this.props.handleSidePanelChange(side, value);
|
||||
};
|
||||
const onPressLeft = onPress.bind(this, 'left');
|
||||
const onPressRight = onPress.bind(this, 'right');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ToolbarRow">
|
||||
<div className="pull-left m-t-1 p-y-1" style={{ padding: '10px' }}>
|
||||
<RoundedButtonGroup
|
||||
options={this.buttonGroups.left}
|
||||
value={this.props.selectedLeftSidePanel || ''}
|
||||
onValueChanged={onPressLeft}
|
||||
/>
|
||||
</div>
|
||||
{buttonComponents}
|
||||
<ConnectedLayoutButton />
|
||||
<ConnectedPluginSwitch />
|
||||
<div
|
||||
className="pull-right m-t-1 rm-x-1"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
{this.buttonGroups.right.length && (
|
||||
<RoundedButtonGroup
|
||||
options={this.buttonGroups.right}
|
||||
value={this.props.selectedRightSidePanel || ''}
|
||||
onValueChanged={onPressRight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="CineDialogContainer" style={cineDialogContainerStyle}>
|
||||
<ConnectedCineDialog />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which extension buttons should be showing, if they're
|
||||
* active, and what their onClick behavior should be.
|
||||
*/
|
||||
function _getButtonComponents(toolbarButtons, activeButtons) {
|
||||
return toolbarButtons.map((button, index) => {
|
||||
let activeCommand = undefined;
|
||||
|
||||
if (button.buttons && button.buttons.length) {
|
||||
// Iterate over button definitions and update `onClick` behavior
|
||||
const childButtons = button.buttons.map(childButton => {
|
||||
childButton.onClick = _handleToolbarButtonClick.bind(this, childButton);
|
||||
|
||||
if (activeButtons.indexOf(childButton.id) > -1) {
|
||||
activeCommand = childButton.id;
|
||||
}
|
||||
|
||||
return childButton;
|
||||
});
|
||||
|
||||
return (
|
||||
<ExpandableToolMenu
|
||||
key={button.id}
|
||||
text={button.label}
|
||||
icon={button.icon}
|
||||
buttons={childButtons}
|
||||
activeCommand={activeCommand}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={button.id}
|
||||
label={button.label}
|
||||
icon={button.icon}
|
||||
onClick={_handleToolbarButtonClick.bind(this, button)}
|
||||
isActive={activeButtons.includes(button.id)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A handy way for us to handle different button types. IE. firing commands for
|
||||
* buttons, or initiation built in behavior.
|
||||
*
|
||||
* @param {*} button
|
||||
* @param {*} evt
|
||||
* @param {*} props
|
||||
*/
|
||||
function _handleToolbarButtonClick(button, evt, props) {
|
||||
if (button.commandName) {
|
||||
const options = Object.assign({ evt }, button.commandOptions);
|
||||
commandsManager.runCommand(button.commandName, options);
|
||||
}
|
||||
|
||||
// TODO: Use Types ENUM
|
||||
// TODO: We can update this to be a `getter` on the extension to query
|
||||
// For the active tools after we apply our updates?
|
||||
if (button.type === 'setToolActive') {
|
||||
this.setState({
|
||||
activeButtons: [button.id],
|
||||
});
|
||||
} else if (button.type === 'builtIn') {
|
||||
this._handleBuiltIn(button.options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function _getVisibleToolbarButtons() {
|
||||
const toolbarModules = extensionManager.modules[MODULE_TYPES.TOOLBAR];
|
||||
const toolbarButtonDefinitions = [];
|
||||
|
||||
toolbarModules.forEach(extension => {
|
||||
const { definitions, defaultContext } = extension.module;
|
||||
definitions.forEach(definition => {
|
||||
const context = definition.context || defaultContext;
|
||||
|
||||
if (this.props.activeContexts.includes(context)) {
|
||||
toolbarButtonDefinitions.push(definition);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return toolbarButtonDefinitions;
|
||||
}
|
||||
|
||||
function _handleBuiltIn({ behavior } = {}) {
|
||||
if (behavior === 'CINE') {
|
||||
this.setState({
|
||||
isCineDialogOpen: !this.state.isCineDialogOpen,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(ToolbarRow);
|
||||
@@ -0,0 +1,10 @@
|
||||
#viewer {
|
||||
background-color: black;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loadingTextDiv {
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { MODULE_TYPES } from '@ohif/core';
|
||||
import OHIF from '@ohif/core';
|
||||
import moment from 'moment';
|
||||
import WhiteLabellingContext from '../WhiteLabellingContext.js';
|
||||
import ConnectedHeader from './ConnectedHeader.js';
|
||||
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
||||
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
|
||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||
import SidePanel from './../components/SidePanel.js';
|
||||
import { extensionManager } from './../App.js';
|
||||
import UserManagerContext from '../UserManagerContext';
|
||||
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,
|
||||
// window.store.getState().viewports.viewportSpecificData
|
||||
viewports: PropTypes.object.isRequired,
|
||||
// window.store.getState().viewports.activeViewportIndex
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
state = {
|
||||
isLeftSidePanelOpen: true,
|
||||
isRightSidePanelOpen: false,
|
||||
selectedRightSidePanel: '',
|
||||
selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this
|
||||
thumbnails: [],
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
this.currentTimepointId = currentTimepointId;
|
||||
this.timepointApi = timepointApi;
|
||||
this.measurementApi = measurementApi;
|
||||
|
||||
if (studies) {
|
||||
const patientId = studies[0] && studies[0].patientId;
|
||||
|
||||
timepointApi.retrieveTimepoints({ patientId });
|
||||
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
||||
|
||||
this.setState({
|
||||
thumbnails: _mapStudiesToThumbnails(studies),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.studies !== prevProps.studies) {
|
||||
const { studies } = this.props;
|
||||
const patientId = studies[0] && studies[0].patientId;
|
||||
const currentTimepointId = this.currentTimepointId;
|
||||
|
||||
this.timepointApi.retrieveTimepoints({ patientId });
|
||||
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
||||
|
||||
this.setState({
|
||||
thumbnails: _mapStudiesToThumbnails(studies),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let VisiblePanelLeft, VisiblePanelRight;
|
||||
const panelExtensions = extensionManager.modules[MODULE_TYPES.PANEL];
|
||||
|
||||
panelExtensions.forEach(panelExt => {
|
||||
panelExt.module.components.forEach(comp => {
|
||||
if (comp.id === this.state.selectedRightSidePanel) {
|
||||
VisiblePanelRight = comp.component;
|
||||
} else if (comp.id === this.state.selectedLeftSidePanel) {
|
||||
VisiblePanelLeft = comp.component;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* HEADER */}
|
||||
<WhiteLabellingContext.Consumer>
|
||||
{whiteLabelling => (
|
||||
<UserManagerContext.Consumer>
|
||||
{userManager => (
|
||||
<ConnectedHeader home={false} userManager={userManager}>
|
||||
{whiteLabelling.logoComponent}
|
||||
</ConnectedHeader>
|
||||
)}
|
||||
</UserManagerContext.Consumer>
|
||||
)}
|
||||
</WhiteLabellingContext.Consumer>
|
||||
|
||||
{/* TOOLBAR */}
|
||||
<ConnectedToolbarRow
|
||||
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
|
||||
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
|
||||
selectedLeftSidePanel={
|
||||
this.state.isLeftSidePanelOpen
|
||||
? this.state.selectedLeftSidePanel
|
||||
: ''
|
||||
}
|
||||
selectedRightSidePanel={
|
||||
this.state.isRightSidePanelOpen
|
||||
? this.state.selectedRightSidePanel
|
||||
: ''
|
||||
}
|
||||
handleSidePanelChange={(side, selectedPanel) => {
|
||||
const sideClicked = side && side[0].toUpperCase() + side.slice(1);
|
||||
const openKey = `is${sideClicked}SidePanelOpen`;
|
||||
const selectedKey = `selected${sideClicked}SidePanel`;
|
||||
const updatedState = Object.assign({}, this.state);
|
||||
|
||||
const isOpen = updatedState[openKey];
|
||||
const prevSelectedPanel = updatedState[selectedKey];
|
||||
// RoundedButtonGroup returns `null` if selected button is clicked
|
||||
const isSameSelectedPanel =
|
||||
prevSelectedPanel === selectedPanel || selectedPanel === null;
|
||||
|
||||
updatedState[selectedKey] = selectedPanel || prevSelectedPanel;
|
||||
|
||||
const isClosedOrShouldClose = !isOpen || isSameSelectedPanel;
|
||||
if (isClosedOrShouldClose) {
|
||||
updatedState[openKey] = !updatedState[openKey];
|
||||
}
|
||||
|
||||
this.setState(updatedState);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
||||
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
||||
|
||||
{/* VIEWPORTS + SIDEPANELS */}
|
||||
<div className="FlexboxLayout">
|
||||
{/* LEFT */}
|
||||
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
|
||||
{VisiblePanelLeft ? (
|
||||
<VisiblePanelLeft
|
||||
viewports={this.props.viewports}
|
||||
activeIndex={this.props.activeViewportIndex}
|
||||
/>
|
||||
) : (
|
||||
<ConnectedStudyBrowser studies={this.state.thumbnails} />
|
||||
)}
|
||||
</SidePanel>
|
||||
|
||||
{/* MAIN */}
|
||||
<div className={classNames('main-content')}>
|
||||
<ConnectedViewerMain studies={this.props.studies} />
|
||||
</div>
|
||||
|
||||
{/* RIGHT */}
|
||||
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
|
||||
{VisiblePanelRight && (
|
||||
<VisiblePanelRight
|
||||
viewports={this.props.viewports}
|
||||
activeIndex={this.props.activeViewportIndex}
|
||||
/>
|
||||
)}
|
||||
</SidePanel>
|
||||
</div>
|
||||
<ConnectedLabellingOverlay />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Viewer;
|
||||
|
||||
/**
|
||||
* What types are these? Why do we have "mapping" dropped in here instead of in
|
||||
* a mapping layer?
|
||||
*
|
||||
* TODO[react]:
|
||||
* - Add sorting of display sets
|
||||
* - Add showStackLoadingProgressBar option
|
||||
*
|
||||
* @param {Study[]} studies
|
||||
* @param {DisplaySet[]} studies[].displaySets
|
||||
*/
|
||||
const _mapStudiesToThumbnails = function(studies) {
|
||||
return studies.map(study => {
|
||||
const { studyInstanceUid } = study;
|
||||
|
||||
const thumbnails = study.displaySets.map(displaySet => {
|
||||
const {
|
||||
displaySetInstanceUid,
|
||||
seriesDescription,
|
||||
seriesNumber,
|
||||
instanceNumber,
|
||||
numImageFrames,
|
||||
} = displaySet;
|
||||
|
||||
let imageId;
|
||||
let altImageText;
|
||||
|
||||
if (displaySet.modality && displaySet.modality === 'SEG') {
|
||||
// TODO: We want to replace this with a thumbnail showing
|
||||
// the segmentation map on the image, but this is easier
|
||||
// and better than what we have right now.
|
||||
altImageText = 'SEG';
|
||||
} else if (displaySet.images && displaySet.images.length) {
|
||||
const imageIndex = Math.floor(displaySet.images.length / 2);
|
||||
|
||||
imageId = displaySet.images[imageIndex].getImageId();
|
||||
} else {
|
||||
altImageText = displaySet.modality ? displaySet.modality : 'UN';
|
||||
}
|
||||
|
||||
return {
|
||||
imageId,
|
||||
altImageText,
|
||||
displaySetInstanceUid,
|
||||
seriesDescription,
|
||||
seriesNumber,
|
||||
instanceNumber,
|
||||
numImageFrames,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
thumbnails,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
.drag-drop-instructions {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drag-drop-instructions h3 {
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.drag-drop-instructions h4 {
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, { Component } from 'react';
|
||||
import { metadata, utils } from '@ohif/core';
|
||||
|
||||
import ConnectedViewer from './ConnectedViewer.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { extensionManager } from './../App.js';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import filesToStudies from '../lib/filesToStudies';
|
||||
import './ViewerLocalFileData.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
const { OHIFStudyMetadata } = metadata;
|
||||
const { studyMetadataManager, updateMetaDataManager } = utils;
|
||||
|
||||
class ViewerLocalFileData extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array,
|
||||
};
|
||||
|
||||
state = {
|
||||
studies: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
updateStudies = studies => {
|
||||
// Render the viewer when the data is ready
|
||||
studyMetadataManager.purge();
|
||||
|
||||
// Map studies to new format, update metadata manager?
|
||||
const updatedStudies = studies.map(study => {
|
||||
const studyMetadata = new OHIFStudyMetadata(
|
||||
study,
|
||||
study.studyInstanceUid
|
||||
);
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
|
||||
study.displaySets =
|
||||
study.displaySets ||
|
||||
studyMetadata.createDisplaySets(sopClassHandlerModules);
|
||||
studyMetadata.setDisplaySets(study.displaySets);
|
||||
|
||||
// Updates WADO-RS metaDataManager
|
||||
updateMetaDataManager(study);
|
||||
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
|
||||
return study;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
studies: updatedStudies,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const onDrop = async acceptedFiles => {
|
||||
this.setState({ loading: true });
|
||||
|
||||
const studies = await filesToStudies(acceptedFiles);
|
||||
const updatedStudies = this.updateStudies(studies);
|
||||
|
||||
if (!updatedStudies) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ studies: updatedStudies, loading: false });
|
||||
};
|
||||
|
||||
if (this.state.error) {
|
||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropzone onDrop={onDrop}>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div {...getRootProps()} style={{ width: '100%', height: '100%' }}>
|
||||
{this.state.studies ? (
|
||||
<ConnectedViewer
|
||||
studies={this.state.studies}
|
||||
studyInstanceUids={
|
||||
this.state.studies &&
|
||||
this.state.studies.map(a => a.studyInstanceUid)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className={'drag-drop-instructions'}>
|
||||
<div className={'drag-drop-contents'}>
|
||||
{this.state.loading ? (
|
||||
<h3>{this.props.t('Loading...')}</h3>
|
||||
) : (
|
||||
<>
|
||||
<h3>
|
||||
{this.props.t(
|
||||
'Drag and Drop DICOM files here to load them in the Viewer'
|
||||
)}
|
||||
</h3>
|
||||
<h4>
|
||||
{this.props.t(
|
||||
"Or click to load the browser's file selector"
|
||||
)}
|
||||
</h4>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<input {...getInputProps()} style={{ display: 'none' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(ViewerLocalFileData);
|
||||
@@ -0,0 +1,21 @@
|
||||
#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%;
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import './ViewerMain.css';
|
||||
|
||||
import { Component } from 'react';
|
||||
import ConnectedLayoutManager from './ConnectedLayoutManager.js';
|
||||
import ConnectedToolContextMenu from './ConnectedToolContextMenu.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
class ViewerMain extends Component {
|
||||
static propTypes = {
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
studies: PropTypes.array,
|
||||
viewportSpecificData: PropTypes.object.isRequired,
|
||||
layout: PropTypes.object.isRequired,
|
||||
setViewportSpecificData: PropTypes.func.isRequired,
|
||||
clearViewportSpecificData: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
displaySets: [],
|
||||
};
|
||||
|
||||
this.cachedViewportData = {};
|
||||
}
|
||||
|
||||
getDisplaySets(studies) {
|
||||
const displaySets = [];
|
||||
studies.forEach(study => {
|
||||
study.displaySets.forEach(dSet => {
|
||||
if (!dSet.plugin) {
|
||||
dSet.plugin = 'cornerstone';
|
||||
}
|
||||
displaySets.push(dSet);
|
||||
});
|
||||
});
|
||||
|
||||
return displaySets;
|
||||
}
|
||||
|
||||
findDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
const study = studies.find(study => {
|
||||
return study.studyInstanceUid === studyInstanceUid;
|
||||
});
|
||||
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
return study.displaySets.find(displaySet => {
|
||||
return displaySet.displaySetInstanceUid === displaySetInstanceUid;
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Add beforeUnload event handler to check for unsaved changes
|
||||
//window.addEventListener('beforeunload', unloadHandlers.beforeUnload);
|
||||
|
||||
// Get all the display sets for the viewer studies
|
||||
if (this.props.studies) {
|
||||
const displaySets = this.getDisplaySets(this.props.studies);
|
||||
|
||||
this.setState({
|
||||
displaySets,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.studies !== prevProps.studies) {
|
||||
const displaySets = this.getDisplaySets(this.props.studies);
|
||||
|
||||
this.setState({
|
||||
displaySets,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getViewportData = () => {
|
||||
const viewportData = [];
|
||||
const { layout, viewportSpecificData } = this.props;
|
||||
|
||||
for (
|
||||
let viewportIndex = 0;
|
||||
viewportIndex < layout.viewports.length;
|
||||
viewportIndex++
|
||||
) {
|
||||
let displaySet = viewportSpecificData[viewportIndex];
|
||||
|
||||
// Use the cached display set in viewport if the new one is empty
|
||||
if (displaySet && !displaySet.displaySetInstanceUid) {
|
||||
displaySet = this.cachedViewportData[viewportIndex];
|
||||
}
|
||||
|
||||
if (
|
||||
displaySet &&
|
||||
displaySet.studyInstanceUid &&
|
||||
displaySet.displaySetInstanceUid
|
||||
) {
|
||||
// Get missing fields from original display set
|
||||
const originalDisplaySet = this.findDisplaySet(
|
||||
this.props.studies,
|
||||
displaySet.studyInstanceUid,
|
||||
displaySet.displaySetInstanceUid
|
||||
);
|
||||
viewportData.push(Object.assign({}, originalDisplaySet, displaySet));
|
||||
} else {
|
||||
// If the viewport is empty, get one available in study
|
||||
const { displaySets } = this.state;
|
||||
displaySet = displaySets.find(
|
||||
ds =>
|
||||
!viewportData.some(
|
||||
v => v.displaySetInstanceUid === ds.displaySetInstanceUid
|
||||
)
|
||||
);
|
||||
viewportData.push(Object.assign({}, displaySet));
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedViewportData = viewportData;
|
||||
|
||||
return viewportData;
|
||||
};
|
||||
|
||||
setViewportData = ({ viewportIndex, item }) => {
|
||||
const displaySet = this.findDisplaySet(
|
||||
this.props.studies,
|
||||
item.studyInstanceUid,
|
||||
item.displaySetInstanceUid
|
||||
);
|
||||
|
||||
this.props.setViewportSpecificData(viewportIndex, displaySet);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ViewerMain">
|
||||
{this.state.displaySets.length && (
|
||||
<ConnectedLayoutManager
|
||||
studies={this.props.studies}
|
||||
viewportData={this.getViewportData()}
|
||||
setViewportData={this.setViewportData}
|
||||
>
|
||||
{/* Children to add to each viewport that support children */}
|
||||
<ConnectedToolContextMenu />
|
||||
</ConnectedLayoutManager>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Clear the entire viewport specific data
|
||||
const { viewportSpecificData } = this.props;
|
||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||
this.props.clearViewportSpecificData(viewportIndex);
|
||||
});
|
||||
|
||||
// TODO: These don't have to be viewer specific?
|
||||
// Could qualify for other routes?
|
||||
// hotkeys.destroy();
|
||||
|
||||
// Remove beforeUnload event handler...
|
||||
//window.removeEventListener('beforeunload', unloadHandlers.beforeUnload);
|
||||
// Destroy the synchronizer used to update reference lines
|
||||
//OHIF.viewer.updateImageSynchronizer.destroy();
|
||||
// TODO: Instruct all plugins to clean up themselves
|
||||
//
|
||||
// Clear references to all stacks in the StackManager
|
||||
//StackManager.clearStacks();
|
||||
// @TypeSafeStudies
|
||||
// Clears OHIF.viewer.Studies collection
|
||||
//OHIF.viewer.Studies.removeAll();
|
||||
// @TypeSafeStudies
|
||||
// Clears OHIF.viewer.StudyMetadataList collection
|
||||
//OHIF.viewer.StudyMetadataList.removeAll();
|
||||
}
|
||||
}
|
||||
|
||||
export default ViewerMain;
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { Component } from "react";
|
||||
import { metadata, studies, utils } from "@ohif/core";
|
||||
|
||||
import ConnectedViewer from "./ConnectedViewer.js";
|
||||
import PropTypes from "prop-types";
|
||||
import { extensionManager } from "./../App.js";
|
||||
|
||||
const { OHIFStudyMetadata } = metadata;
|
||||
const { retrieveStudiesMetadata } = studies;
|
||||
const { studyMetadataManager, updateMetaDataManager } = utils;
|
||||
|
||||
class ViewerRetrieveStudyData extends Component {
|
||||
static propTypes = {
|
||||
studyInstanceUids: PropTypes.array.isRequired,
|
||||
seriesInstanceUids: PropTypes.array,
|
||||
server: PropTypes.object
|
||||
};
|
||||
|
||||
state = {
|
||||
studies: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
// TODO: Avoid using timepoints here
|
||||
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
|
||||
const { studyInstanceUids, seriesInstanceUids, server } = this.props;
|
||||
|
||||
try {
|
||||
const studies = await retrieveStudiesMetadata(
|
||||
server,
|
||||
studyInstanceUids,
|
||||
seriesInstanceUids
|
||||
);
|
||||
|
||||
// Render the viewer when the data is ready
|
||||
// TODO: CLEAR THIS SOMEWHERE ELSE
|
||||
studyMetadataManager.purge();
|
||||
|
||||
// Map studies to new format, update metadata manager?
|
||||
const updatedStudies = studies.map(study => {
|
||||
const studyMetadata = new OHIFStudyMetadata(
|
||||
study,
|
||||
study.studyInstanceUid
|
||||
);
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules["sopClassHandlerModule"];
|
||||
|
||||
study.displaySets =
|
||||
study.displaySets ||
|
||||
studyMetadata.createDisplaySets(sopClassHandlerModules);
|
||||
studyMetadata.setDisplaySets(study.displaySets);
|
||||
|
||||
// Updates WADO-RS metaDataManager
|
||||
updateMetaDataManager(study);
|
||||
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
|
||||
return study;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
studies: updatedStudies
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
error: true
|
||||
});
|
||||
|
||||
// TODO: Handle gracefully instead of throwing?
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectedViewer
|
||||
studies={this.state.studies}
|
||||
studyInstanceUids={this.props.studyInstanceUids}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ViewerRetrieveStudyData;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { connect } from 'react-redux';
|
||||
import DicomFileUploaderModal from './DicomFileUploaderModal.js';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
const isActive = a => a.active === true;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const activeServer = state.servers.servers.find(isActive);
|
||||
|
||||
// TODO: Not sure I like this approach since it means we are recreating
|
||||
// this function every time redux changes
|
||||
const retrieveAuthHeaderFunction = () =>
|
||||
OHIF.DICOMWeb.getAuthorizationHeader(activeServer);
|
||||
|
||||
return {
|
||||
retrieveAuthHeaderFunction,
|
||||
url: activeServer && activeServer.qidoRoot,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedDicomFileUploader = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(DicomFileUploaderModal);
|
||||
|
||||
export default ConnectedDicomFileUploader;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { connect } from 'react-redux';
|
||||
import DicomStorePickerModal from './DicomStorePickerModal.js';
|
||||
|
||||
const isActive = a => a.active === true;
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const activeServer = state.servers.servers.find(isActive);
|
||||
|
||||
return {
|
||||
user: state.oidc && state.oidc.user,
|
||||
url: activeServer && activeServer.qidoRoot,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setServers: servers => {
|
||||
const action = {
|
||||
type: 'SET_SERVERS',
|
||||
servers,
|
||||
};
|
||||
dispatch(action);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedDicomStorePicker = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DicomStorePickerModal);
|
||||
|
||||
export default ConnectedDicomStorePicker;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from './api/GoogleCloudApi';
|
||||
import DatasetsList from './DatasetsList';
|
||||
import './googleCloud.css';
|
||||
|
||||
export default class DatasetPicker extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
datasets: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
project: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
accessToken: PropTypes.string,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setAccessToken(this.props.accessToken);
|
||||
|
||||
const response = await api.loadDatasets(
|
||||
this.props.project.projectId,
|
||||
this.props.location.locationId
|
||||
);
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
datasets: response.data.datasets || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { datasets, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<DatasetsList
|
||||
datasets={datasets}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import DicomStorePicker from './DicomStorePicker';
|
||||
import DatasetPicker from './DatasetPicker';
|
||||
import ProjectPicker from './ProjectPicker';
|
||||
import LocationPicker from './LocationPicker';
|
||||
import GoogleCloudApi from './api/GoogleCloudApi';
|
||||
import './googleCloud.css';
|
||||
|
||||
class DatasetSelector extends Component {
|
||||
state = {
|
||||
project: null,
|
||||
location: null,
|
||||
dataset: null,
|
||||
unloading: false,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
event: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
canClose: PropTypes.string,
|
||||
setServers: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
onProjectSelect = project => {
|
||||
this.setState({
|
||||
project,
|
||||
});
|
||||
};
|
||||
|
||||
onLocationSelect = location => {
|
||||
this.setState({
|
||||
location,
|
||||
});
|
||||
};
|
||||
|
||||
onDatasetSelect = dataset => {
|
||||
this.setState({
|
||||
dataset,
|
||||
});
|
||||
};
|
||||
|
||||
onProjectClick = () => {
|
||||
this.setState({
|
||||
dataset: null,
|
||||
location: null,
|
||||
project: null,
|
||||
});
|
||||
};
|
||||
|
||||
onLocationClick = () => {
|
||||
this.setState({
|
||||
dataset: null,
|
||||
location: null,
|
||||
});
|
||||
};
|
||||
|
||||
onDatasetClick = () => {
|
||||
this.setState({
|
||||
dataset: null,
|
||||
});
|
||||
};
|
||||
|
||||
onDicomStoreSelect = dicomStoreJson => {
|
||||
const dicomStore = dicomStoreJson.name;
|
||||
const parts = dicomStore.split('/');
|
||||
const result = {
|
||||
wadoUriRoot: GoogleCloudApi.urlBase + `/${dicomStore}/dicomWeb`,
|
||||
qidoRoot: GoogleCloudApi.urlBase + `/${dicomStore}/dicomWeb`,
|
||||
wadoRoot: GoogleCloudApi.urlBase + `/${dicomStore}/dicomWeb`,
|
||||
project: parts[1],
|
||||
location: parts[3],
|
||||
dataset: parts[5],
|
||||
dicomStore: parts[7],
|
||||
};
|
||||
this.props.setServers(result);
|
||||
};
|
||||
|
||||
render() {
|
||||
const accessToken = this.props.user.access_token;
|
||||
|
||||
const { project, location, dataset } = this.state;
|
||||
const {
|
||||
onProjectClick,
|
||||
onLocationClick,
|
||||
onDatasetClick,
|
||||
onProjectSelect,
|
||||
onLocationSelect,
|
||||
onDatasetSelect,
|
||||
onDicomStoreSelect,
|
||||
} = this;
|
||||
|
||||
let projectBreadcrumbs = (
|
||||
<div className="gcp-picker--path">
|
||||
<span>{this.props.t('Select a Project')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (project) {
|
||||
projectBreadcrumbs = (
|
||||
<div className="gcp-picker--path">
|
||||
<span onClick={onProjectClick}>{project.name}</span>
|
||||
{project && location && (
|
||||
<span onClick={onLocationClick}>
|
||||
{' '}
|
||||
-> {location.name.split('/')[3]}
|
||||
</span>
|
||||
)}
|
||||
{project && location && dataset && (
|
||||
<span onClick={onDatasetClick}>
|
||||
{' '}
|
||||
-> {dataset.name.split('/')[5]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{projectBreadcrumbs}
|
||||
{!project && (
|
||||
<ProjectPicker
|
||||
accessToken={accessToken}
|
||||
onSelect={onProjectSelect}
|
||||
/>
|
||||
)}
|
||||
|
||||
{project && !location && (
|
||||
<LocationPicker
|
||||
accessToken={accessToken}
|
||||
project={project}
|
||||
onSelect={onLocationSelect}
|
||||
/>
|
||||
)}
|
||||
{project && location && !dataset && (
|
||||
<DatasetPicker
|
||||
accessToken={accessToken}
|
||||
project={project}
|
||||
location={location}
|
||||
onSelect={onDatasetSelect}
|
||||
/>
|
||||
)}
|
||||
{project && location && dataset && (
|
||||
<DicomStorePicker
|
||||
accessToken={accessToken}
|
||||
dataset={dataset}
|
||||
onSelect={onDicomStoreSelect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DatasetSelector);
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
class DatasetsList extends Component {
|
||||
state = {
|
||||
search: '',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
datasets: PropTypes.array,
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
renderTableRow = dataset => {
|
||||
return (
|
||||
<tr
|
||||
key={dataset.name}
|
||||
className={
|
||||
this.state.highlightedItem === dataset.name
|
||||
? 'noselect active'
|
||||
: 'noselect'
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
this.onHighlightItem(dataset.name);
|
||||
}}
|
||||
onClick={() => {
|
||||
this.props.onSelect(dataset);
|
||||
}}
|
||||
>
|
||||
<td>{dataset.name.split('/')[5]}</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
onHighlightItem(dataset) {
|
||||
this.setState({ highlightedItem: dataset });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="DatasetList">
|
||||
{this.props.datasets.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblDatasetList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('Dataset')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.datasets && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DatasetsList);
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import DicomUploader from './DicomUploader';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
class DicomFileUploaderModal extends Component {
|
||||
static propTypes = {
|
||||
url: PropTypes.string,
|
||||
retrieveAuthHeaderFunction: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
uploaded: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.isOpen}
|
||||
onHide={this.props.onClose}
|
||||
aria-labelledby="ModalHeader"
|
||||
className="modal fade themed in"
|
||||
backdrop={false}
|
||||
size={'md'}
|
||||
keyboard={true}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{this.props.t('Upload DICOM Files')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<DicomUploader
|
||||
url={this.props.url}
|
||||
retrieveAuthHeaderFunction={this.props.retrieveAuthHeaderFunction}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DicomFileUploaderModal);
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
class DicomStoreList extends Component {
|
||||
state = {
|
||||
search: '',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
stores: PropTypes.array,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
renderTableRow = store => {
|
||||
return (
|
||||
<tr
|
||||
key={store.name}
|
||||
className={
|
||||
this.state.highlightedItem === store.name
|
||||
? 'noselect active'
|
||||
: 'noselect'
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
this.onHighlightItem(store.name);
|
||||
}}
|
||||
onClick={() => {
|
||||
this.props.onSelect(store);
|
||||
}}
|
||||
>
|
||||
<td className="project">{store.name.split('/')[7]}</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
onHighlightItem(store) {
|
||||
this.setState({ highlightedItem: store });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="StoreList">{this.props.stores.map(this.renderTableRow)}</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblStoreList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('DICOM Store')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.stores && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DicomStoreList);
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from './api/GoogleCloudApi';
|
||||
import DicomStoreList from './DicomStoreList';
|
||||
import './googleCloud.css';
|
||||
|
||||
export default class DicomStorePicker extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
stores: [],
|
||||
locations: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dataset: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
accessToken: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setAccessToken(this.props.accessToken);
|
||||
|
||||
const response = await api.loadDicomStores(this.props.dataset.name);
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
stores: response.data.dicomStores || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { stores, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
|
||||
return (
|
||||
<DicomStoreList
|
||||
stores={stores}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import DatasetSelector from './DatasetSelector';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
class DicomStorePickerModal extends Component {
|
||||
static propTypes = {
|
||||
url: PropTypes.string,
|
||||
user: PropTypes.object.isRequired,
|
||||
setServers: PropTypes.func.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
handleEvent = data => {
|
||||
const servers = [
|
||||
{
|
||||
name: data.dicomStore,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
qidoSupportsIncludeField: false,
|
||||
type: 'dicomWeb',
|
||||
qidoRoot: data.qidoRoot,
|
||||
wadoRoot: data.wadoRoot,
|
||||
wadoUriRoot: data.wadoUriRoot,
|
||||
active: true,
|
||||
supportsFuzzyMatching: false,
|
||||
},
|
||||
];
|
||||
|
||||
this.props.setServers(servers);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.isOpen}
|
||||
onHide={this.props.onClose}
|
||||
aria-labelledby="ModalHeader"
|
||||
className="modal fade themed in"
|
||||
backdrop={false}
|
||||
size={'md'}
|
||||
keyboard={true}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{this.props.t('Google Cloud Healthcare API')}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<DatasetSelector
|
||||
setServers={this.handleEvent}
|
||||
user={this.props.user}
|
||||
url={this.props.url}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(DicomStorePickerModal);
|
||||
@@ -0,0 +1,20 @@
|
||||
.dicom-uploader .button {
|
||||
float: left;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.invisible-input {
|
||||
position: absolute;
|
||||
display: none;
|
||||
z-index: -1000;
|
||||
max-width: 0 !important;
|
||||
max-height: 0 !important;
|
||||
}
|
||||
|
||||
.dicom-uploader .table-header {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 28px;
|
||||
text-align: left;
|
||||
margin: 20px auto;
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { formatFileSize } from './utils/helpers';
|
||||
import CancellationToken from './utils/CancellationToken';
|
||||
import dicomUploader from './api/DicomUploadService';
|
||||
import './DicomUploader.css';
|
||||
|
||||
export default class DicomUploader extends Component {
|
||||
state = {
|
||||
status: 'Upload',
|
||||
isCancelled: false,
|
||||
errorsCount: 0,
|
||||
files: null,
|
||||
uploadedVolume: null,
|
||||
wholeVolumeStr: null,
|
||||
isFilesListHidden: true,
|
||||
timeLeft: null,
|
||||
uploadedList: null,
|
||||
totalCount: 0,
|
||||
successfullyUploadedCount: 0,
|
||||
lastFile: '',
|
||||
uploadContext: null, // this is probably not needed, but we use this variable to distinguish between different downloads
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
event: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
retrieveAuthHeaderFunction: PropTypes.func,
|
||||
};
|
||||
|
||||
filesLeft() {
|
||||
return (
|
||||
this.state.uploadedList.length + ' of ' + this.state.totalCount + ' files'
|
||||
);
|
||||
}
|
||||
|
||||
volumeLeft() {
|
||||
let left = formatFileSize(this.state.uploadedVolume);
|
||||
return left + ' of ' + this.state.wholeVolumeStr;
|
||||
}
|
||||
|
||||
percents() {
|
||||
return parseInt(
|
||||
(100 * this.state.uploadedList.length) /
|
||||
Object.keys(this.state.files).length
|
||||
);
|
||||
}
|
||||
|
||||
isFinished() {
|
||||
return (
|
||||
this.state.isCancelled ||
|
||||
Object.keys(this.state.files).length === this.state.uploadedList.length
|
||||
);
|
||||
}
|
||||
|
||||
errorsMessage() {
|
||||
const errors = this.state.errorsCount === 1 ? ' error' : ' errors';
|
||||
return (
|
||||
this.state.errorsCount + errors + ' while uploading, click for more info'
|
||||
);
|
||||
}
|
||||
|
||||
uploadFiles = files => {
|
||||
const filesArray = Array.from(files.target.files);
|
||||
const filesDict = {};
|
||||
filesArray.forEach((file, i) => {
|
||||
const fileDesc = {
|
||||
id: i,
|
||||
name: file.name,
|
||||
path: file.webkitRelativePath || file.name,
|
||||
size: file.size,
|
||||
error: null,
|
||||
processed: false,
|
||||
processedInUI: false,
|
||||
};
|
||||
filesDict[i] = fileDesc;
|
||||
file.fileId = i;
|
||||
});
|
||||
const wholeVolume = filesArray.map(f => f.size).reduce((a, b) => a + b);
|
||||
const uploadContext = Math.random();
|
||||
this.setState({
|
||||
status: 'Uploading...',
|
||||
files: filesDict,
|
||||
uploadedList: [],
|
||||
uploadedVolume: 0,
|
||||
lastFile: filesArray[0].name,
|
||||
totalCount: filesArray.length,
|
||||
wholeVolumeStr: formatFileSize(wholeVolume),
|
||||
uploadContext: uploadContext,
|
||||
cancellationToken: new CancellationToken(),
|
||||
});
|
||||
const cancellationToken = new CancellationToken();
|
||||
const uploadCallback = (fileId, error) =>
|
||||
uploadContext === this.state.uploadContext &&
|
||||
this.uploadCallback.call(this, fileId, error);
|
||||
|
||||
dicomUploader.setRetrieveAuthHeaderFunction(this.props.retrieveAuthHeaderFunction);
|
||||
|
||||
dicomUploader.smartUpload(
|
||||
files.target.files,
|
||||
this.props.url,
|
||||
uploadCallback,
|
||||
cancellationToken
|
||||
);
|
||||
};
|
||||
|
||||
uploadCallback(fileId, error) {
|
||||
const file = this.state.files[fileId];
|
||||
file.processed = true;
|
||||
if (!error) {
|
||||
let uploadedVolume = this.state.uploadedVolume + file.size;
|
||||
this.setState({ uploadedVolume });
|
||||
} else {
|
||||
file.error = error;
|
||||
this.setState({ errorsCount: this.state.errorsCount + 1 });
|
||||
}
|
||||
this.setState({ lastFile: file.name });
|
||||
let uploadedList = this.state.uploadedList;
|
||||
uploadedList.push(file);
|
||||
this.setState({ uploadedList });
|
||||
}
|
||||
|
||||
renderTableRow = file => {
|
||||
let error = null;
|
||||
if (file.error !== null) {
|
||||
error = <p style={{ color: 'red' }}>{file.error}</p>;
|
||||
}
|
||||
return (
|
||||
<tr key={file.id}>
|
||||
<td className="project">
|
||||
{file.name} {error}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.files === null) {
|
||||
return (
|
||||
<div className="dicom-uploader">
|
||||
<div className="button">
|
||||
<label htmlFor="file">
|
||||
<img src="./assets/Button_File.svg" alt="upload file"></img>
|
||||
</label>
|
||||
<input
|
||||
className="invisible-input"
|
||||
onChange={this.uploadFiles}
|
||||
type="file"
|
||||
id="file"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="button">
|
||||
<label htmlFor="folder">
|
||||
<img src="./assets/Button_Folder.svg" alt="upload folder"></img>
|
||||
</label>
|
||||
<input
|
||||
className="invisible-input"
|
||||
type="file"
|
||||
onChange={this.uploadFiles}
|
||||
id="folder"
|
||||
webkitdirectory="true"
|
||||
mozdirectory="true"
|
||||
multiple
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table id="tblProjectList" className="table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="table-header">
|
||||
{this.percents()}% {this.filesLeft()}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ProjectList">
|
||||
{this.state.uploadedList.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from './api/GoogleCloudApi';
|
||||
import LocationsList from './LocationsList';
|
||||
import './googleCloud.css';
|
||||
|
||||
export default class LocationPicker extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
locations: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
project: PropTypes.object,
|
||||
onSelect: PropTypes.func,
|
||||
accessToken: PropTypes.string,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setAccessToken(this.props.accessToken);
|
||||
|
||||
const response = await api.loadLocations(this.props.project.projectId);
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
locations: response.data.locations || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { locations, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<LocationsList
|
||||
locations={locations}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
class LocationsList extends Component {
|
||||
state = {
|
||||
search: '',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
locations: PropTypes.array,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
onSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
renderTableRow = location => {
|
||||
return (
|
||||
<tr
|
||||
key={location.locationId}
|
||||
className={
|
||||
this.state.highlightedItem === location.locationId
|
||||
? 'noselect active'
|
||||
: 'noselect'
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
this.onHighlightItem(location.locationId);
|
||||
}}
|
||||
onClick={() => {
|
||||
this.props.onSelect(location);
|
||||
}}
|
||||
>
|
||||
<td>{location.name.split('/')[3]}</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
onHighlightItem(locationId) {
|
||||
this.setState({ highlightedItem: locationId });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="LocationList">
|
||||
{this.props.locations.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblLocationList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('Location')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.locations && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(LocationsList);
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import api from './api/GoogleCloudApi';
|
||||
import ProjectsList from './ProjectsList';
|
||||
import './googleCloud.css';
|
||||
|
||||
export default class ProjectPicker extends Component {
|
||||
state = {
|
||||
error: null,
|
||||
loading: true,
|
||||
projects: [],
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
onSelect: PropTypes.func,
|
||||
accessToken: PropTypes.string,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
api.setAccessToken(this.props.accessToken);
|
||||
const response = await api.loadProjects();
|
||||
|
||||
if (response.isError) {
|
||||
this.setState({
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
projects: response.data.projects || [],
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { projects, loading, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<ProjectsList
|
||||
projects={projects}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './googleCloud.css';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
class ProjectsList extends Component {
|
||||
state = {
|
||||
search: '',
|
||||
highlightedItem: null,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
projects: PropTypes.array,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
loading: true,
|
||||
};
|
||||
|
||||
renderTableRow = project => {
|
||||
return (
|
||||
<tr
|
||||
key={project.projectId}
|
||||
className={
|
||||
this.state.highlightedItem === project.projectId
|
||||
? 'noselect active'
|
||||
: 'noselect'
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
this.onHighlightItem(project.projectId);
|
||||
}}
|
||||
onClick={() => {
|
||||
this.onHighlightItem(project.projectId);
|
||||
this.props.onSelect(project);
|
||||
}}
|
||||
>
|
||||
<td>{project.name}</td>
|
||||
<td>{project.projectId}</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
onHighlightItem(project) {
|
||||
this.setState({ highlightedItem: project });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="ProjectList">
|
||||
{this.props.projects.map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
<table id="tblProjectList" className="gcp-table table noselect">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{this.props.t('Project')}</th>
|
||||
<th>{this.props.t('ID')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.projects && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation('Common')(ProjectsList);
|
||||
@@ -0,0 +1,88 @@
|
||||
import { httpErrorToStr, checkDicomFile } from '../utils/helpers';
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
class DicomUploadService {
|
||||
async smartUpload(files, url, uploadCallback, cancellationToken) {
|
||||
const CHUNK_SIZE = 1; // Only one file per request is supported so far
|
||||
const MAX_PARALLEL_JOBS = 50; // FIXME: tune MAX_PARALLEL_JOBS number
|
||||
|
||||
let filesArray = Array.from(files);
|
||||
if (filesArray.length === 0) {
|
||||
throw new Error('No files were provided.');
|
||||
}
|
||||
|
||||
let parallelJobsCount = Math.min(filesArray.length, MAX_PARALLEL_JOBS);
|
||||
let completed = false;
|
||||
|
||||
const processJob = async (resolve, reject) => {
|
||||
while (filesArray.length > 0) {
|
||||
if (cancellationToken.get()) return;
|
||||
let chunk = filesArray.slice(0, CHUNK_SIZE);
|
||||
filesArray = filesArray.slice(CHUNK_SIZE);
|
||||
let error = null;
|
||||
try {
|
||||
if (chunk.length > 1) throw new Error('Not implemented');
|
||||
if (chunk.length === 1)
|
||||
await this.simpleUpload(chunk[0], url);
|
||||
} catch (err) {
|
||||
// It looks like a stupid bug of Babel that err is not an actual Exception object
|
||||
error = httpErrorToStr(err);
|
||||
}
|
||||
chunk.forEach(file => uploadCallback(file.fileId, error));
|
||||
if (!completed && filesArray.length === 0) {
|
||||
completed = true;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await new Promise(resolve => {
|
||||
for (let i = 0; i < parallelJobsCount; i++) {
|
||||
processJob(resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async simpleUpload(file, url) {
|
||||
const client = this.getClient(url);
|
||||
const loadedFile = await this.readFile(file);
|
||||
const content = loadedFile.content;
|
||||
if (!checkDicomFile(content))
|
||||
throw new Error('This is not a valid DICOM file.');
|
||||
|
||||
await client.storeInstances({ datasets: [content] });
|
||||
}
|
||||
|
||||
readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
resolve({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
content: reader.result,
|
||||
});
|
||||
};
|
||||
reader.onerror = error => reject(error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
setRetrieveAuthHeaderFunction(func) {
|
||||
this.retrieveAuthHeaderFunc = func;
|
||||
}
|
||||
|
||||
getClient(url) {
|
||||
const headers = this.retrieveAuthHeaderFunc();
|
||||
|
||||
// TODO: a bit weird we are creating a new dicomweb client instance for every upload
|
||||
return new api.DICOMwebClient({
|
||||
url,
|
||||
headers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new DicomUploadService();
|
||||
@@ -0,0 +1,94 @@
|
||||
class GoogleCloudApi {
|
||||
setAccessToken(accessToken) {
|
||||
if (!accessToken) console.error('Access token is empty');
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
get fetchConfig() {
|
||||
if (!this.accessToken) throw new Error('OIDC access_token is not set');
|
||||
return {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + this.accessToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get urlBase() {
|
||||
return `https://healthcare.googleapis.com/v1beta1`;
|
||||
}
|
||||
|
||||
get urlBaseProject() {
|
||||
return this.urlBase + `/projects`;
|
||||
}
|
||||
|
||||
async doRequest(urlStr, config = {}, params = {}) {
|
||||
const url = new URL(urlStr);
|
||||
let data = null;
|
||||
url.search = new URLSearchParams(params);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { ...this.fetchConfig, config });
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (err) {}
|
||||
if (response.status >= 200 && response.status < 300 && data != null) {
|
||||
if (data.nextPageToken != null) {
|
||||
params.pageToken = data.nextPageToken;
|
||||
let subPage = await this.doRequest(urlStr, config, params);
|
||||
for (let key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
data[key] = data[key].concat(subPage.data[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
isError: false,
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
isError: true,
|
||||
status: response.status,
|
||||
message:
|
||||
(data && data.error && data.error.message) || 'Unknown error',
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
if (data && data.error) {
|
||||
return {
|
||||
isError: true,
|
||||
status: err.status,
|
||||
message: err.response.data.error.message || 'Unspecified error',
|
||||
};
|
||||
}
|
||||
return {
|
||||
isError: true,
|
||||
message: (err && err.message) || 'Oops! Something went wrong',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async loadProjects() {
|
||||
return this.doRequest(
|
||||
'https://cloudresourcemanager.googleapis.com/v1/projects'
|
||||
);
|
||||
}
|
||||
|
||||
async loadLocations(projectId) {
|
||||
return this.doRequest(`${this.urlBaseProject}/${projectId}/locations`);
|
||||
}
|
||||
|
||||
async loadDatasets(projectId, locationId) {
|
||||
return this.doRequest(
|
||||
`${this.urlBaseProject}/${projectId}/locations/${locationId}/datasets`
|
||||
);
|
||||
}
|
||||
|
||||
async loadDicomStores(dataset) {
|
||||
return this.doRequest(`${this.urlBase}/${dataset}/dicomStores`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new GoogleCloudApi();
|
||||
@@ -0,0 +1,143 @@
|
||||
.loading-icon-spin {
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
font-size: 30px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.gcp-table {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.gcp-table > tr {
|
||||
height: 20px;
|
||||
}
|
||||
.gcp-table > thead {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gcp-table > thead tr th {
|
||||
padding: 0;
|
||||
border-bottom: 1px solid var(--ui-border-color-active);
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-top: 0;
|
||||
}
|
||||
.gcp-table > tbody tr {
|
||||
padding: 5px;
|
||||
background-color: var(--ui-gray-darker);
|
||||
}
|
||||
.gcp-table > tbody tr:nth-child(even) {
|
||||
background-color: var(--ui-gray-dark);
|
||||
}
|
||||
|
||||
.gcp-table > tbody tr td {
|
||||
padding: 8px;
|
||||
height: $body-cell-height;
|
||||
line-height: $body-cell-height;
|
||||
color: var(--table-text-primary-color);
|
||||
font-weight: 300;
|
||||
border-top: 1px solid var(--ui-gray-lighter);
|
||||
border-bottom: 1px solid var(--ui-gray-lighter);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gcp-table > tbody tr td.emptyCell {
|
||||
color: #516873;
|
||||
}
|
||||
.gcp-table > tbody tr td.emptyValue {
|
||||
color: var(--ui-gray-light);
|
||||
}
|
||||
.gcp-table > tbody tr:hover,
|
||||
.gcp-table > tbody tr:active,
|
||||
.gcp-table > tbody tr.active {
|
||||
background-color: var(--table-hover-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
.gcp-table > tbody tr:hover td,
|
||||
.gcp-table > tbody tr:active td,
|
||||
.gcp-table > tbody tr.active td {
|
||||
border-top: 1px solid var(--ui-gray-lighter);
|
||||
border-bottom: 1px solid var(--ui-gray-lighter);
|
||||
background-color: var(--table-hover-color);
|
||||
}
|
||||
|
||||
.gcp-files-selector {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.gcp-files-selector__btn-dir,
|
||||
.gcp-files-selector__btn-file {
|
||||
height: 80px !important;
|
||||
width: 228px !important;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.gcp-files-selector__btn-dir {
|
||||
background-image: url('/assets/Button_Folder.svg');
|
||||
}
|
||||
.gcp-files-selector__btn-file {
|
||||
background-image: url('/assets/Button_File.svg');
|
||||
}
|
||||
|
||||
.gcp-window {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.gcp-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.gcp-picker {
|
||||
box-sizing: border-box;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
width: 650px;
|
||||
height: 600px;
|
||||
background-color: #161a1f;
|
||||
padding: 20px 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gcp-dicom-picker__exit {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
filter: opacity(0.5);
|
||||
}
|
||||
|
||||
.gcp-dicom-picker__exit:hover {
|
||||
filter: opacity(1);
|
||||
}
|
||||
|
||||
.gcp-picker--btn {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.gcp-picker--title {
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.gcp-picker--path {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 28px;
|
||||
text-align: left;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export default class CancellationToken {
|
||||
constructor() {
|
||||
this.cancelled = false;
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.cancelled;
|
||||
}
|
||||
|
||||
set(value) {
|
||||
this.cancelled = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export function formatFileSize(size) {
|
||||
if (size === 0) return '0 B';
|
||||
const n = Math.floor(Math.log(size) / Math.log(1024));
|
||||
return (
|
||||
(size / Math.pow(1024, n)).toFixed(2) * 1 +
|
||||
' ' +
|
||||
['B', 'kB', 'MB', 'GB', 'TB'][n]
|
||||
);
|
||||
}
|
||||
|
||||
export function httpErrorToStr(error) {
|
||||
if (!error) return 'Unknown error';
|
||||
if (error.response) {
|
||||
try {
|
||||
const jsonResponse = JSON.parse(error.response);
|
||||
if (
|
||||
jsonResponse.error &&
|
||||
jsonResponse.error.code &&
|
||||
jsonResponse.error.message
|
||||
)
|
||||
return jsonResponse.error.code + ' - ' + jsonResponse.error.message;
|
||||
} catch (err) {
|
||||
return error.response;
|
||||
}
|
||||
}
|
||||
return error.message || 'Unknown error.';
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
export function checkDicomFile(arrayBuffer) {
|
||||
if (arrayBuffer.length <= 132) return false;
|
||||
const arr = new Uint8Array(arrayBuffer.slice(128, 132));
|
||||
// bytes from 128 to 132 must be "DICM"
|
||||
return Array.from('DICM').every((char, i) => char.charCodeAt(0) === arr[i]);
|
||||
}
|
||||
@@ -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,20 @@
|
||||
/**
|
||||
* Entry point index.js for UMD packaging
|
||||
*/
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App.js';
|
||||
|
||||
function installViewer(props, containerId = 'root', callback) {
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (!container) {
|
||||
throw new Error(
|
||||
"No root element found to install viewer. Please add a <div> with the id 'root', or pass a DOM element into the installViewer function."
|
||||
);
|
||||
}
|
||||
|
||||
return ReactDOM.render(<App {...props} />, container, callback);
|
||||
}
|
||||
|
||||
export { App, installViewer };
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Entry point for development and production PWA builds.
|
||||
* Packaged (NPM) builds go through `index-umd.js`
|
||||
*/
|
||||
import App from './App.js';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
/**
|
||||
* EXTENSIONS
|
||||
* =================
|
||||
*
|
||||
* Importing and modifying the extensions our app uses HERE allows us to leverage
|
||||
* tree shaking and a few other niceties. However, by including them here they become
|
||||
* "baked in" to the published application.
|
||||
*
|
||||
* Depending on your use case/needs, you may want to consider not adding any extensions
|
||||
* by default HERE, and instead provide them via the configuration specified at
|
||||
* `window.config.extensions`, or by using the exported `App` component, and passing
|
||||
* in your extensions as props.
|
||||
*/
|
||||
import OHIFVTKExtension from '@ohif/extension-vtk';
|
||||
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
||||
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
||||
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
||||
|
||||
// Default Settings
|
||||
let config = {};
|
||||
const appDefaults = {
|
||||
routerBasename: '/',
|
||||
relativeWebWorkerScriptsPath: '',
|
||||
};
|
||||
|
||||
console.log(OHIFVTKExtension);
|
||||
|
||||
if (window) {
|
||||
config = window.config || {};
|
||||
config.extensions = [
|
||||
OHIFVTKExtension,
|
||||
OHIFDicomHtmlExtension,
|
||||
OHIFDicomMicroscopyExtension,
|
||||
OHIFDicomPDFExtension,
|
||||
];
|
||||
}
|
||||
|
||||
const appProps = Object.assign({}, appDefaults, config);
|
||||
|
||||
// Create App
|
||||
const app = React.createElement(App, appProps, null);
|
||||
|
||||
// Render
|
||||
ReactDOM.render(app, document.getElementById('root'));
|
||||
@@ -0,0 +1,27 @@
|
||||
import Hammer from 'hammerjs';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneMath from 'cornerstone-math';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
export default function(configuration = {}) {
|
||||
// For debugging
|
||||
window.cornerstoneTools = cornerstoneTools;
|
||||
|
||||
cornerstoneTools.external.cornerstone = cornerstone;
|
||||
cornerstoneTools.external.Hammer = Hammer;
|
||||
cornerstoneTools.external.cornerstoneMath = cornerstoneMath;
|
||||
cornerstoneTools.init(configuration);
|
||||
|
||||
// Set the tool font and font size
|
||||
// context.font = "[style] [variant] [weight] [size]/[line height] [font family]";
|
||||
const fontFamily =
|
||||
'Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif';
|
||||
cornerstoneTools.textStyle.setFont(`16px ${fontFamily}`);
|
||||
|
||||
// Tool styles/colors
|
||||
cornerstoneTools.toolStyle.setToolWidth(2);
|
||||
cornerstoneTools.toolColors.setToolColor('rgb(255, 255, 0)');
|
||||
cornerstoneTools.toolColors.setActiveColor('rgb(0, 255, 0)');
|
||||
|
||||
cornerstoneTools.store.state.touchProximity = 40;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
|
||||
function datasetsToStudies(datasets) {
|
||||
const StudyInstanceUIDs = new Set();
|
||||
datasets.forEach(ds => {
|
||||
StudyInstanceUIDs.add(ds.StudyInstanceUID);
|
||||
});
|
||||
|
||||
const studies = [];
|
||||
Array.from(StudyInstanceUIDs).forEach(studyInstanceUid => {
|
||||
const studyDatasets = datasets.filter(
|
||||
ds => ds.StudyInstanceUID === studyInstanceUid
|
||||
);
|
||||
const firstDataset = studyDatasets[0];
|
||||
const study = {
|
||||
studyInstanceUid: firstDataset.StudyInstanceUID,
|
||||
studyDate: firstDataset.StudyDate,
|
||||
studyTime: firstDataset.StudyTime,
|
||||
accessionNumber: firstDataset.AccessionNumber,
|
||||
referringPhysicianName: firstDataset.ReferringPhysicianName,
|
||||
patientName: firstDataset.PatientName,
|
||||
patientId: firstDataset.PatientID,
|
||||
patientBirthdate: firstDataset.PatientBirthDate,
|
||||
patientSex: firstDataset.PatientSex,
|
||||
studyId: firstDataset.StudyID,
|
||||
studyDescription: firstDataset.StudyDescription,
|
||||
//numberOfStudyRelatedSeries: DICOMWeb.getString(study['00201206']),
|
||||
//numberOfStudyRelatedInstances: DICOMWeb.getString(study['00201208']),
|
||||
// modality: DICOMWeb.getString(study['00080060']),
|
||||
// modalitiesInStudy: DICOMWeb.getString(study['00080061']),
|
||||
//modalities:
|
||||
seriesList: [],
|
||||
};
|
||||
|
||||
const SeriesInstanceUIDs = new Set();
|
||||
studyDatasets.forEach(ds => {
|
||||
SeriesInstanceUIDs.add(ds.SeriesInstanceUID);
|
||||
});
|
||||
|
||||
Array.from(SeriesInstanceUIDs).forEach(seriesInstanceUid => {
|
||||
const seriesDatasets = studyDatasets.filter(
|
||||
ds => ds.SeriesInstanceUID === seriesInstanceUid
|
||||
);
|
||||
|
||||
const SOPInstanceUIDs = new Set();
|
||||
seriesDatasets.forEach(ds => {
|
||||
SOPInstanceUIDs.add(ds.SOPInstanceUID);
|
||||
|
||||
study.seriesList.push({
|
||||
seriesInstanceUid: ds.SeriesInstanceUID,
|
||||
seriesDescription: ds.SeriesDescription,
|
||||
seriesNumber: ds.SeriesNumber,
|
||||
instances: [],
|
||||
});
|
||||
});
|
||||
|
||||
Array.from(SOPInstanceUIDs).forEach(sopInstanceUid => {
|
||||
const instance = seriesDatasets.find(
|
||||
a => a.SOPInstanceUID === sopInstanceUid
|
||||
);
|
||||
const series = study.seriesList.find(
|
||||
a => a.seriesInstanceUid === seriesInstanceUid
|
||||
);
|
||||
|
||||
series.instances.push({
|
||||
sopInstanceUid: instance.SOPInstanceUID,
|
||||
sopClassUid: instance.SOPClassUID,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
numberOfFrames: instance.NumberOfFrames,
|
||||
instanceNumber: instance.InstanceNumber,
|
||||
getImageId: () => instance.imageId, // TODO: Change getImageId to check for instance.imageId property first.
|
||||
/*imageType: DICOMWeb.getString(instance['00080008']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
||||
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
||||
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
||||
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
||||
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
||||
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
||||
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
||||
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
|
||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
|
||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
|
||||
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
|
||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||
rescaleType: DICOMWeb.getNumber(instance['00281054']),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: DICOMWeb.getString(instance['00200062']),
|
||||
viewPosition: DICOMWeb.getString(instance['00185101']),
|
||||
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
|
||||
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
|
||||
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
||||
frameTimeVector: parseFloatArray(
|
||||
DICOMWeb.getString(instance['00181065'])
|
||||
),
|
||||
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
|
||||
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
|
||||
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
|
||||
derivationDescription: DICOMWeb.getString(instance['00282111']),
|
||||
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
|
||||
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
|
||||
echoNumber: DICOMWeb.getString(instance['00180086']),
|
||||
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
|
||||
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
|
||||
wadouri: WADOProxy.convertURL(wadouri, server),
|
||||
wadorsuri: WADOProxy.convertURL(wadorsuri, server),*/
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
studies.push(study);
|
||||
});
|
||||
|
||||
console.warn(studies);
|
||||
|
||||
return studies;
|
||||
}
|
||||
|
||||
export default async function filesToStudies(files) {
|
||||
const imagePromises = files.map(file => {
|
||||
const imageId = cornerstoneWADOImageLoader.wadouri.fileManager.add(file);
|
||||
return cornerstone
|
||||
.loadAndCacheImage(imageId)
|
||||
.catch(error => console.warn(error));
|
||||
});
|
||||
|
||||
const images = await Promise.all(imagePromises);
|
||||
const datasets = [];
|
||||
|
||||
images.forEach(image => {
|
||||
if (!image || !image.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const arrayBuffer = image.data.byteArray.buffer;
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
|
||||
dicomData.meta
|
||||
);
|
||||
dataset.imageId = image.imageId;
|
||||
|
||||
datasets.push(dataset);
|
||||
});
|
||||
|
||||
return datasetsToStudies(datasets);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData';
|
||||
|
||||
export default function getMeasurementLocationCallback(
|
||||
eventData,
|
||||
tool,
|
||||
options
|
||||
) {
|
||||
const { toolType } = tool;
|
||||
const { element } = eventData;
|
||||
const doneCallback = updateTableWithNewMeasurementData;
|
||||
|
||||
const ToolInstance = cornerstoneTools.getToolForElement(element, toolType);
|
||||
|
||||
if (
|
||||
!ToolInstance ||
|
||||
!ToolInstance.configuration ||
|
||||
!ToolInstance.configuration.getMeasurementLocationCallback
|
||||
) {
|
||||
console.warn(
|
||||
'Tool instance configuration is missing: getMeasurementLocationCallback'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ToolInstance.configuration.getMeasurementLocationCallback(
|
||||
tool,
|
||||
eventData,
|
||||
doneCallback,
|
||||
options
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import OHIF from "@ohif/core";
|
||||
import cornerstone from "cornerstone-core";
|
||||
|
||||
export default function updateTableWithNewMeasurementData({
|
||||
toolType,
|
||||
measurementNumber,
|
||||
location,
|
||||
description
|
||||
}) {
|
||||
// Update all measurements by measurement number
|
||||
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
|
||||
const measurements = measurementApi.tools[toolType].filter(
|
||||
m => m.measurementNumber === measurementNumber
|
||||
);
|
||||
|
||||
measurements.forEach(measurement => {
|
||||
measurement.location = location;
|
||||
measurement.description = description;
|
||||
|
||||
measurementApi.updateMeasurement(measurement.toolType, measurement);
|
||||
});
|
||||
|
||||
measurementApi.syncMeasurementsAndToolData();
|
||||
|
||||
// Update images in all active viewports
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export default function bounding(elementRef, currentPosition = {}) {
|
||||
if (!elementRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentElement = elementRef.current;
|
||||
const {
|
||||
offsetParent,
|
||||
offsetTop,
|
||||
offsetHeight,
|
||||
offsetLeft,
|
||||
offsetWidth,
|
||||
} = currentElement;
|
||||
let top = currentPosition.top || offsetTop;
|
||||
let left = currentPosition.left || offsetLeft;
|
||||
|
||||
if (!offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
let maxHeight = `${offsetParent.offsetHeight}px`;
|
||||
|
||||
if (offsetHeight + top > offsetParent.offsetHeight) {
|
||||
top = top - (offsetHeight + top - offsetParent.offsetHeight);
|
||||
if (top < 0) {
|
||||
top = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (left + offsetWidth > offsetParent.offsetWidth) {
|
||||
left = offsetParent.offsetWidth - offsetWidth;
|
||||
if (left < 0) {
|
||||
left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (maxHeight && currentElement.style.maxHeight !== maxHeight) {
|
||||
currentElement.style.maxHeight = maxHeight;
|
||||
}
|
||||
if (currentElement.style.top !== `${top}px`) {
|
||||
currentElement.style.top = `${top}px`;
|
||||
}
|
||||
if (currentElement.style.left !== `${left}px`) {
|
||||
currentElement.style.left = `${left}px`;
|
||||
}
|
||||
}
|
||||
@@ -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,61 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import queryString from 'query-string';
|
||||
import ConnectedViewerRetrieveStudyData from '../connectedComponents/ConnectedViewerRetrieveStudyData.js';
|
||||
|
||||
function decodeStudyUids(studyUids) {
|
||||
const decodedData = window.atob(studyUids);
|
||||
|
||||
return decodedData.split(';');
|
||||
}
|
||||
|
||||
function getQueryParameters(location) {
|
||||
if (location) {
|
||||
return queryString.parse(location.search);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function IHEInvokeImageDisplay({ location }) {
|
||||
const {
|
||||
// patientID,
|
||||
requestType,
|
||||
studyUID,
|
||||
} = getQueryParameters(location);
|
||||
|
||||
switch (requestType) {
|
||||
case 'STUDY':
|
||||
return (
|
||||
<ConnectedViewerRetrieveStudyData
|
||||
studyInstanceUids={studyUID.split(';')}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'STUDYBASE64':
|
||||
return (
|
||||
<ConnectedViewerRetrieveStudyData
|
||||
studyInstanceUids={decodeStudyUids(studyUID)}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'PATIENT':
|
||||
// TODO: connect this to the StudyList when we have the filter parameters set up
|
||||
// return <StudyList patientUids={patientID.split(';')} />;
|
||||
return '';
|
||||
|
||||
default:
|
||||
// TODO: Figure out what to do here, this won't work because StudyList expects studies
|
||||
// return <StudyList />;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
IHEInvokeImageDisplay.propTypes = {
|
||||
location: PropTypes.shape({
|
||||
search: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default withRouter(IHEInvokeImageDisplay);
|
||||
@@ -0,0 +1,9 @@
|
||||
.not-found {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import './NotFound.css';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className={'not-found'}>
|
||||
<div>
|
||||
<h4>Sorry, this page does not exist.</h4>
|
||||
<h5>
|
||||
<Link to={'/'}>Go back to the Study List</Link>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import React, { Component } from "react";
|
||||
import { log, metadata, studies, utils } from "@ohif/core";
|
||||
|
||||
import PropTypes from "prop-types";
|
||||
import Viewer from "../connectedComponents/Viewer";
|
||||
import { extensionManager } from "./../App.js";
|
||||
import qs from "querystring";
|
||||
|
||||
const { OHIFStudyMetadata } = metadata;
|
||||
const { retrieveStudiesMetadata } = studies;
|
||||
const { studyMetadataManager, updateMetaDataManager } = 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 => {
|
||||
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) {
|
||||
log.warn("Response was undefined");
|
||||
reject(new Error("Response was undefined"));
|
||||
}
|
||||
|
||||
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 = [];
|
||||
|
||||
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
|
||||
log.info(`Sending Request to: ${url}`);
|
||||
oReq.open("GET", url);
|
||||
oReq.setRequestHeader("Accept", "application/json");
|
||||
|
||||
// Fire the request to the server
|
||||
oReq.send();
|
||||
});
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
try {
|
||||
let { search } = this.props.location;
|
||||
|
||||
// Remove ? prefix which is included for some reason
|
||||
search = search.slice(1, search.length);
|
||||
|
||||
const query = qs.parse(search);
|
||||
const studies = await StandaloneRouting.parseQueryAndFetchStudies(query);
|
||||
|
||||
studyMetadataManager.purge();
|
||||
|
||||
// Map studies to new format, update metadata manager?
|
||||
const updatedStudies = studies.map(study => {
|
||||
const studyMetadata = new OHIFStudyMetadata(
|
||||
study,
|
||||
study.studyInstanceUid
|
||||
);
|
||||
const sopClassHandlerModules =
|
||||
extensionManager.modules["sopClassHandlerModule"];
|
||||
|
||||
study.displaySets =
|
||||
study.displaySets ||
|
||||
studyMetadata.createDisplaySets(sopClassHandlerModules);
|
||||
studyMetadata.setDisplaySets(study.displaySets);
|
||||
|
||||
// Updates WADO-RS metaDataManager
|
||||
updateMetaDataManager(study);
|
||||
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
|
||||
return study;
|
||||
});
|
||||
|
||||
this.setState({ studies: updatedStudies });
|
||||
} catch (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;
|
||||
Loaded 100 of 919 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user