feat(react-measurements): Add measurement table - WIP

This commit is contained in:
Evren Ozkan 2019-01-31 22:01:39 -05:00 committed by Gustavo Lelis
parent e1f1d9b630
commit 84e2f86a76
13 changed files with 458 additions and 130 deletions

View File

@ -110,6 +110,7 @@
"ohif-dicom-html-extension": "^0.0.2", "ohif-dicom-html-extension": "^0.0.2",
"ohif-dicom-microscopy-extension": "^0.0.5", "ohif-dicom-microscopy-extension": "^0.0.5",
"ohif-dicom-pdf-extension": "^0.0.5", "ohif-dicom-pdf-extension": "^0.0.5",
"moment": "^2.24.0",
"oidc-client": "^1.6.1", "oidc-client": "^1.6.1",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"react-bootstrap-modal": "^4.2.0", "react-bootstrap-modal": "^4.2.0",

View File

@ -5,6 +5,7 @@ body,
height: 100%; height: 100%;
width: 100%; width: 100%;
margin: 0; margin: 0;
overflow: hidden;
} }
body { body {

View File

@ -19,11 +19,11 @@ import {
reducer as oidcReducer reducer as oidcReducer
} from 'redux-oidc'; } from 'redux-oidc';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader'; import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import './App.css';
const { ExtensionManager } = OHIF.extensions; const { ExtensionManager } = OHIF.extensions;
//import Icons from "./images/icons.svg" //import Icons from "./images/icons.svg"
const Icons = '/icons.svg'; const Icons = '/icons.svg';
const { reducers, localStorage } = OHIF.redux; const { reducers, localStorage } = OHIF.redux;

View File

@ -0,0 +1,48 @@
import { connect } from 'react-redux';
import { MeasurementTable } from 'react-viewerbase';
import moment from 'moment';
function convertTimepointsToTableData(timepoints) {
if (!timepoints || !timepoints.length) {
return [];
}
return [
{
label: 'Study date:',
date: moment(timepoints[0].latestDate).format('DD-MMM-YY')
}
];
}
function convertMeasurementsToTableData(measurements) {
const tableData = [
{
groupName: 'Measurements',
measurements: []
}
];
if (!measurements || !measurements.length) {
return tableData;
}
// TODO: Add measurements into tableData
return tableData;
}
const mapStateToProps = state => {
const { timepoints, measurements } = state;
return {
timepoints: convertTimepointsToTableData(timepoints.timepoints),
measurementCollection: convertMeasurementsToTableData(measurements)
};
};
const ConnectedMeasurementTable = connect(
mapStateToProps,
null
)(MeasurementTable);
export default ConnectedMeasurementTable;

View File

@ -2,13 +2,14 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ViewerMain from './ViewerMain.js'; import ViewerMain from './ViewerMain.js';
import ConnectedMeasurementTable from './ConnectedMeasurementTable';
import './FlexboxLayout.css'; import './FlexboxLayout.css';
class FlexboxLayout extends Component { class FlexboxLayout extends Component {
static propTypes = { static propTypes = {
studies: PropTypes.array.isRequired, studies: PropTypes.array.isRequired,
leftSidebarOpen: PropTypes.bool.isRequired, leftSidebarOpen: PropTypes.bool.isRequired,
rightSidebarOpen: PropTypes.bool.isRequired, rightSidebarOpen: PropTypes.bool.isRequired
}; };
state = { state = {
@ -30,12 +31,17 @@ class FlexboxLayout extends Component {
// - Add sorting of display sets // - Add sorting of display sets
// - Add useMiddleSeriesInstanceAsThumbnail // - Add useMiddleSeriesInstanceAsThumbnail
// - Add showStackLoadingProgressBar option // - Add showStackLoadingProgressBar option
return studies.map((study) => { return studies.map(study => {
const { studyInstanceUid } = study; const { studyInstanceUid } = study;
const thumbnails = study.displaySets.map((displaySet) => { const thumbnails = study.displaySets.map(displaySet => {
const { displaySetInstanceUid, seriesDescription, seriesNumber, instanceNumber, numImageFrames } = displaySet; const {
displaySetInstanceUid,
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames
} = displaySet;
let imageId; let imageId;
@ -49,7 +55,7 @@ class FlexboxLayout extends Component {
seriesDescription, seriesDescription,
seriesNumber, seriesNumber,
instanceNumber, instanceNumber,
numImageFrames, numImageFrames
}; };
}); });
@ -58,10 +64,10 @@ class FlexboxLayout extends Component {
thumbnails thumbnails
}; };
}); });
} };
render() { render() {
let mainContentClassName = "main-content" let mainContentClassName = 'main-content';
if (this.props.leftSidebarOpen) { if (this.props.leftSidebarOpen) {
mainContentClassName += ' sidebar-left-open'; mainContentClassName += ' sidebar-left-open';
} }
@ -73,15 +79,27 @@ class FlexboxLayout extends Component {
// TODO[react]: Add measurementLightTable // TODO[react]: Add measurementLightTable
return ( return (
<div className="FlexboxLayout"> <div className="FlexboxLayout">
<div className={this.props.leftSidebarOpen ? "sidebar-menu sidebar-left sidebar-open" : "sidebar-menu sidebar-left"}> <div
className={
this.props.leftSidebarOpen
? 'sidebar-menu sidebar-left sidebar-open'
: 'sidebar-menu sidebar-left'
}
>
<ConnectedStudyBrowser studies={this.state.studiesForBrowser} /> <ConnectedStudyBrowser studies={this.state.studiesForBrowser} />
</div> </div>
<div className={mainContentClassName}> <div className={mainContentClassName}>
<ViewerMain studies={this.props.studies} /> <ViewerMain studies={this.props.studies} />
</div> </div>
{/* <div className={this.props.rightSidebarOpen ? "sidebar-menu sidebar-right sidebar-open" : "sidebar-menu sidebar-right"}> <div
{{> measurementLightTable (clone this)}} className={
</div> */} this.props.rightSidebarOpen
? 'sidebar-menu sidebar-right sidebar-open'
: 'sidebar-menu sidebar-right'
}
>
<ConnectedMeasurementTable />
</div>
</div> </div>
); );
} }

View File

@ -1,4 +1,4 @@
import React, { Component } from "react"; import React, { Component } from 'react';
import ConnectedToolbarSection from './ConnectedToolbarSection'; import ConnectedToolbarSection from './ConnectedToolbarSection';
import ConnectedLayoutButton from './ConnectedLayoutButton'; import ConnectedLayoutButton from './ConnectedLayoutButton';
import ConnectedCineDialog from './ConnectedCineDialog.js'; import ConnectedCineDialog from './ConnectedCineDialog.js';
@ -26,34 +26,61 @@ class ToolbarRow extends Component {
rightSidebarOpen: false rightSidebarOpen: false
}; };
onLeftSidebarValueChanged = (value) => { onLeftSidebarValueChanged = value => {
this.props.setLeftSidebarOpen(!!value); this.props.setLeftSidebarOpen(!!value);
} };
onRightSidebarValueChanged = value => {
this.props.setRightSidebarOpen(!!value);
};
onClickCineToolbarButton = () => { onClickCineToolbarButton = () => {
this.setState({ this.setState({
cineDialogOpen: !this.state.cineDialogOpen cineDialogOpen: !this.state.cineDialogOpen
}); });
} };
render() { render() {
const leftSidebarToggle = [{ const leftSidebarToggle = [
{
value: 'studies', value: 'studies',
svgLink: `${Icons}#icon-studies`, svgLink: `${Icons}#icon-studies`,
svgWidth: 15, svgWidth: 15,
svgHeight: 13, svgHeight: 13,
bottomLabel: 'Series', bottomLabel: 'Series'
}]; }
];
const leftSidebarValue = this.props.leftSidebarOpen ? leftSidebarToggle[0].value : null; const rightSidebarToggle = [
{
value: 'measurements',
svgLink: `${Icons}#icon-measurements-lesions`,
svgWidth: 15,
svgHeight: 13,
bottomLabel: 'Measurements'
}
];
const leftSidebarValue = this.props.leftSidebarOpen
? leftSidebarToggle[0].value
: null;
const rightSidebarValue = this.props.rightSidebarOpen
? rightSidebarToggle[0].value
: null;
const cineDialogContainerStyle = { const cineDialogContainerStyle = {
display: this.state.cineDialogOpen ? 'inline-block' : 'none' display: this.state.cineDialogOpen ? 'inline-block' : 'none'
} };
return (<div className="ToolbarRow"> return (
<div className="ToolbarRow">
<div className="pull-left m-t-1 p-y-1" style={{ padding: '10px' }}> <div className="pull-left m-t-1 p-y-1" style={{ padding: '10px' }}>
<RoundedButtonGroup options={leftSidebarToggle} value={leftSidebarValue} onValueChanged={this.onLeftSidebarValueChanged}/> <RoundedButtonGroup
options={leftSidebarToggle}
value={leftSidebarValue}
onValueChanged={this.onLeftSidebarValueChanged}
/>
</div> </div>
<ConnectedToolbarSection /> <ConnectedToolbarSection />
<ConnectedLayoutButton /> <ConnectedLayoutButton />
@ -63,11 +90,15 @@ class ToolbarRow extends Component {
text={'CINE'} text={'CINE'}
iconClasses={'fa fa-youtube'} iconClasses={'fa fa-youtube'}
/> />
<div className='CineDialogContainer' style={cineDialogContainerStyle}> <div className="CineDialogContainer" style={cineDialogContainerStyle}>
<ConnectedCineDialog /> <ConnectedCineDialog />
</div> </div>
<div className="pull-right m-t-1 rm-x-1"> <div className="pull-right m-t-1 rm-x-1" style={{ marginLeft: 'auto' }}>
{/* > roundedButtonGroup rightSidebarToggleButtonData */} <RoundedButtonGroup
options={rightSidebarToggle}
value={rightSidebarValue}
onValueChanged={this.onRightSidebarValueChanged}
/>
</div> </div>
</div> </div>
); );

View File

@ -3,12 +3,15 @@ import PropTypes from 'prop-types';
//import OHIF from 'ohif-core'; //import OHIF from 'ohif-core';
//import { CineDialog } from 'react-viewerbase'; //import { CineDialog } from 'react-viewerbase';
import OHIF from 'ohif-core';
import moment from 'moment';
import WhiteLabellingContext from '../WhiteLabellingContext.js'; import WhiteLabellingContext from '../WhiteLabellingContext.js';
import ConnectedHeader from './ConnectedHeader.js'; import ConnectedHeader from './ConnectedHeader.js';
import ConnectedFlexboxLayout from './ConnectedFlexboxLayout.js'; import ConnectedFlexboxLayout from './ConnectedFlexboxLayout.js';
import ConnectedToolbarRow from './ConnectedToolbarRow.js'; import ConnectedToolbarRow from './ConnectedToolbarRow.js';
import ConnectedStudyLoadingMonitor from './ConnectedStudyLoadingMonitor.js'; import ConnectedStudyLoadingMonitor from './ConnectedStudyLoadingMonitor.js';
import StudyPrefetcher from '../components/StudyPrefetcher.js'; import StudyPrefetcher from '../components/StudyPrefetcher.js';
import measurementTools from '../measurementTools';
import './Viewer.css'; import './Viewer.css';
/** /**
* Inits OHIF Hanging Protocol's onReady. * Inits OHIF Hanging Protocol's onReady.
@ -51,6 +54,117 @@ class Viewer extends Component {
studies: PropTypes.array studies: PropTypes.array
}; };
constructor(props) {
super(props);
const retrieveMeasurements = (patientId, timepointIds) => {
OHIF.log.info('retrieveMeasurements');
// TODO: Retrieve the measurements from the latest available SR
return Promise.resolve();
};
const storeMeasurements = (measurementData, timepointIds) => {
OHIF.log.info('storeMeasurements');
// TODO: Store the measurements into a new SR sent to the active server
return Promise.resolve();
};
const retrieveTimepoints = filter => {
OHIF.log.info('retrieveTimepoints');
// Get the earliest and latest study date
let earliestDate = new Date().toISOString();
let latestDate = new Date().toISOString();
if (props.studies) {
latestDate = new Date('1000-01-01').toISOString();
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: props.studyInstanceUids,
patientId: filter.patientId,
earliestDate,
latestDate,
isLocked: false
}
]);
};
const storeTimepoints = timepointData => {
OHIF.log.info('storeTimepoints');
return Promise.resolve();
};
const updateTimepoint = (timepointData, query) => {
OHIF.log.info('updateTimepoint');
return Promise.resolve();
};
const removeTimepoint = timepointId => {
OHIF.log.info('removeTimepoint');
return Promise.resolve();
};
const disassociateStudy = (timepointIds, studyInstanceUid) => {
OHIF.log.info('disassociateStudy');
return Promise.resolve();
};
OHIF.measurements.MeasurementApi.setConfiguration({
measurementTools,
dataExchange: {
retrieve: retrieveMeasurements,
store: storeMeasurements
}
});
OHIF.measurements.TimepointApi.setConfiguration({
dataExchange: {
retrieve: retrieveTimepoints,
store: storeTimepoints,
remove: removeTimepoint,
update: updateTimepoint,
disassociate: disassociateStudy
}
});
}
componentDidMount() {
const { studies } = this.props;
const { TimepointApi, MeasurementApi } = OHIF.measurements;
// TODO: Get the Redux store from somewhere else
const { store } = window;
const timepointApi = new TimepointApi(store, 'TimepointId');
const measurementApi = new MeasurementApi(store, timepointApi);
const apis = {
timepointApi,
measurementApi
};
Object.assign(OHIF.viewer, apis);
const patientId = studies[0] && studies[0].patientId;
timepointApi.retrieveTimepoints({ patientId });
// TODO: Retrieve measurements and sync them with tool data
// measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
// measurementApi.syncMeasurementsAndToolData();
}
render() { render() {
return ( return (
<> <>

View File

@ -0,0 +1,15 @@
const displayFunction = data => {
return data.text || '';
};
export default {
id: 'arrowAnnotate',
name: 'ArrowAnnotate',
toolGroup: 'allTools',
cornerstoneToolType: 'arrowAnnotate',
options: {
measurementTable: {
displayFunction
}
}
};

View File

@ -0,0 +1,22 @@
const displayFunction = data => {
let meanValue = '';
if (data.meanStdDev && data.meanStdDev.mean) {
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
}
return meanValue;
// let meanValue = data.meanStdDev && data.meanStdDev.mean || 0;
// return numberWithCommas(meanValue).toFixed(2) + ' HU';
//return data.meanStdDev.mean.toFixed(2);
};
export default {
id: 'ellipticalRoi',
name: 'Ellipse',
toolGroup: 'allTools',
cornerstoneToolType: 'ellipticalRoi',
options: {
measurementTable: {
displayFunction
}
}
};

View File

@ -0,0 +1,21 @@
import length from './length';
import ellipticalRoi from './ellipticalRoi';
import rectangleRoi from './rectangleRoi';
import simpleAngle from './simpleAngle';
import arrowAnnotate from './arrowAnnotate';
const trackedTools = [
length,
ellipticalRoi,
rectangleRoi,
simpleAngle,
arrowAnnotate
];
export default [
{
id: 'allTools',
name: 'Measurements',
childTools: trackedTools
}
];

View File

@ -0,0 +1,19 @@
const displayFunction = data => {
let lengthValue = '';
if (data.length) {
lengthValue = data.length.toFixed(2) + ' mm';
}
return lengthValue;
};
export default {
id: 'length',
name: 'Length',
toolGroup: 'allTools',
cornerstoneToolType: 'length',
options: {
measurementTable: {
displayFunction
}
}
};

View File

@ -0,0 +1,19 @@
const displayFunction = data => {
let meanValue = '';
if (data.meanStdDev && data.meanStdDev.mean) {
meanValue = data.meanStdDev.mean.toFixed(2) + ' HU';
}
return meanValue;
};
export default {
id: 'rectangleRoi',
name: 'Rectangle',
toolGroup: 'allTools',
cornerstoneToolType: 'rectangleRoi',
options: {
measurementTable: {
displayFunction
}
}
};

View File

@ -0,0 +1,19 @@
const displayFunction = data => {
let text = '';
if (data.rAngle) {
text = data.rAngle.toFixed(2) + String.fromCharCode(parseInt('00B0', 16));
}
return text;
};
export default {
id: 'simpleAngle',
name: 'SimpleAngle',
toolGroup: 'allTools',
cornerstoneToolType: 'simpleAngle',
options: {
measurementTable: {
displayFunction
}
}
};