add new viewer folder

This commit is contained in:
Erik Ziegler 2018-12-14 15:23:48 +01:00
parent d541c23117
commit fc5bd73c1e
26 changed files with 23182 additions and 0 deletions

2164
OHIFViewer-react/README.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
{
"name": "react-cornerstone-viewport-example",
"version": "0.0.0",
"license": "MIT",
"private": true,
"dependencies": {
"cornerstone-core": "2.2.8",
"cornerstone-math": "0.1.7",
"cornerstone-tools": "3.0.0-b.1471",
"cornerstone-wado-image-loader": "2.2.3",
"dicom-parser": "1.8.3",
"hammerjs": "^2.0.8",
"lodash.debounce": "^4.0.8",
"moment": "^2.22.2",
"prop-types": "^15.6.2",
"react": "^16.4.1",
"react-cornerstone-viewport": "^0.1.1",
"react-dom": "^16.4.1",
"react-scripts": "^2.1.1",
"react-viewerbase": "^0.1.2",
"ohif-core": "link:/Users/Erik/Projects/OHIF/ohif-core"
},
"scripts": {
"copy:webworkers": "cp node_modules/cornerstone-wado-image-loader/dist/*.min.js* public",
"version": "node -p -e \"'export default \\'' + require('./package.json').version + '\\';'\" > src/version.js",
"sha": "echo \"export default '\"$(git rev-parse HEAD)\"';\" > src/sha.js",
"details": "npm run version && npm run sha",
"prep": "npm run details && npm run copy:webworkers",
"start": "npm run prep && react-scripts start",
"build": "npm run prep && react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<title>react-cornerstone-viewport</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,8 @@
{
"short_name": "react-cornerstone-viewport",
"name": "react-cornerstone-viewport",
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,16 @@
html,
body,
#root {
background-color: black;
height: 100%;
width: 100%;
margin: 0;
}
body {
box-sizing: border-box;
margin: 0;
padding: 0;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -0,0 +1,83 @@
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { Route, Switch } from 'react-router-dom';
import { OHIF } from 'ohif-core';
import ViewerRouting from "./ViewerRouting.js";
import IHEInvokeImageDisplay from './IHEInvokeImageDisplay.js';
import './App.css';
const reload = () => window.location.reload();
const Studylist = OHIF.studylist.components.StudyList;
function setContext(context) {
/*Rollbar.configure({
payload: {
context
}
});*/
console.log(context);
}
class App extends Component {
componentDidMount() {
// Temporary until the rest of the UI is in React
window.router = {
history: this.props.history
};
this.unlisten = this.props.history.listen((location, action) => {
setContext(window.location.pathname);
});
}
componentWillUnmount() {
this.unlisten();
}
render() {
return (
<Switch>
<Route
exact
path="/studylist"
component={Studylist}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route
exact
path="/"
component={Studylist}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route
path="/viewer/:studyInstanceUids"
component={ViewerRouting}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route
path="/study/:studyInstanceUid/series/:seriesInstanceUids"
component={ViewerRouting}
/*auth={this.props.auth}*/
store={this.props.store}
/>
<Route
path="/IHEInvokeImageDisplay"
component={IHEInvokeImageDisplay}
/*auth={this.props.auth}*/
store={this.props.store}
/>
{/*<Route path="/silent-refresh.html" onEnter={reload} />
<Route path="/logout-redirect.html" onEnter={reload} />*/}
<Route render={() =>
<div> Sorry, this page does not exist. </div>}
/>
</Switch>
);
}
}
export default withRouter(App);

View File

@ -0,0 +1,33 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import ViewerFromStudyData from "./ViewerFromStudyData.js";
function IHEInvokeImageDisplay({ match }) {
const requestType = match.params.query.requestType;
let studyInstanceUids;
let displayStudyList = false;
if (requestType === "STUDY") {
studyInstanceUids = match.params.query.studyUID.split(';');
} else if (requestType === "STUDYBASE64") {
const uids = this.params.query.studyUID;
const decodedData = window.atob(uids);
studyInstanceUids = decodedData.split(';');
} else if (requestType === "PATIENT") {
const patientUids = this.params.query.patientID.split(';');
displayStudyList = true
} else {
displayStudyList = true
}
if (displayStudyList) {
//return (<StudyList/>);
return ('');
}
return (
<ViewerFromStudyData studyInstanceUids={studyInstanceUids}/>
);
}
export default IHEInvokeImageDisplay;

View File

@ -0,0 +1,102 @@
import React, {Component} from "react";
import PropTypes from "prop-types";
import Viewer from "./viewer/viewer.js";
import { OHIF } from 'ohif-core';
// TODO: Move to react-viewerbase
function createDisplaySets(studies) {
// Define the OHIF.viewer.data global object
// TODO: Save all data that is currently in OHIF.viewer in redux instead
OHIF.viewer.data = OHIF.viewer.data || {};
// @TypeSafeStudies
// Clears OHIF.viewer.Studies collection
OHIF.viewer.Studies.removeAll();
// @TypeSafeStudies
// Clears OHIF.viewer.StudyMetadataList collection
OHIF.viewer.StudyMetadataList.removeAll();
OHIF.viewer.data.studyInstanceUids = [];
const updatedStudies = studies.map(study => {
const studyMetadata = new OHIF.metadata.OHIFStudyMetadata(study, study.studyInstanceUid);
let displaySets = study.displaySets;
if (!study.displaySets) {
displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(studyMetadata);
study.displaySets = displaySets;
}
studyMetadata.setDisplaySets(displaySets);
study.selected = true;
OHIF.viewer.Studies.insert(study);
OHIF.viewer.StudyMetadataList.insert(studyMetadata);
OHIF.viewer.data.studyInstanceUids.push(study.studyInstanceUid);
// Updates WADO-RS metaDataManager
OHIF.viewerbase.updateMetaDataManager(study);
return study;
});
return updatedStudies;
}
class ViewerFromStudyData extends Component {
constructor(props) {
super(props);
this.state = {
studies: null,
error: null
};
}
componentDidMount() {
// TODO: Avoid using timepoints here
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
const params = {
studyInstanceUids: this.props.studyInstanceUids,
seriesInstanceUids: this.props.seriesInstanceUids,
}
const promise = OHIF.viewerbase.prepareViewerData(params);
// Render the viewer when the data is ready
promise.then(({ studies, viewerData }) => {
OHIF.viewer.data = viewerData;
const updatedStudies = createDisplaySets(studies);
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}/>
);
}
}
ViewerFromStudyData.propTypes = {
studyInstanceUids: PropTypes.array.isRequired,
seriesInstanceUids: PropTypes.array
};
export default ViewerFromStudyData;

View File

@ -0,0 +1,34 @@
import React, {Component} from "react";
import PropTypes from "prop-types";
import ViewerFromStudyData from "./ViewerFromStudyData.js";
// TODO: Move to react-viewerbase
function ViewerRouting({ match }) {
const { studyInstanceUids, seriesInstanceUids } = match.params;
let studyUIDs;
let seriesUIDs;
if (studyInstanceUids && !seriesInstanceUids) {
studyUIDs = studyInstanceUids.split(';');
} else if (studyInstanceUids && seriesInstanceUids) {
studyUIDs = [match.params.studyInstanceUid];
seriesUIDs = match.params.seriesInstanceUids.split(';');
}
return (
<ViewerFromStudyData studyInstanceUids={studyUIDs} seriesInstanceUids={seriesUIDs}/>
);
}
ViewerRouting.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
studyInstanceUids: PropTypes.string.isRequired,
seriesInstanceUids: PropTypes.string
})
})
};
export default ViewerRouting;

