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-microscopy-extension": "^0.0.5",
"ohif-dicom-pdf-extension": "^0.0.5",
"moment": "^2.24.0",
"oidc-client": "^1.6.1",
"prop-types": "^15.6.2",
"react-bootstrap-modal": "^4.2.0",

View File

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

View File

@ -19,11 +19,11 @@ import {
reducer as oidcReducer
} from 'redux-oidc';
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
import './App.css';
const { ExtensionManager } = OHIF.extensions;
//import Icons from "./images/icons.svg"
const Icons = '/icons.svg';
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,89 +2,107 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
import ViewerMain from './ViewerMain.js';
import ConnectedMeasurementTable from './ConnectedMeasurementTable';
import './FlexboxLayout.css';
class FlexboxLayout extends Component {
static propTypes = {
studies: PropTypes.array.isRequired,
leftSidebarOpen: PropTypes.bool.isRequired,
rightSidebarOpen: PropTypes.bool.isRequired,
};
static propTypes = {
studies: PropTypes.array.isRequired,
leftSidebarOpen: PropTypes.bool.isRequired,
rightSidebarOpen: PropTypes.bool.isRequired
};
state = {
studiesForBrowser: []
};
state = {
studiesForBrowser: []
};
componentDidMount() {
const studiesForBrowser = this.getStudiesForBrowser();
componentDidMount() {
const studiesForBrowser = this.getStudiesForBrowser();
this.setState({
studiesForBrowser
});
}
this.setState({
studiesForBrowser
});
}
getStudiesForBrowser = () => {
const { studies } = this.props;
getStudiesForBrowser = () => {
const { studies } = this.props;
// TODO[react]:
// - Add sorting of display sets
// - Add useMiddleSeriesInstanceAsThumbnail
// - Add showStackLoadingProgressBar option
return studies.map((study) => {
const { studyInstanceUid } = study;
// TODO[react]:
// - Add sorting of display sets
// - Add useMiddleSeriesInstanceAsThumbnail
// - Add showStackLoadingProgressBar option
return studies.map(study => {
const { studyInstanceUid } = study;
const thumbnails = study.displaySets.map((displaySet) => {
const { displaySetInstanceUid, seriesDescription, seriesNumber, instanceNumber, numImageFrames } = displaySet;
const thumbnails = study.displaySets.map(displaySet => {
const {
displaySetInstanceUid,
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames
} = displaySet;
let imageId;
let imageId;
if (displaySet.images && displaySet.images.length) {
imageId = displaySet.images[0].getImageId();
}
return {
imageId,
displaySetInstanceUid,
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames,
};
});
return {
studyInstanceUid,
thumbnails
};
});
}
render() {
let mainContentClassName = "main-content"
if (this.props.leftSidebarOpen) {
mainContentClassName += ' sidebar-left-open';
if (displaySet.images && displaySet.images.length) {
imageId = displaySet.images[0].getImageId();
}
if (this.props.rightSidebarOpen) {
mainContentClassName += ' sidebar-right-open';
}
return {
imageId,
displaySetInstanceUid,
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames
};
});
// TODO[react]: Add measurementLightTable
return (
<div className="FlexboxLayout">
<div className={this.props.leftSidebarOpen ? "sidebar-menu sidebar-left sidebar-open" : "sidebar-menu sidebar-left"}>
<ConnectedStudyBrowser studies={this.state.studiesForBrowser}/>
</div>
<div className={mainContentClassName}>
<ViewerMain studies={this.props.studies}/>
</div>
{/* <div className={this.props.rightSidebarOpen ? "sidebar-menu sidebar-right sidebar-open" : "sidebar-menu sidebar-right"}>
{{> measurementLightTable (clone this)}}
</div> */}
</div>
);
return {
studyInstanceUid,
thumbnails
};
});
};
render() {
let mainContentClassName = 'main-content';
if (this.props.leftSidebarOpen) {
mainContentClassName += ' sidebar-left-open';
}
if (this.props.rightSidebarOpen) {
mainContentClassName += ' sidebar-right-open';
}
// TODO[react]: Add measurementLightTable
return (
<div className="FlexboxLayout">
<div
className={
this.props.leftSidebarOpen
? 'sidebar-menu sidebar-left sidebar-open'
: 'sidebar-menu sidebar-left'
}
>
<ConnectedStudyBrowser studies={this.state.studiesForBrowser} />
</div>
<div className={mainContentClassName}>
<ViewerMain studies={this.props.studies} />
</div>
<div
className={
this.props.rightSidebarOpen
? 'sidebar-menu sidebar-right sidebar-open'
: 'sidebar-menu sidebar-right'
}
>
<ConnectedMeasurementTable />
</div>
</div>
);
}
}
export default FlexboxLayout;

View File

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

View File

@ -3,12 +3,15 @@ import PropTypes from 'prop-types';
//import OHIF from 'ohif-core';
//import { CineDialog } from 'react-viewerbase';
import OHIF from 'ohif-core';
import moment from 'moment';
import WhiteLabellingContext from '../WhiteLabellingContext.js';
import ConnectedHeader from './ConnectedHeader.js';
import ConnectedFlexboxLayout from './ConnectedFlexboxLayout.js';
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
import ConnectedStudyLoadingMonitor from './ConnectedStudyLoadingMonitor.js';
import StudyPrefetcher from '../components/StudyPrefetcher.js';
import measurementTools from '../measurementTools';
import './Viewer.css';
/**
* Inits OHIF Hanging Protocol's onReady.
@ -51,6 +54,117 @@ class Viewer extends Component {
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() {
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
}
}
};