From 84e2f86a765e5625b3893ff7d69377d31a497b09 Mon Sep 17 00:00:00 2001 From: Evren Ozkan Date: Thu, 31 Jan 2019 22:01:39 -0500 Subject: [PATCH] feat(react-measurements): Add measurement table - WIP --- Packages-react/ohif-viewer/package.json | 1 + Packages-react/ohif-viewer/src/App.css | 1 + Packages-react/ohif-viewer/src/App.js | 2 +- .../ConnectedMeasurementTable.js | 48 ++++++ .../src/connectedComponents/FlexboxLayout.js | 154 ++++++++++-------- .../src/connectedComponents/ToolbarRow.js | 153 ++++++++++------- .../src/connectedComponents/Viewer.js | 114 +++++++++++++ .../src/measurementTools/arrowAnnotate.js | 15 ++ .../src/measurementTools/ellipticalRoi.js | 22 +++ .../ohif-viewer/src/measurementTools/index.js | 21 +++ .../src/measurementTools/length.js | 19 +++ .../src/measurementTools/rectangleRoi.js | 19 +++ .../src/measurementTools/simpleAngle.js | 19 +++ 13 files changed, 458 insertions(+), 130 deletions(-) create mode 100644 Packages-react/ohif-viewer/src/connectedComponents/ConnectedMeasurementTable.js create mode 100644 Packages-react/ohif-viewer/src/measurementTools/arrowAnnotate.js create mode 100644 Packages-react/ohif-viewer/src/measurementTools/ellipticalRoi.js create mode 100644 Packages-react/ohif-viewer/src/measurementTools/index.js create mode 100644 Packages-react/ohif-viewer/src/measurementTools/length.js create mode 100644 Packages-react/ohif-viewer/src/measurementTools/rectangleRoi.js create mode 100644 Packages-react/ohif-viewer/src/measurementTools/simpleAngle.js diff --git a/Packages-react/ohif-viewer/package.json b/Packages-react/ohif-viewer/package.json index 7fc8dfb02..183db3978 100644 --- a/Packages-react/ohif-viewer/package.json +++ b/Packages-react/ohif-viewer/package.json @@ -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", diff --git a/Packages-react/ohif-viewer/src/App.css b/Packages-react/ohif-viewer/src/App.css index 68bca728d..25ef88265 100644 --- a/Packages-react/ohif-viewer/src/App.css +++ b/Packages-react/ohif-viewer/src/App.css @@ -5,6 +5,7 @@ body, height: 100%; width: 100%; margin: 0; + overflow: hidden; } body { diff --git a/Packages-react/ohif-viewer/src/App.js b/Packages-react/ohif-viewer/src/App.js index bc7ebbcc0..134c91a3e 100644 --- a/Packages-react/ohif-viewer/src/App.js +++ b/Packages-react/ohif-viewer/src/App.js @@ -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; diff --git a/Packages-react/ohif-viewer/src/connectedComponents/ConnectedMeasurementTable.js b/Packages-react/ohif-viewer/src/connectedComponents/ConnectedMeasurementTable.js new file mode 100644 index 000000000..d3759194d --- /dev/null +++ b/Packages-react/ohif-viewer/src/connectedComponents/ConnectedMeasurementTable.js @@ -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; diff --git a/Packages-react/ohif-viewer/src/connectedComponents/FlexboxLayout.js b/Packages-react/ohif-viewer/src/connectedComponents/FlexboxLayout.js index eb272e5b8..a0618960d 100644 --- a/Packages-react/ohif-viewer/src/connectedComponents/FlexboxLayout.js +++ b/Packages-react/ohif-viewer/src/connectedComponents/FlexboxLayout.js @@ -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 ( -
-
- -
-
- -
- {/*
- {{> measurementLightTable (clone this)}} -
*/} -
- ); + 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 ( +
+
+ +
+
+ +
+
+ +
+
+ ); + } } export default FlexboxLayout; diff --git a/Packages-react/ohif-viewer/src/connectedComponents/ToolbarRow.js b/Packages-react/ohif-viewer/src/connectedComponents/ToolbarRow.js index 9eebf7494..89623fd8c 100644 --- a/Packages-react/ohif-viewer/src/connectedComponents/ToolbarRow.js +++ b/Packages-react/ohif-viewer/src/connectedComponents/ToolbarRow.js @@ -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 (
-
- -
- - - -
- -
-
- {/* > roundedButtonGroup rightSidebarToggleButtonData */} -
-
- ); - } + return ( +
+
+ +
+ + + +
+ +
+
+ +
+
+ ); + } } export default ToolbarRow; diff --git a/Packages-react/ohif-viewer/src/connectedComponents/Viewer.js b/Packages-react/ohif-viewer/src/connectedComponents/Viewer.js index 0af643745..2cda2a7d6 100644 --- a/Packages-react/ohif-viewer/src/connectedComponents/Viewer.js +++ b/Packages-react/ohif-viewer/src/connectedComponents/Viewer.js @@ -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 ( <> diff --git a/Packages-react/ohif-viewer/src/measurementTools/arrowAnnotate.js b/Packages-react/ohif-viewer/src/measurementTools/arrowAnnotate.js new file mode 100644 index 000000000..e5568fe1a --- /dev/null +++ b/Packages-react/ohif-viewer/src/measurementTools/arrowAnnotate.js @@ -0,0 +1,15 @@ +const displayFunction = data => { + return data.text || ''; +}; + +export default { + id: 'arrowAnnotate', + name: 'ArrowAnnotate', + toolGroup: 'allTools', + cornerstoneToolType: 'arrowAnnotate', + options: { + measurementTable: { + displayFunction + } + } +}; diff --git a/Packages-react/ohif-viewer/src/measurementTools/ellipticalRoi.js b/Packages-react/ohif-viewer/src/measurementTools/ellipticalRoi.js new file mode 100644 index 000000000..93096f43b --- /dev/null +++ b/Packages-react/ohif-viewer/src/measurementTools/ellipticalRoi.js @@ -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 + } + } +}; diff --git a/Packages-react/ohif-viewer/src/measurementTools/index.js b/Packages-react/ohif-viewer/src/measurementTools/index.js new file mode 100644 index 000000000..803422ca4 --- /dev/null +++ b/Packages-react/ohif-viewer/src/measurementTools/index.js @@ -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 + } +]; diff --git a/Packages-react/ohif-viewer/src/measurementTools/length.js b/Packages-react/ohif-viewer/src/measurementTools/length.js new file mode 100644 index 000000000..5348dd859 --- /dev/null +++ b/Packages-react/ohif-viewer/src/measurementTools/length.js @@ -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 + } + } +}; diff --git a/Packages-react/ohif-viewer/src/measurementTools/rectangleRoi.js b/Packages-react/ohif-viewer/src/measurementTools/rectangleRoi.js new file mode 100644 index 000000000..1248bdc2c --- /dev/null +++ b/Packages-react/ohif-viewer/src/measurementTools/rectangleRoi.js @@ -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 + } + } +}; diff --git a/Packages-react/ohif-viewer/src/measurementTools/simpleAngle.js b/Packages-react/ohif-viewer/src/measurementTools/simpleAngle.js new file mode 100644 index 000000000..3197c4043 --- /dev/null +++ b/Packages-react/ohif-viewer/src/measurementTools/simpleAngle.js @@ -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 + } + } +};