View File

@ -0,0 +1,37 @@
import { OHIF } from 'ohif-core';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import sha from './sha.js';
import version from './version.js';
OHIF.info = {
sha,
version
};
const maxWebWorkers = Math.max(navigator.hardwareConcurrency - 1, 1);
const config = {
maxWebWorkers,
startWebWorkersOnDemand: true,
webWorkerPath: OHIF.utils.absoluteUrl('packages/ohif_cornerstone/public/js/cornerstoneWADOImageLoaderWebWorker.es5.js'),
taskConfiguration: {
decodeTask: {
loadCodecsOnStartup: true,
initializeCodecsOnStartup: false,
codecsPath: OHIF.utils.absoluteUrl('packages/ohif_cornerstone/public/js/cornerstoneWADOImageLoaderCodecs.es5.js'),
usePDFJS: false
}
}
};
cornerstoneWADOImageLoader.webWorkerManager.initialize(config);
cornerstoneWADOImageLoader.configure({
beforeSend: function(xhr) {
const headers = OHIF.DICOMWeb.getAuthorizationHeader();
if (headers.Authorization) {
xhr.setRequestHeader("Authorization", headers.Authorization);
}
}
});

View File

@ -0,0 +1,169 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { OHIF } from 'ohif-core';
import { StudyBrowser } from 'react-viewerbase';
import cornerstone from 'cornerstone-core';
/**
* Asynchronous wrapper around Cornerstone's renderToCanvas method.
*
* @param {HTMLElement} canvasElement An HTML <canvas> element
* @param {Image} image A Cornerstone Image
*
* @return {Promise} A promise tracking the progress of the rendering. Resolves empty.
*/
function renderAsync(canvasElement, image) {
return new Promise((resolve, reject) => {
try {
cornerstone.renderToCanvas(canvasElement, image);
resolve();
} catch(error) {
reject(error);
}
});
}
// Define a handler for success on image load
const loadSuccess = function(image, studyInstanceUid, displaySetInstanceUid) {
// This is an off-screen canvas. It's used to get dataURL images by using
// cornerstone.renderToCanvas function.
const canvasElement = document.createElement('canvas');
canvasElement.width = 193;
canvasElement.height = 123;
// Render the image to canvas to be able to get its dataURL
renderAsync(canvasElement, image).then(() => {
const studies = this.state.studiesForBrowser;
const study = studies.find(study => study.studyInstanceUid === studyInstanceUid);
const thumbnail = study.thumbnails.find(t => t.displaySetInstanceUid === displaySetInstanceUid);
thumbnail.imageSrc = canvasElement.toDataURL('image/jpeg', 1);
this.setState({
studiesForBrowser: studies
});
});
};
// Define a handler for error on image load
const loadError = function(error, studyInstanceUid, displaySetInstanceUid) {
const studies = this.state.studiesForBrowser;
const study = studies.find(study => study.studyInstanceUid === studyInstanceUid);
const thumbnail = study.thumbnails.find(t => t.displaySetInstanceUid === displaySetInstanceUid);
thumbnail.error = error;
this.setState({
studiesForBrowser: studies
});
};
class FlexboxLayout extends Component {
constructor(props) {
super(props);
this.state = {
leftSidebarOpen: true,
rightSidebarOpen: false,
studiesForBrowser: this.getStudiesForBrowser(),
};
this.getStudiesForBrowser = this.getStudiesForBrowser.bind(this);
this.getThumbnailsFromImageIds = this.getThumbnailsFromImageIds.bind(this);
}
componentDidMount() {
this.getThumbnailsFromImageIds();
}
getThumbnailsFromImageIds() {
const studyThumbnails = this.state.studiesForBrowser.forEach(function (study) {
const { studyInstanceUid } = study;
study.thumbnails.forEach(function (displaySet) {
const imageId = displaySet.imageId;
const {displaySetInstanceUid, seriesDescription, seriesNumber, instanceNumber, numImageFrames} = displaySet;
cornerstone.loadAndCacheImage(imageId).then((image) => {
loadSuccess.call(this, image, studyInstanceUid, displaySetInstanceUid);
}, error => {
loadError.call(this, error, studyInstanceUid, displaySetInstanceUid);
});
}, this);
}, this);
}
getStudiesForBrowser() {
// @TypeSafeStudies
const studies = OHIF.viewer.Studies.findAllBy({
selected: true
});
// TODO[react]:
// - Add sorting of display sets
// - Pass in which display set is active from Redux
// - Pass in errors and stack loading progress from Redux
// - Add useMiddleSeriesInstanceAsThumbnail
// - Add showStackLoadingProgressBar option
return studies.map((study) => {
const { studyInstanceUid } = study;
const thumbnails = study.displaySets.map((displaySet) => {
const active = false;
const stackPercentComplete = 0;
const { displaySetInstanceUid, seriesDescription, seriesNumber, instanceNumber, numImageFrames } = displaySet;
const imageId = displaySet.images[0].getImageId();
return {
imageSrc: '',
imageId,
displaySetInstanceUid,
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames,
active,
stackPercentComplete
};
});
return {
studyInstanceUid,
thumbnails
};
});
}
render() {
let mainContentClassName = "mainContent"
if (this.state.leftSidebarOpen) {
mainContentClassName += ' sidebar-left-open';
}
if (this.state.rightSidebarOpen) {
mainContentClassName += ' sidebar-right-open';
}
// TODO[react]: Add measurementLightTable
return (
<div className="viewerSection">
<div className={this.state.leftSidebarOpen ? "sidebarMenu sidebar-left sidebar-open" : "sidebarMenu sidebar-left"}>
<StudyBrowser studies={this.state.studiesForBrowser}/>
</div>
<div className={mainContentClassName}>
{/* TODO: Needs to be moved from Meteor package to react-viewerbase: <ViewerMain studies={this.props.studies}/>*/}
</div>
<div className={this.state.rightSidebarOpen ? "sidebarMenu sidebar-right sidebar-open" : "sidebarMenu sidebar-right"}>
{/*{{> measurementLightTable (clone this)}}*/}
</div>
</div>
);
}
}
FlexboxLayout.propTypes = {
studies: PropTypes.array.isRequired
};
export default FlexboxLayout;

