Re-add extensions as top level dir
This commit is contained in:
1 parent
77ec71a12a
commit
780f84af89
86 files changed
+51115
No files matched your search
@@ -0,0 +1,36 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { ToolbarSection } from 'react-viewerbase';
|
||||
import OHIF from 'ohif-core'
|
||||
|
||||
const { setToolActive } = OHIF.redux.actions;
|
||||
const Icons = 'icons.svg';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const activeButton = state.tools.buttons.find(tool => tool.active === true);
|
||||
|
||||
return {
|
||||
buttons: [ {
|
||||
command: 'Rotate',
|
||||
type: 'tool',
|
||||
text: 'Rotate',
|
||||
svgUrl: `${Icons}#3d-rotate`,
|
||||
active: true
|
||||
}],
|
||||
activeCommand: 'Rotate'
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setToolActive: tool => {
|
||||
//dispatch(setToolActive(tool.command))
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedToolbarSection = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ToolbarSection);
|
||||
|
||||
export default ConnectedToolbarSection;
|
||||
@@ -0,0 +1,61 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { VTKMPRViewport } from 'react-vtkjs-viewport';
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
const {
|
||||
setViewportActive,
|
||||
setViewportSpecificData,
|
||||
clearViewportSpecificData
|
||||
} = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const activeButton = state.tools.buttons.find(tool => tool.active === true);
|
||||
let dataFromStore;
|
||||
|
||||
if (state.extensions && state.extensions.vtk) {
|
||||
dataFromStore = state.extensions.vtk;
|
||||
}
|
||||
|
||||
// If this is the active viewport, enable prefetching.
|
||||
const { viewportIndex } = ownProps; //.viewportData;
|
||||
const isActive = viewportIndex === state.viewports.activeViewportIndex;
|
||||
const viewportSpecificData =
|
||||
state.viewports.viewportSpecificData[viewportIndex] || {};
|
||||
|
||||
const viewportLayout = state.viewports.layout.viewports[viewportIndex];
|
||||
const pluginDetails = viewportLayout.vtk || {};
|
||||
|
||||
return {
|
||||
layout: state.viewports.layout,
|
||||
isActive,
|
||||
...pluginDetails,
|
||||
activeTool: activeButton && activeButton.command,
|
||||
...dataFromStore,
|
||||
enableStackPrefetch: isActive,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
const { viewportIndex } = ownProps;
|
||||
|
||||
return {
|
||||
setViewportActive: () => {
|
||||
dispatch(setViewportActive(viewportIndex));
|
||||
},
|
||||
|
||||
setViewportSpecificData: data => {
|
||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
||||
},
|
||||
|
||||
clearViewportSpecificData: () => {
|
||||
dispatch(clearViewportSpecificData(viewportIndex));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedVTKViewport = connect(
|
||||
mapStateToProps,
|
||||
//mapDispatchToProps
|
||||
)(VTKMPRViewport);
|
||||
|
||||
export default ConnectedVTKViewport;
|
||||
@@ -0,0 +1,17 @@
|
||||
.imageViewerLoadingIndicator {
|
||||
color: #91B9CD;
|
||||
}
|
||||
|
||||
.loadingIndicator {
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 18px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
/* Necessary for click-through to cornerstone element below */
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './LoadingIndicator.css';
|
||||
|
||||
class LoadingIndicator extends PureComponent {
|
||||
static propTypes = {
|
||||
percentComplete: PropTypes.number.isRequired,
|
||||
error: PropTypes.object
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
percentComplete: 0,
|
||||
error: null
|
||||
};
|
||||
|
||||
render() {
|
||||
let percComplete;
|
||||
if (this.props.percentComplete && this.props.percentComplete !== 100) {
|
||||
percComplete = `${this.props.percentComplete}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.props.error ? (
|
||||
<div className="imageViewerErrorLoadingIndicator loadingIndicator">
|
||||
<div className="indicatorContents">
|
||||
<h4>Error Loading Image</h4>
|
||||
<p className="description">An error has occurred.</p>
|
||||
<p className="details">{this.props.error.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
||||
<div className="indicatorContents">
|
||||
<p>
|
||||
Loading... <i className="fa fa-spin fa-circle-o-notch fa-fw" />{' '}
|
||||
{percComplete}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LoadingIndicator;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { Component } from 'react';
|
||||
import OHIFVTKViewport from './OHIFVTKViewport.js';
|
||||
import ToolbarModule from './ToolbarModule.js';
|
||||
|
||||
/**
|
||||
* Pass in 'children' to a React component. The purpose of this is to
|
||||
* allow end users of this extension to pass in components which will
|
||||
* be rendered on top of the base components.
|
||||
*
|
||||
* @param WrappedComponent
|
||||
* @param children
|
||||
* @return {function(*): *}
|
||||
*/
|
||||
function withChildren(WrappedComponent, children) {
|
||||
return function(props) {
|
||||
return <WrappedComponent children={children} { ...props } />;
|
||||
};
|
||||
}
|
||||
|
||||
// Note: If you are authoring extensions which use stateful libraries (e.g. cornerstone-core, react-redux) as peerDependencies and are also duplicated at the application level, try using 'yalc' to link it to the application, rather than yarn link. This can help fix 'module not found' issues.
|
||||
// https://github.com/whitecolor/yalc
|
||||
|
||||
export default class OHIFVTKExtension {
|
||||
constructor(children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension ID is a unique id, might be used for namespacing extension specific redux actions/reducers (?)
|
||||
*/
|
||||
getExtensionId() {
|
||||
return 'vtk';
|
||||
}
|
||||
|
||||
getViewportModule() {
|
||||
if (this.children && this.children.viewport) {
|
||||
return withChildren(OHIFVTKViewport, this.children.viewport);
|
||||
}
|
||||
|
||||
return OHIFVTKViewport;
|
||||
}
|
||||
|
||||
getSopClassHandler() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getPanelModule() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getToolbarModule() {
|
||||
return ToolbarModule;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF from 'ohif-core';
|
||||
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
|
||||
import LoadingIndicator from './LoadingIndicator.js';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
// Metadata configuration
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
const SOP_CLASSES = {
|
||||
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4'
|
||||
};
|
||||
|
||||
const specialCaseHandlers = {};
|
||||
specialCaseHandlers[
|
||||
SOP_CLASSES.SEGMENTATION_STORAGE
|
||||
] = handleSegmentationStorage;
|
||||
|
||||
class OHIFVTKViewport extends Component {
|
||||
state = {
|
||||
viewportData: null
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
studies: PropTypes.object,
|
||||
displaySet: PropTypes.object,
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
static id = 'OHIFCornerstoneViewport';
|
||||
|
||||
static init() {
|
||||
console.log('OHIFCornerstoneViewport init()');
|
||||
}
|
||||
|
||||
static destroy() {
|
||||
console.log('OHIFCornerstoneViewport destroy()');
|
||||
StackManager.clearStacks();
|
||||
}
|
||||
|
||||
static getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
) {
|
||||
// Create shortcut to displaySet
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
});
|
||||
|
||||
// Get stack from Stack Manager
|
||||
const storedStack = StackManager.findOrCreateStack(study, displaySet);
|
||||
|
||||
// Clone the stack here so we don't mutate it
|
||||
const stack = Object.assign({}, storedStack);
|
||||
|
||||
if (frameIndex !== undefined) {
|
||||
stack.currentImageIdIndex = frameIndex;
|
||||
} else if (sopInstanceUid) {
|
||||
const index = stack.imageIds.findIndex(imageId => {
|
||||
const sopCommonModule = cornerstone.metaData.get(
|
||||
'sopCommonModule',
|
||||
imageId
|
||||
);
|
||||
if (!sopCommonModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
return sopCommonModule.sopInstanceUID === sopInstanceUid;
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
stack.currentImageIdIndex = index;
|
||||
}
|
||||
} else {
|
||||
stack.currentImageIdIndex = 0;
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
static getViewportData = (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
) => {
|
||||
return OHIFVTKViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
};
|
||||
|
||||
getViewportData = (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stack = OHIFVTKViewport.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
let imageDataObject;
|
||||
let labelmapDataObject;
|
||||
let doneLoadingCallback;
|
||||
let callbacks;
|
||||
|
||||
switch (sopClassUid) {
|
||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||
throw new Error("Not yet implemented");
|
||||
|
||||
const data = handleSegmentationStorage(stack.imageIds, displaySetInstanceUid, cornerstone);
|
||||
imageDataObject = data.referenceDataObject;
|
||||
labelmapDataObject = data.labelmapDataObject;
|
||||
|
||||
doneLoadingCallback = () => {
|
||||
resolve({
|
||||
data: imageDataObject.vtkImageData,
|
||||
labelmap: labelmapDataObject
|
||||
});
|
||||
};
|
||||
|
||||
callbacks = [
|
||||
doneLoadingCallback
|
||||
];
|
||||
|
||||
loadImageData(imageDataObject, callbacks, cornerstone);
|
||||
|
||||
break;
|
||||
default:
|
||||
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid, cornerstone);
|
||||
doneLoadingCallback = () => {
|
||||
resolve({
|
||||
data: imageDataObject.vtkImageData,
|
||||
});
|
||||
};
|
||||
|
||||
callbacks = [
|
||||
doneLoadingCallback
|
||||
];
|
||||
|
||||
loadImageData(imageDataObject, callbacks, cornerstone);
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setStateFromProps() {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUids,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
} = displaySet;
|
||||
|
||||
if (sopClassUids.length > 1) {
|
||||
console.warn(
|
||||
'More than one SOPClassUid in the same series is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
const sopClassUid = sopClassUids[0];
|
||||
|
||||
this.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
).then(({ data, labelmap })=> {
|
||||
this.setState({
|
||||
viewportData: data,
|
||||
labelmap
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setStateFromProps();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUid !==
|
||||
prevDisplaySet.displaySetInstanceUid ||
|
||||
displaySet.sopInstanceUid !== prevDisplaySet.sopInstanceUid ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
this.setStateFromProps();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let childrenWithProps = null;
|
||||
|
||||
// TODO: Does it make more sense to use Context?
|
||||
if (this.props.children && this.props.children.length) {
|
||||
childrenWithProps = this.props.children.map((child, index) => {
|
||||
return React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.state.viewportData ? (
|
||||
<ConnectedVTKViewport
|
||||
data={this.state.viewportData}
|
||||
labelmap={this.state.labelmap}
|
||||
viewportIndex={this.props.viewportIndex}
|
||||
/>
|
||||
) : <div style={style}>
|
||||
<LoadingIndicator/>
|
||||
</div>}
|
||||
{childrenWithProps}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OHIFVTKViewport;
|
||||
@@ -0,0 +1,15 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ConnectedToolbarSection from './ConnectedToolbarSection';
|
||||
|
||||
class ToolbarModule extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="ToolbarModule">
|
||||
<ConnectedToolbarSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToolbarModule;
|
||||
@@ -0,0 +1,121 @@
|
||||
import OHIF from 'ohif-core';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
});
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
function getDisplaySetsBySeries(studies, studyInstanceUid, seriesInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
return study.displaySets.filter(set => {
|
||||
return set.seriesInstanceUid === seriesInstanceUid;
|
||||
});
|
||||
}
|
||||
|
||||
function getCornerstoneStack(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
// Create shortcut to displaySet
|
||||
const displaySet = getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
|
||||
// Get stack from Stack Manager
|
||||
const stack = StackManager.findOrCreateStack(study, displaySet);
|
||||
|
||||
// Clone the stack here so we don't mutate it later
|
||||
const stackClone = Object.assign({}, stack);
|
||||
stackClone.currentImageIdIndex = 0;
|
||||
|
||||
return stackClone;
|
||||
}
|
||||
|
||||
function retrieveDicomData(wadoUri) {
|
||||
// TODO: Authorization header depends on the server. If we ever have multiple servers
|
||||
// we will need to figure out how / when to pass this information in.
|
||||
return fetch(wadoUri, {
|
||||
headers: OHIF.DICOMWeb.getAuthorizationHeader()
|
||||
}).then(response => response.arrayBuffer());
|
||||
}
|
||||
|
||||
async function handleSegmentationStorage(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
const displaySet = getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
const segWadoUri = displaySet.images[0].getData().wadouri;
|
||||
const arrayBuffer = await retrieveDicomData(segWadoUri);
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(arrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
|
||||
const segments = dcmjs.adapters.VTKjs.Segmentation.generateSegments(dataset);
|
||||
|
||||
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
|
||||
const seriesInstanceUid = dataset.ReferencedSeriesSequence.SeriesInstanceUID;
|
||||
const displaySets = getDisplaySetsBySeries(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid
|
||||
);
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
'More than one display set with the same seriesInstanceUid. This is not supported yet...'
|
||||
);
|
||||
}
|
||||
|
||||
const referenceDisplaySet = displaySets[0];
|
||||
const imageIds = referenceDisplaySet.images.map(image => image.getImageId());
|
||||
|
||||
if (!results) {
|
||||
throw new Error('Fractional segmentations are not supported');
|
||||
}
|
||||
|
||||
const cachedStack = StackManager.findOrCreateStack(
|
||||
study,
|
||||
referenceDisplaySet
|
||||
);
|
||||
const stack = Object.assign({}, cachedStack);
|
||||
stack.currentImageIdIndex = 0;
|
||||
|
||||
return {
|
||||
referenceDataObject,
|
||||
labelmapDataObject
|
||||
}
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
stack
|
||||
};
|
||||
}
|
||||
|
||||
export default handleSegmentationStorage;
|
||||
@@ -0,0 +1,3 @@
|
||||
import OHIFVTKExtension from './OHIFVTKExtension.js';
|
||||
|
||||
export default OHIFVTKExtension;
|
||||
Reference in new issue
Block a user