View File

@ -0,0 +1,65 @@
.viewerSection
display: flex
flex: 1
flex-flow: row nowrap
align-items: stretch
height: 'calc(100% - %s)' % ($toolbarHeight)
width: 100%
.sidebarMenu
height: 100%
// required transformation to make inner fixed elements relative to this one
transform(scale(1))
transition($sidebarTransition)
.sidebar-option
height: 100%
max-width: inherit
position: absolute
transform(translateX(100%))
transition($sidebarTransition)
width: 100%
&.active
transform(translateX(0%))
.sidebar-left
theme('border-right', '%s solid $uiBorderColor' % $uiBorderThickness)
flex: 1
margin-left: - $studiesSidebarMenuWidth
max-width: $studiesSidebarMenuWidth
order: 1
&.sidebar-open
margin-left: 0
.mainContent
flex: 1
height: 100%
order: 2
overflow: hidden
transition($sidebarTransition)
width: 100%
.sidebar-right
flex: 1
margin-right: - $rightSidebarMenuWidth
max-width: $rightSidebarMenuWidth
order: 3
position: relative
&[data-timepoints="3"]
margin-right: - ($rightSidebarMenuWidth + 135.5px)
max-width: $rightSidebarMenuWidth + 135.5px
&[data-timepoints="4"]
margin-right: - ($rightSidebarMenuWidth + 270px)
max-width: $rightSidebarMenuWidth + 270px
&.sidebar-open
margin-right: 0
.studiesListedChanger
theme('border-bottom', '%s solid $uiBorderColor' % $uiBorderThickness)
padding: 20px 10px
text-align: center

View File

@ -0,0 +1,15 @@
<head>
<meta charset="utf-8">
<title>OHIF Viewer</title>
<meta name="description" content="Open Health Imaging Foundation DICOM Viewer">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1,maximum-scale=1,user-scalable=no">
<link rel="shortcut icon" type="image/png" href="/images/logo.png" sizes="16x16 32x32 64x64">
<!--Best display for mobile devices-->
<meta http-equiv="cleartype" content="on">
<meta name="MobileOptimized" content="320">
<meta name="HandheldFriendly" content="True">
<meta name="apple-mobile-web-app-capable" content="yes">
</head>
<body>
<div id="root"></div>
</body>

View File

@ -0,0 +1,22 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import App from './App.js';
import { OHIF } from 'ohif-core';
import './config';
const store = createStore(OHIF.redux.combinedReducer);
// TODO[react] Use a provider when the whole tree is React
window.store = store;
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App store={store}/>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);

View File

@ -0,0 +1,3 @@
import loglevel from 'loglevel';
log = loglevel.getLogger('OHIFViewer');
log.setLevel('info');

View File

@ -0,0 +1 @@
export default '7b66dec163174eae2f6aca133f740529c8b4d08c';

View File

@ -0,0 +1 @@
export default '0.0.0';

View File

@ -0,0 +1,99 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import { OHIF } from 'ohif-core';
import { CineDialog } from 'react-viewerbase';
import FlexboxLayout from '../flexboxLayout/flexboxLayout.js';
/**
* 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();
// Caches Layout Manager: Hanging Protocol uses it for layout management according to current protocol
const layoutManager = OHIF.viewerbase.layoutManager;
// 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);
});
};*/
OHIF.viewer.defaultTool = {
left: 'wwwc',
right: 'zoom',
middle: 'pan'
};
OHIF.viewer.refLinesEnabled = true;
OHIF.viewer.cine = {
framesPerSecond: 24,
loop: true
};
const viewportUtils = OHIF.viewerbase.viewportUtils;
OHIF.viewer.functionList = {
toggleCineDialog: viewportUtils.toggleCineDialog,
toggleCinePlay: viewportUtils.toggleCinePlay,
clearTools: viewportUtils.clearTools,
resetViewport: viewportUtils.resetViewport,
invert: viewportUtils.invert
};
// Create the synchronizer used to update reference lines
OHIF.viewer.updateImageSynchronizer = new cornerstoneTools.Synchronizer('cornerstonenewimage', cornerstoneTools.updateImageSynchronizer);
OHIF.viewer.metadataProvider = new OHIF.cornerstone.MetadataProvider();
// Metadata configuration
const metadataProvider = OHIF.viewer.metadataProvider;
cornerstone.metaData.addProvider(metadataProvider.provider.bind(metadataProvider));
class Viewer extends Component {
constructor(props) {
super(props);
this.state = {
leftSidebar: 'studies',
rightSidebar: 'measurements',
studies: this.props.studies
};
}
render() {
return (<>
<div className='viewerDialogs'>
{/*<CineDialog/>*/}
</div>
<div id="viewer" className='Viewer'>
{/*<ToolbarSection/>*/}
<FlexboxLayout studies={this.state.studies}/>
</div>
</>
);
}
}
Viewer.propTypes = {
studies: PropTypes.array
};
export default Viewer;

View File

@ -0,0 +1,13 @@
@import "{ohif:viewerbase}/app"
body
background-color: black
#viewer
background-color: black
height: 100%
width: 100%
.loadingTextDiv
theme('color', '$textSecondaryColor')
font-size: 30px

File diff suppressed because it is too large Load Diff

10103
OHIFViewer-react/yarn.lock Normal file

File diff suppressed because it is too large Load Diff