Merge branch 'react' into docs/orthanc-proxy

This commit is contained in:
Danny Brown authored and GitHub committed 2019-05-09 12:10:16 -04:00
commit b7f100f601
33 files changed
+2219 -151

No files matched your search

+7 -21
View File
@@ -19,6 +19,8 @@ import {
getUserManagerForOpenIdConnectClient,
initWebWorkers,
} from './utils/index.js'
import setupTools from './setupTools'
import ConnectedToolContextMenu from './connectedComponents/ConnectedToolContextMenu'
const { ExtensionManager } = OHIF.extensions
const { reducers, localStorage } = OHIF.redux
@@ -35,31 +37,15 @@ store.subscribe(() => {
})
})
const availableTools = [
{ name: 'Pan', mouseButtonMasks: [1, 4] },
{ name: 'Zoom', mouseButtonMasks: [1, 2] },
{ name: 'Wwwc', mouseButtonMasks: [1] },
{ name: 'Bidirectional', mouseButtonMasks: [1] },
{ name: 'Length', mouseButtonMasks: [1] },
{ name: 'Angle', mouseButtonMasks: [1] },
{ name: 'StackScroll', mouseButtonMasks: [1] },
{ name: 'Brush', mouseButtonMasks: [1] },
{ name: 'FreehandMouse', mouseButtonMasks: [1] },
{ name: 'PanMultiTouch' },
{ name: 'ZoomTouchPinch' },
{ name: 'StackScrollMouseWheel' },
{ name: 'StackScrollMultiTouch' },
]
setupTools(store)
const toolAction = OHIF.redux.actions.setExtensionData('cornerstone', {
availableTools,
})
store.dispatch(toolAction)
const children = {
viewport: [<ConnectedToolContextMenu />],
}
/** TODO: extensions should be passed in as prop as soon as we have the extensions as separate packages and then registered by ExtensionsManager */
const extensions = [
new OHIFCornerstoneExtension({}),
new OHIFCornerstoneExtension({ children }),
new OHIFVTKExtension(),
new OHIFDicomPDFExtension(),
new OHIFDicomHtmlExtension(),
@@ -0,0 +1,6 @@
.editDescriptionDialog {
position: absolute;
z-index: 300;
width: 320px;
transition: all 300ms linear;
}
@@ -0,0 +1,82 @@
import { Component } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import SimpleDialog from '../SimpleDialog/SimpleDialog.js'
import bounding from '../../lib/utils/bounding.js'
import { getDialogStyle } from './../Labelling/labellingPositionUtils.js'
import './EditDescriptionDialog.css'
export default class EditDescriptionDialog extends Component {
static defaultProps = {
componentRef: React.createRef(),
componentStyle: {},
}
static propTypes = {
measurementData: PropTypes.object.isRequired,
onCancel: PropTypes.func.isRequired,
componentRef: PropTypes.object,
componentStyle: PropTypes.object,
onUpdate: PropTypes.func.isRequired,
}
constructor(props) {
super(props)
this.state = {
description: props.measurementData.description || '',
}
this.mainElement = React.createRef()
}
componentDidMount = () => {
bounding(this.mainElement)
}
componentDidUpdate(prevProps) {
if (this.props.description !== prevProps.description) {
this.setState({
description: this.props.description,
})
}
}
render() {
const style = getDialogStyle(this.props.componentStyle)
return (
<SimpleDialog
headerTitle="Edit Description"
onClose={this.onClose}
onConfirm={this.onConfirm}
rootClass="editDescriptionDialog"
componentRef={this.mainElement}
componentStyle={style}
>
<input
value={this.state.description}
className="simpleDialogInput"
id="description"
autoComplete="off"
autoFocus
onChange={this.handleChange}
/>
</SimpleDialog>
)
}
onClose = () => {
this.props.onCancel()
}
onConfirm = () => {
this.props.onUpdate(this.state.description)
}
handleChange = event => {
this.setState({ description: event.target.value })
}
}
+276
View File
@@ -0,0 +1,276 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import cloneDeep from 'lodash.clonedeep'
import bounding from '../../lib/utils/bounding.js'
import LabellingTransition from './LabellingTransition.js'
import { SelectTree } from 'react-viewerbase'
import { getAddLabelButtonStyle } from './labellingPositionUtils.js'
import OHIFLabellingData from './OHIFLabellingData.js'
export default class LabellingFlow extends Component {
static propTypes = {
eventData: PropTypes.object.isRequired,
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
}
constructor(props) {
super(props)
const { location, locationLabel, description } = props.measurementData
let style = props.componentStyle
if (!props.skipAddLabelButton) {
style = getAddLabelButtonStyle(props.measurementData, props.eventData)
}
this.state = {
location,
locationLabel,
description,
skipAddLabelButton: props.skipAddLabelButton,
editDescription: props.editDescription,
editLocation: props.editLocation,
componentStyle: style,
confirmationState: false,
displayComponent: true,
}
this.mainElement = React.createRef()
this.descriptionInput = React.createRef()
this.initialItems = OHIFLabellingData
this.currentItems = cloneDeep(this.initialItems)
}
componentDidUpdate = () => {
this.repositionComponent()
}
render() {
let mainElementClassName = 'labellingComponent'
if (this.state.editDescription) {
mainElementClassName += ' editDescription'
}
const style = Object.assign({}, this.state.componentStyle)
if (this.state.skipAddLabelButton) {
style.left -= 160
}
return (
<LabellingTransition
displayComponent={this.state.displayComponent}
onTransitionExit={this.props.labellingDoneCallback}
>
<div
className={mainElementClassName}
style={style}
ref={this.mainElement}
onMouseLeave={this.fadeOutAndLeave}
onMouseEnter={this.clearFadeOutTimer}
>
{this.labellingStateFragment()}
</div>
</LabellingTransition>
)
}
labellingStateFragment = () => {
const {
skipAddLabelButton,
editLocation,
description,
locationLabel,
} = this.state
if (!skipAddLabelButton) {
return (
<>
<button
type="button"
className="addLabelButton"
onClick={this.showLabelling}
>
{this.state.location ? 'Edit' : 'Add'} Label
</button>
</>
)
} else {
if (editLocation) {
return (
<SelectTree
items={this.currentItems}
columns={1}
onSelected={this.selectTreeSelectCalback}
selectTreeFirstTitle="Assign Label"
onComponentChange={this.repositionComponent}
/>
)
} else {
return (
<>
<div
className="checkIconWrapper"
onClick={this.fadeOutAndLeaveFast}
>
<svg className="checkIcon">
<use xlinkHref="/icons.svg#check-solid" />
</svg>
</div>
<div className="locationDescriptionWrapper">
<div className="location">{locationLabel}</div>
<div className="description">
<input
id="descriptionInput"
ref={this.descriptionInput}
defaultValue={description || ''}
autoComplete="off"
/>
</div>
</div>
<div className="commonButtons">
<button
type="button"
className="commonButton left"
onClick={this.relabel}
>
Relabel
</button>
<button
type="button"
className="commonButton right"
onClick={this.setDescriptionUpdateMode}
>
{description ? 'Edit ' : 'Add '}
Description
</button>
</div>
<div className="editDescriptionButtons">
<button
type="button"
className="commonButton left"
onClick={this.descriptionCancel}
>
Cancel
</button>
<button
type="button"
className="commonButton right"
onClick={this.descriptionSave}
>
Save
</button>
</div>
</>
)
}
}
}
relabel = () => {
this.setState({
editLocation: true,
})
}
setDescriptionUpdateMode = () => {
this.descriptionInput.current.focus()
this.setState({
editDescription: true,
})
}
descriptionCancel = () => {
const { description = '' } = cloneDeep(this.state)
this.descriptionInput.current.value = description
this.setState({
editDescription: false,
})
}
descriptionSave = () => {
const description = this.descriptionInput.current.value
this.props.updateLabelling({ description })
this.setState({
description,
editDescription: false,
})
}
selectTreeSelectCalback = (event, itemSelected) => {
const location = itemSelected.value
this.props.updateLabelling({ location })
const viewportTopPosition = this.mainElement.current.offsetParent.offsetTop
const componentStyle = {
top: event.nativeEvent.y - viewportTopPosition - 25,
left: this.state.componentStyle.left,
}
this.setState({
editLocation: false,
confirmationState: true,
location: itemSelected.value,
locationLabel: itemSelected.label,
componentStyle,
})
if (this.isTouchScreen) {
this.setTimeout = setTimeout(() => {
this.setState({
displayComponent: false,
})
}, 2000)
}
}
showLabelling = () => {
this.setState({
skipAddLabelButton: true,
editLocation: false,
})
}
fadeOutAndLeave = () => {
// Wait for 1 sec to dismiss the labelling component
this.fadeOutTimer = setTimeout(() => {
this.setState({
displayComponent: false,
})
}, 1000)
}
fadeOutAndLeaveFast = () => {
this.setState({
displayComponent: false,
})
}
clearFadeOutTimer = () => {
if (!this.fadeOutTimer) {
return
}
clearTimeout(this.fadeOutTimer)
}
repositionComponent = () => {
// SetTimeout for the css animation to end.
setTimeout(() => {
bounding(this.mainElement)
}, 200)
}
}
@@ -0,0 +1,130 @@
.labellingComponent {
position: absolute;
text-align: center;
z-index: 300;
transition: all 200ms linear;
}
.labellingComponent .selectedLabel,
.labellingComponent .selectedDescription {
padding: 5px;
background-color: white;
width: 150px;
}
.labellingComponent .addLabelButton {
color: #000000;
background-color: #20a5d6;
border: 2px solid #44626f;
border-radius: 14px;
cursor: pointer;
font-weight: bold;
font-size: 13px;
line-height: 24px;
opacity: 1;
padding: 0 14px;
transition: opacity 0.3s ease;
outline: none;
cursor: pointer;
}
.labellingComponent .commonButton {
border: 1px solid #44626f;
color: #ffffff;
background-color: #000000;
border-radius: 16px;
font-weight: bold;
font-size: 13px;
line-height: 26px;
padding: 0 12px;
margin: 10px 5px 0 0;
outline: none;
cursor: pointer;
}
.labellingComponent .locationDescriptionWrapper {
background-color: #ffffff;
line-height: 46px;
height: 46px;
font-size: 13px;
position: relative;
width: 290px;
min-width: 260px;
padding: 0 12px;
margin: 0 auto;
display: inline-block;
}
.labellingComponent .locationDescriptionWrapper .location {
transition: all 300ms linear;
position: absolute;
white-space: nowrap;
bottom: 0;
}
.labellingComponent.editDescription .locationDescriptionWrapper .location {
bottom: 38px;
}
.labellingComponent .locationDescriptionWrapper #descriptionInput {
transition-delay: all 300ms linear;
visibility: hidden;
outline: none;
height: 46px;
width: 100%;
line-height: 20px;
font-size: 13px;
border: none;
}
.labellingComponent.editDescription
.locationDescriptionWrapper
#descriptionInput {
visibility: visible;
}
.labellingComponent.editDescription .location {
color: #337ab7;
}
.labellingComponent .commonButtons,
.labellingComponent.editDescription .editDescriptionButtons {
display: block;
margin-left: 55px;
}
.labellingComponent.editDescription .commonButtons,
.labellingComponent .editDescriptionButtons {
display: none;
}
.labellingComponent .commonButtons {
text-align: center;
margin-left: 55px;
}
.labellingComponent .commonButton.left {
float: left;
}
.labellingComponent .commonButton.right {
float: right;
}
.labellingComponent .checkIconWrapper {
display: inline-block;
background-color: #337ab7;
border-radius: 46px;
width: 46px;
height: 46px;
margin-right: 10px;
vertical-align: bottom;
cursor: pointer;
}
.labellingComponent .checkIcon {
width: 20px;
height: 20px;
margin: 13px;
fill: black;
}
@@ -0,0 +1,130 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import cloneDeep from 'lodash.clonedeep'
import EditDescriptionDialog from './../EditDescriptionDialog/EditDescriptionDialog.js'
import LabellingFlow from './LabellingFlow.js'
import './LabellingManager.css'
export default class LabellingManager extends Component {
static propTypes = {
eventData: PropTypes.object.isRequired,
measurementData: PropTypes.object.isRequired,
labellingDoneCallback: PropTypes.func.isRequired,
updateLabelling: PropTypes.func.isRequired,
skipAddLabelButton: PropTypes.bool,
editLocation: PropTypes.bool,
editDescription: PropTypes.bool,
editDescriptionOnDialog: PropTypes.bool,
}
static defaultProps = {
skipAddLabelButton: false,
editLocation: false,
editDescription: false,
editDescriptionOnDialog: false,
}
constructor(props) {
super(props)
const measurementData = cloneDeep(props.measurementData)
this.treatMeasurementData(measurementData)
let editLocation = props.editLocation
if (!props.editDescription && !props.editLocation) {
editLocation = true
}
this.state = {
componentStyle: getComponentPosition(props.eventData),
skipAddLabelButton: props.skipAddLabelButton,
editLocation: editLocation,
editDescription: props.editDescription,
editDescriptionOnDialog: props.editDescriptionOnDialog,
measurementData: measurementData,
}
}
componentDidMount = () => {
document.addEventListener('touchstart', this.onTouchStart)
}
componentWillUnmount = () => {
document.removeEventListener('touchstart', this.onTouchStart)
}
render() {
return this.getRenderComponent()
}
getRenderComponent = () => {
const {
editLocation,
editDescription,
editDescriptionOnDialog,
measurementData,
} = this.state
if (editDescriptionOnDialog) {
return (
<EditDescriptionDialog
onCancel={this.props.labellingDoneCallback}
onUpdate={this.descriptionDialogUpdate}
componentRef={this.editDescriptionDialog}
componentStyle={this.state.componentStyle}
measurementData={measurementData}
/>
)
}
if (editLocation || editDescription) {
return (
<LabellingFlow
{...this.props}
componentStyle={this.state.componentStyle}
/>
)
}
}
treatMeasurementData = measurementData => {
const { editDescription, editLocation } = this.props
if (editDescription) {
measurementData.description = undefined
}
if (editLocation) {
measurementData.location = undefined
}
}
responseDialogUpdate = response => {
this.props.updateLabelling({
response,
})
this.props.labellingDoneCallback()
}
descriptionDialogUpdate = description => {
this.props.updateLabelling({
description,
})
this.props.labellingDoneCallback()
}
}
function getComponentPosition(eventData) {
const {
event: { clientX: left, clientY: top },
} = eventData
return {
left,
top,
}
}
@@ -0,0 +1,21 @@
.labelling-appear {
opacity: 0;
}
.labelling-appear.labelling-appear-active {
opacity: 1;
transition: opacity 500ms linear;
}
.labelling-exit {
opacity: 1;
}
.labelling-exit.labelling-exit-active {
opacity: 0;
transition: opacity 500ms linear;
}
.labelling-exit-done {
opacity: 0;
}
@@ -0,0 +1,31 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { CSSTransition } from 'react-transition-group'
import './LabellingTransition.css'
// If these variables changes, CSS must be updated
const transitionDuration = 500
const transitionClassName = 'labelling'
const transitionOnAppear = true
export default class LabellingTransition extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
displayComponent: PropTypes.bool.isRequired,
onTransitionExit: PropTypes.func.isRequired,
}
render() {
return (
<CSSTransition
in={this.props.displayComponent}
appear={transitionOnAppear}
timeout={transitionDuration}
classNames={transitionClassName}
onExited={this.props.onTransitionExit}
>
{this.props.children}
</CSSTransition>
)
}
}
@@ -0,0 +1,39 @@
const items = [
'Abdomen/Chest Wall',
'Adrenal',
'Bladder',
'Bone',
'Brain',
'Breast',
'Colon',
'Esophagus',
'Extremities',
'Gallbladder',
'Kidney',
'Liver',
'Lung',
'Lymph Node',
'Mediastinum/Hilum',
'Muscle',
'Neck',
'Other Soft Tissue',
'Ovary',
'Pancreas',
'Pelvis',
'Peritoneum/Omentum',
'Prostate',
'Retroperitoneum',
'Small Bowel',
'Spleen',
'Stomach',
'Subcutaneous',
]
const OHIFLabellingData = items.map(item => {
return {
label: item,
value: item,
}
})
export default OHIFLabellingData
@@ -0,0 +1,53 @@
import cornerstone from 'cornerstone-core'
const buttonSize = {
width: 96,
height: 28,
}
export function getAddLabelButtonStyle(measurementData, eventData) {
const { start, end } = measurementData.handles
const { client } = eventData.currentPoints
const clientStart = cornerstone.pixelToCanvas(eventData.element, start)
const clientEnd = cornerstone.pixelToCanvas(eventData.element, end)
const canvasOffSetLeft = client.x - clientStart.x
const canvasOffSetTop = client.y - clientStart.y
const position = {
left: clientEnd.x + canvasOffSetLeft,
top: clientEnd.y + canvasOffSetTop,
}
if (start.y > end.y) {
position.top -= buttonSize.height
}
if (start.x > end.x) {
position.left -= buttonSize.width
}
return position
}
export function getDialogStyle(componentStyle) {
const style = Object.assign({}, componentStyle)
const dialogProps = {
width: 320,
height: 230,
}
// Get max values to avoid position out of the screen
const maxLeft = window.innerWidth - dialogProps.width
const maxTop = window.innerHeight - dialogProps.height
// Positioning the dialog with its center on the click event
style.left -= dialogProps.width / 2
style.top -= dialogProps.height / 2
if (style.left > maxLeft) {
style.left = maxLeft
}
if (style.top > maxTop) {
style.top = maxTop
}
return style
}
@@ -0,0 +1,133 @@
.simpleDialog {
position: fixed;
border: 0;
border-radius: 6px;
background-color: #151a1f;
}
.simpleDialog .header {
border-bottom-width: 3px;
border-bottom-style: solid;
border-bottom-color: #000;
padding: 19px 22px 17px;
position: relative;
}
.simpleDialog .header .title {
font-size: 20px;
font-weight: 600;
line-height: 24px;
padding-right: 40px;
color: #91b9cd;
margin: 0;
}
.simpleDialog .header .closeBtn {
height: 20px;
opacity: 1;
overflow: hidden;
padding: 2px;
text-align: center;
text-shadow: none;
width: 20px;
color: #91b9cd;
cursor: pointer;
position: absolute;
right: 21px;
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
transition: color 0.3s ease;
}
.simpleDialog .header .closeIcon {
color: transparent;
display: block;
font-size: 0;
height: 100%;
line-height: 0;
overflow: hidden;
position: relative;
width: 100%;
}
.simpleDialog .header .closeIcon:after,
.simpleDialog .header .closeIcon:before {
content: ' ';
display: block;
height: 2px;
transition: background-color 0.3s ease;
width: 19px;
background-color: #91b9cd;
}
.simpleDialog .header .closeIcon:before {
left: 1px;
position: absolute;
top: 1px;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-transform-origin: 1px 50%;
transform-origin: 1px 50%;
}
.simpleDialog .header .closeIcon:after {
right: 1px;
position: absolute;
top: 1px;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
-webkit-transform-origin: calc(100% - 1px) 50%;
transform-origin: calc(100% - 1px) 50%;
}
.simpleDialog .content {
padding: 16px 22px 25px;
position: relative;
color: #fff;
}
.simpleDialog .content .simpleDialogSelect,
.simpleDialog .content .simpleDialogInput {
background-color: #fff;
color: #000;
border: 0;
border-radius: 2px;
font-size: 14px;
height: 30px;
width: 100%;
line-height: 16px;
padding: 8px 9px 6px;
margin-top: 10px;
display: block;
}
.simpleDialog .content .simpleDialogInputLabel {
font-size: 14px;
font-weight: 700;
line-height: 16px;
color: #fff;
}
.simpleDialog .footer {
padding: 15px;
text-align: right;
}
.simpleDialog .footer .btn {
transition: background-color 0.3s ease;
color: #000;
border: 0;
border-radius: 4px;
font-size: 15px;
font-weight: 400;
height: 37px;
line-height: 37px;
padding: 0 12px;
margin-bottom: 0;
margin-left: 5px;
}
.simpleDialog .footer .btn-confirm {
color: #fff;
background-color: #337ab7;
}
@@ -0,0 +1,56 @@
import { Component } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
import './SimpleDialog.css'
class SimpleDialog extends Component {
static defaultProps = {
componentStyle: {},
rootClass: '',
}
render() {
return (
<div
className={`simpleDialog ${this.props.rootClass}`}
ref={this.props.componentRef}
style={this.props.componentStyle}
>
<form>
<div className="header">
<span className="closeBtn" onClick={this.props.onClose}>
<span className="closeIcon">x</span>
</span>
<h4 className="title">{this.props.headerTitle}</h4>
</div>
<div className="content">{this.props.children}</div>
<div className="footer">
<button
type="button"
className="btn btn-cancel"
onClick={this.props.onClose}
>
Cancel
</button>
<button
type="button"
className="btn btn-confirm"
onClick={this.props.onConfirm}
>
Confirm
</button>
</div>
</form>
</div>
)
}
}
SimpleDialog.propTypes = {
headerTitle: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
}
export default SimpleDialog
@@ -0,0 +1,24 @@
import { connect } from 'react-redux'
import LabellingOverlay from './LabellingOverlay'
const mapStateToProps = state => {
if (!state.ui || !state.ui.labelling) {
return {
visible: false,
}
}
const labellingFlowData = state.ui.labelling
return {
visible: false,
...labellingFlowData,
}
}
const ConnectedLabellingOverlay = connect(
mapStateToProps,
null
)(LabellingOverlay)
export default ConnectedLabellingOverlay
@@ -1,35 +1,41 @@
import { connect } from 'react-redux';
import { MeasurementTable } from 'react-viewerbase';
import OHIF from 'ohif-core';
import moment from 'moment';
import { connect } from 'react-redux'
import { MeasurementTable } from 'react-viewerbase'
import OHIF from 'ohif-core'
import moment from 'moment'
import cornerstone from 'cornerstone-core'
import jumpToRowItem from '../lib/jumpToRowItem.js'
import getMeasurementLocationCallback from '../lib/getMeasurementLocationCallback'
const { setViewportSpecificData } = OHIF.redux.actions;
const { MeasurementApi } = OHIF.measurements
function groupBy(list, props) {
return list.reduce((a, b) => {
(a[b[props]] = a[b[props]] || []).push(b);
return a;
}, {});
(a[b[props]] = a[b[props]] || []).push(b)
return a
}, {})
}
function getAllTools() {
const config = OHIF.measurements.MeasurementApi.getConfiguration();
let tools = [];
const config = OHIF.measurements.MeasurementApi.getConfiguration()
let tools = []
config.measurementTools.forEach(
toolGroup => (tools = tools.concat(toolGroup.childTools))
);
)
return tools;
return tools
}
function getMeasurementText(measurementData) {
const { location, description } = measurementData;
let text = '...';
const { location, description } = measurementData
let text = '...'
if (location) {
text = location;
text = location
if (description) {
text += `(${description})`;
text += `(${description})`
}
}
return text;
return text
}
function getDataForEachMeasurementNumber(
@@ -37,51 +43,62 @@ function getDataForEachMeasurementNumber(
timepoints,
displayFunction
) {
const data = [];
const data = []
// on each measurement number we should get each measurement data by available timepoint
measurementNumberList.forEach(measurement => {
timepoints.forEach(timepoint => {
const eachData = {
displayText: '...'
};
if (measurement.timepointId === timepoint.timepointId) {
eachData.displayText = displayFunction(measurement);
displayText: '...',
}
data.push(eachData);
});
});
if (measurement.timepointId === timepoint.timepointId) {
eachData.displayText = displayFunction(measurement)
}
data.push(eachData)
})
})
return data;
return data
}
function convertMeasurementsToTableData(toolCollections, timepoints) {
const config = OHIF.measurements.MeasurementApi.getConfiguration();
const toolGroups = config.measurementTools;
const tools = getAllTools();
const config = OHIF.measurements.MeasurementApi.getConfiguration()
const toolGroups = config.measurementTools
const tools = getAllTools()
const tableMeasurements = toolGroups.map(toolGroup => {
return {
groupName: toolGroup.name,
groupId: toolGroup.id,
measurements: []
};
});
measurements: [],
}
})
Object.keys(toolCollections).forEach(toolId => {
const toolMeasurements = toolCollections[toolId];
const tool = tools.find(tool => tool.id === toolId);
const { displayFunction } = tool.options.measurementTable;
const toolMeasurements = toolCollections[toolId]
const tool = tools.find(tool => tool.id === toolId)
const { displayFunction } = tool.options.measurementTable
// Group by measurementNumber so we can display then all in the same line
const groupedMeasurements = groupBy(toolMeasurements, 'measurementNumber');
const groupedMeasurements = groupBy(toolMeasurements, 'measurementNumber')
Object.keys(groupedMeasurements).forEach(groupedMeasurementsIndex => {
const measurementNumberList =
groupedMeasurements[groupedMeasurementsIndex];
groupedMeasurements[groupedMeasurementsIndex]
const {
measurementNumber,
lesionNamingNumber,
toolType,
} = measurementNumberList[0]
const measurementId = measurementNumberList[0]._id
//check if all measurements with same measurementNumber will have same LABEL
const tableMeasurement = {
measurementId: measurementNumberList[0]._id,
label: getMeasurementText(measurementNumberList[0]),
measurementId,
measurementNumber,
lesionNamingNumber,
toolType,
hasWarnings: false, //TODO
warningTitle: '', //TODO
isSplitLesion: false, //TODO
@@ -90,48 +107,239 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
measurementNumberList,
timepoints,
displayFunction
)
};
),
}
// find the group object for the tool
const toolGroupMeasurements = tableMeasurements.find(group => {
return group.groupId === tool.toolGroup;
});
return group.groupId === tool.toolGroup
})
// inject the new measurement for this measurementNumer
toolGroupMeasurements.measurements.push(tableMeasurement);
});
});
toolGroupMeasurements.measurements.push(tableMeasurement)
})
})
return tableMeasurements;
return tableMeasurements
}
function convertTimepointsToTableData(timepoints) {
if (!timepoints || !timepoints.length) {
return [];
return []
}
return [
{
label: 'Study date:',
date: moment(timepoints[0].latestDate).format('DD-MMM-YY')
}
];
date: moment(timepoints[0].latestDate).format('DD-MMM-YY'),
},
]
}
const mapStateToProps = state => {
const { timepoints, measurements } = state.timepointManager;
const { timepoints, measurements } = state.timepointManager
return {
timepoints: convertTimepointsToTableData(timepoints),
measurementCollection: convertMeasurementsToTableData(
measurements,
timepoints
)
};
};
),
timepointManager: state.timepointManager,
viewports: state.viewports,
}
}
const mapDispatchToProps = dispatch => {
return {
dispatchRelabel: (event, measurementData, viewportsState) => {
const activeViewportIndex =
(viewportsState && viewportsState.activeViewportIndex) || 0
const enabledElements = cornerstone.getEnabledElements()
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
OHIF.log.error('Failed to find the enabled element')
return
}
const { element } = enabledElements[activeViewportIndex]
const eventData = {
event: {
clientX: event.clientX,
clientY: event.clientY,
},
element,
}
const { toolType, measurementId } = measurementData
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
return measurement._id === measurementId
})
const options = {
skipAddLabelButton: true,
editLocation: true,
}
// Clone the tool not to set empty location initially
const toolForLocation = Object.assign({}, tool, { location: null })
getMeasurementLocationCallback(eventData, toolForLocation, options)
},
dispatchEditDescription: (event, measurementData, viewportsState) => {
const activeViewportIndex =
(viewportsState && viewportsState.activeViewportIndex) || 0
const enabledElements = cornerstone.getEnabledElements()
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
OHIF.log.error('Failed to find the enabled element')
return
}
const { element } = enabledElements[activeViewportIndex]
const eventData = {
event: {
clientX: event.clientX,
clientY: event.clientY,
},
element,
}
const { toolType, measurementId } = measurementData
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
return measurement._id === measurementId
})
const options = {
editDescriptionOnDialog: true,
}
getMeasurementLocationCallback(eventData, tool, options)
},
dispatchJumpToRowItem: (
measurementData,
viewportsState,
timepointManagerState,
options
) => {
const actionData = jumpToRowItem(
measurementData,
viewportsState,
timepointManagerState,
dispatch,
options
)
actionData.viewportSpecificData.forEach(viewportSpecificData => {
const { viewportIndex, displaySet } = viewportSpecificData;
dispatch(setViewportSpecificData(viewportIndex, displaySet));
})
const { toolType, measurementNumber } = measurementData
const measurementApi = MeasurementApi.Instance
Object.keys(measurementApi.tools).forEach(toolType => {
const measurements = measurementApi.tools[toolType]
measurements.forEach(measurement => {
measurement.active = false
})
})
const measurementsToActive = measurementApi.tools[toolType].filter(
measurement => {
return measurement.measurementNumber === measurementNumber
}
)
measurementsToActive.forEach(measurementToActive => {
measurementToActive.active = true
})
measurementApi.syncMeasurementsAndToolData()
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element)
})
// Needs to update viewports.layout state to set layout
//const layout = actionData.layout;
//dispatch(setLayout(layout))
// Needs to update viewports.activeViewportIndex to the first updated viewport
//const viewportIndex = actionData.viewportIndex;
//dispatch(setViewportActive(viewportIndex));
// Needs to update timepointsManager.measurements state to set active measurementId
// TODO: Not yet implemented
//dispatch(setActiveMeasurement(measurementData.measurementId))
// (later): Needs to set some property on state.extensions.cornerstone to synchronize viewport scrolling
},
}
}
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
return {
timepoints: propsFromState.timepoints,
measurementCollection: propsFromState.measurementCollection,
selectedMeasurementNumber: ownProps.selectedMeasurementNumber,
...propsFromDispatch,
onItemClick: (event, measurementData) => {
// TODO: Add timepointId to .data for measurementData?
// TODO: Tooltype should be on the level below? This should
// provide the entire row item?
const viewportsState = propsFromState.viewports
const timepointManagerState = propsFromState.timepointManager
// TODO: invertViewportTimepointsOrder should be stored in / read from user preferences
// TODO: childToolKey should come from the measurement table when it supports child tools
const options = {
invertViewportTimepointsOrder: false,
childToolKey: null,
}
propsFromDispatch.dispatchJumpToRowItem(
measurementData,
viewportsState,
timepointManagerState,
options
)
},
onRelabelClick: (event, measurementData) => {
const viewportsState = propsFromState.viewports
propsFromDispatch.dispatchRelabel(event, measurementData, viewportsState)
},
onEditDescriptionClick: (event, measurementData) => {
const viewportsState = propsFromState.viewports
propsFromDispatch.dispatchEditDescription(
event,
measurementData,
viewportsState
)
},
onDeleteClick: (event, measurementData) => {
const { MeasurementHandlers } = OHIF.measurements;
MeasurementHandlers.onRemoved({
detail: {
toolType: measurementData.toolType,
measurementData: {
_id: measurementData.measurementId,
lesionNamingNumber: measurementData.lesionNamingNumber,
measurementNumber: measurementData.measurementNumber
}
}
});
}
}
}
const ConnectedMeasurementTable = connect(
mapStateToProps,
null
)(MeasurementTable);
mapDispatchToProps,
mergeProps
)(MeasurementTable)
export default ConnectedMeasurementTable;
export default ConnectedMeasurementTable
@@ -0,0 +1,34 @@
import { connect } from 'react-redux'
import ToolContextMenu from './ToolContextMenu'
const mapStateToProps = (state, ownProps) => {
if (!state.ui || !state.ui.contextMenu) {
return {
visible: false,
}
}
const { viewportIndex } = ownProps
const toolContextMenuData = state.ui.contextMenu[viewportIndex]
let availableTools
if (
state.extensions &&
state.extensions.cornerstone &&
state.extensions.cornerstone.availableTools
) {
availableTools = state.extensions.cornerstone.availableTools
}
return {
...toolContextMenuData,
availableTools,
}
}
const ConnectedToolContextMenu = connect(
mapStateToProps,
null
)(ToolContextMenu)
export default ConnectedToolContextMenu
@@ -0,0 +1,33 @@
import { connect } from 'react-redux';
import ViewerMain from './ViewerMain';
import OHIF from 'ohif-core';
const { setViewportSpecificData, clearViewportSpecificData } = OHIF.redux.actions;
const mapStateToProps = state => {
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
return {
activeViewportIndex,
layout,
viewportSpecificData
}
};
const mapDispatchToProps = dispatch => {
return {
setViewportSpecificData: (viewportIndex, data) => {
dispatch(setViewportSpecificData(viewportIndex, data));
},
clearViewportSpecificData: () => {
dispatch(clearViewportSpecificData());
}
};
};
const ConnectedViewerMain = connect(
mapStateToProps,
mapDispatchToProps
)(ViewerMain);
export default ConnectedViewerMain;
+32 -32
View File
@@ -1,38 +1,38 @@
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';
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'
import ConnectedViewerMain from './ConnectedViewerMain.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
};
rightSidebarOpen: PropTypes.bool.isRequired,
}
state = {
studiesForBrowser: []
};
studiesForBrowser: [],
}
componentDidMount() {
const studiesForBrowser = this.getStudiesForBrowser();
const studiesForBrowser = this.getStudiesForBrowser()
this.setState({
studiesForBrowser
});
studiesForBrowser,
})
}
getStudiesForBrowser = () => {
const { studies } = this.props;
const { studies } = this.props
// TODO[react]:
// - Add sorting of display sets
// - Add useMiddleSeriesInstanceAsThumbnail
// - Add showStackLoadingProgressBar option
return studies.map(study => {
const { studyInstanceUid } = study;
const { studyInstanceUid } = study
const thumbnails = study.displaySets.map(displaySet => {
const {
@@ -40,13 +40,13 @@ class FlexboxLayout extends Component {
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames
} = displaySet;
numImageFrames,
} = displaySet
let imageId;
let imageId
if (displaySet.images && displaySet.images.length) {
imageId = displaySet.images[0].getImageId();
imageId = displaySet.images[0].getImageId()
}
return {
@@ -55,25 +55,25 @@ class FlexboxLayout extends Component {
seriesDescription,
seriesNumber,
instanceNumber,
numImageFrames
};
});
numImageFrames,
}
})
return {
studyInstanceUid,
thumbnails
};
});
};
thumbnails,
}
})
}
render() {
let mainContentClassName = 'main-content';
let mainContentClassName = 'main-content'
if (this.props.leftSidebarOpen) {
mainContentClassName += ' sidebar-left-open';
mainContentClassName += ' sidebar-left-open'
}
if (this.props.rightSidebarOpen) {
mainContentClassName += ' sidebar-right-open';
mainContentClassName += ' sidebar-right-open'
}
// TODO[react]: Make ConnectedMeasurementTable extension with state.timepointManager
@@ -89,7 +89,7 @@ class FlexboxLayout extends Component {
<ConnectedStudyBrowser studies={this.state.studiesForBrowser} />
</div>
<div className={mainContentClassName}>
<ViewerMain studies={this.props.studies} />
<ConnectedViewerMain studies={this.props.studies} />
</div>
<div
className={
@@ -101,8 +101,8 @@ class FlexboxLayout extends Component {
<ConnectedMeasurementTable />
</div>
</div>
);
)
}
}
export default FlexboxLayout;
export default FlexboxLayout
@@ -0,0 +1,23 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import LabellingManager from '../components/Labelling/LabellingManager'
class LabellingOverlay extends Component {
static propTypes = {
visible: PropTypes.bool.isRequired,
}
static defaultProps = {
visible: false,
}
render() {
if (!this.props.visible) {
return null
}
return <LabellingManager {...this.props} />
}
}
export default LabellingOverlay
@@ -0,0 +1,34 @@
.ToolContextMenu {
position: absolute;
background-color: white;
border: 1px solid white;
border-radius: 5px;
z-index: 1000;
display: block;
width: 170px;
}
.ToolContextMenu > ul {
list-style-type: none;
padding-left: 0;
margin: 0;
}
.ToolContextMenu > ul > li > button {
padding: 10px;
font-size: 14px;
border: none;
color: #516873;
border-radius: 3px;
outline: none;
cursor: pointer;
background: none;
}
.ToolContextMenu > ul > li > button:hover {
color: #16202b;
}
.ToolContextMenu > ul > li > button:active {
color: #79f9fe;
}
+244
View File
@@ -0,0 +1,244 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import cornerstone from 'cornerstone-core'
import cornerstoneTools from 'cornerstone-tools'
import getMeasurementLocationCallback from '../lib/getMeasurementLocationCallback'
import './ToolContextMenu.css'
const toolTypes = ['Angle', 'Length']
let defaultDropdownItems = [
{
actionType: 'Delete',
action: ({ nearbyToolData, eventData }) => {
const element = eventData.element
cornerstoneTools.removeToolState(
element,
nearbyToolData.toolType,
nearbyToolData.tool
)
cornerstone.updateImage(element)
},
},
{
actionType: 'setLabel',
action: ({ nearbyToolData, eventData }) => {
const { tool } = nearbyToolData
const options = {
skipAddLabelButton: true,
editLocation: true,
}
getMeasurementLocationCallback(eventData, tool, options)
},
},
{
actionType: 'setDescription',
action: ({ nearbyToolData, eventData }) => {
const { tool } = nearbyToolData
const options = {
editDescriptionOnDialog: true,
}
getMeasurementLocationCallback(eventData, tool, options)
},
},
]
function getNearbyToolData(element, coords, toolTypes) {
const nearbyTool = {}
let pointNearTool = false
toolTypes.forEach(toolType => {
const toolData = cornerstoneTools.getToolState(element, toolType)
if (!toolData) {
return
}
toolData.data.forEach(function(data, index) {
// TODO: Fix this, it's ugly
let toolInterface = cornerstoneTools.getToolForElement(element, toolType)
if (!toolInterface) {
toolInterface = cornerstoneTools.getToolForElement(
element,
`${toolType}Tool`
)
}
if (!toolInterface) {
throw new Error('Tool not found.')
}
if (toolInterface.pointNearTool(element, data, coords)) {
pointNearTool = true
nearbyTool.tool = data
nearbyTool.index = index
nearbyTool.toolType = toolType
}
})
if (pointNearTool) {
return false
}
})
return pointNearTool ? nearbyTool : undefined
}
function getDropdownItems(eventData, isTouchEvent = false, availableTools) {
const nearbyToolData = getNearbyToolData(
eventData.element,
eventData.currentPoints.canvas,
toolTypes,
availableTools
)
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
if (
isTouchEvent &&
nearbyToolData &&
nearbyToolData.toolType === 'arrowAnnotate'
) {
return
}
let dropdownItems = []
if (nearbyToolData) {
defaultDropdownItems.forEach(function(item) {
item.params = {
eventData,
nearbyToolData,
}
if (item.actionType === 'Delete') {
item.text = 'Delete measurement'
}
if (item.actionType === 'setLabel') {
item.text = 'Relabel'
}
if (item.actionType === 'setDescription') {
item.text = `${
nearbyToolData.tool.description ? 'Edit' : 'Add'
} Description`
}
dropdownItems.push(item)
})
}
return dropdownItems
}
class ToolContextMenu extends Component {
static propTypes = {
isTouchEvent: PropTypes.bool.isRequired,
eventData: PropTypes.object,
onClose: PropTypes.func,
availableTools: PropTypes.array,
visible: PropTypes.bool.isRequired,
}
static defaultProps = {
visible: true,
isTouchEvent: false,
}
constructor(props) {
super(props)
this.mainElement = React.createRef()
}
render() {
if (!this.props.eventData) {
return null
}
const { isTouchEvent, eventData, availableTools } = this.props
const dropdownItems = getDropdownItems(
eventData,
isTouchEvent,
availableTools
)
// Skip if there is no dropdown item
if (!dropdownItems.length) {
return ''
}
const dropdownComponents = dropdownItems.map(item => {
const itemOnClick = event => {
item.action(item.params)
if (this.props.onClose) {
this.props.onClose()
}
}
return (
<li key={item.actionType}>
<button className="form-action" onClick={itemOnClick}>
<span key={item.actionType}>{item.text}</span>
</button>
</li>
)
})
const position = {
top: `${eventData.currentPoints.canvas.y}px`,
left: `${eventData.currentPoints.canvas.x}px`,
}
return (
<div className="ToolContextMenu" style={position} ref={this.mainElement}>
<ul className="bounded">{dropdownComponents}</ul>
</div>
)
}
componentDidMount = () => {
if (this.mainElement.current) {
this.updateElementPosition()
}
}
componentDidUpdate = () => {
if (this.mainElement.current) {
this.updateElementPosition()
}
}
updateElementPosition = () => {
const {
offsetParent,
offsetTop,
offsetHeight,
offsetWidth,
offsetLeft,
} = this.mainElement.current
const { eventData } = this.props
if (offsetTop + offsetHeight > offsetParent.offsetHeight) {
const offBoundPixels =
offsetTop + offsetHeight - offsetParent.offsetHeight
const top = eventData.currentPoints.canvas.y - offBoundPixels
this.mainElement.current.style.top = `${top > 0 ? top : 0}px`
}
if (offsetLeft + offsetWidth > offsetParent.offsetWidth) {
const offBoundPixels = offsetLeft + offsetWidth - offsetParent.offsetWidth
const left = eventData.currentPoints.canvas.x - offBoundPixels
this.mainElement.current.style.left = `${left > 0 ? left : 0}px`
}
}
}
export default ToolContextMenu
+2
View File
@@ -9,6 +9,7 @@ import WhiteLabellingContext from '../WhiteLabellingContext.js';
import ConnectedHeader from './ConnectedHeader.js';
import ConnectedFlexboxLayout from './ConnectedFlexboxLayout.js';
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
import './Viewer.css';
/**
* Inits OHIF Hanging Protocol's onReady.
@@ -184,6 +185,7 @@ class Viewer extends Component {
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
{/*<StudyPrefetcher studies={this.props.studies} />*/}
<ConnectedFlexboxLayout studies={this.props.studies} />
<ConnectedLabellingOverlay />
</div>
</>
);
+38 -19
View File
@@ -6,23 +6,28 @@ import ConnectedLayoutManager from './ConnectedLayoutManager.js';
import './ViewerMain.css';
class ViewerMain extends Component {
state = {
viewportData: []
};
static propTypes = {
studies: PropTypes.array.isRequired
studies: PropTypes.array.isRequired,
setViewportSpecificData: PropTypes.func.isRequired,
clearViewportSpecificData: PropTypes.func.isRequired
};
constructor(props) {
super(props);
OHIF.hotkeysUtil.setup('viewer');
this.state = {
displaySets: []
}
}
getDisplaySets(studies) {
const displaySets = [];
studies.forEach(study => {
study.displaySets.forEach(dSet => {
if (!dSet.plugin) {
dSet.plugin = 'cornerstone';
}
displaySets.push(dSet);
});
});
@@ -52,29 +57,37 @@ class ViewerMain extends Component {
const displaySets = this.getDisplaySets(this.props.studies);
this.setState({
viewportData: displaySets
displaySets
});
}
getViewportData = () => {
const viewportData = [];
const { layout, viewportSpecificData } = this.props;
for (let viewportIndex = 0; viewportIndex < layout.viewports.length; viewportIndex++) {
let displaySet = viewportSpecificData[viewportIndex];
// If the viewport is empty, get one available in study
if (!displaySet || !displaySet.displaySetInstanceUid) {
const { displaySets } = this.state;
displaySet = displaySets.find(ds => !viewportData.some(v => v.displaySetInstanceUid === ds.displaySetInstanceUid));
}
viewportData.push(displaySet);
}
return viewportData;
};
setViewportData = ({ viewportIndex, item }) => {
// TODO: Replace this with mapDispatchToProps call
// if we decide to put viewport info into redux
// Note: Use Slice because React does a shallow equality check. Mutating the array
// would not trigger a re-render. We have to create a copy.
const updatedViewportData = this.state.viewportData.slice(0);
const displaySet = this.findDisplaySet(
this.props.studies,
item.studyInstanceUid,
item.displaySetInstanceUid
);
updatedViewportData[viewportIndex] = Object.assign({}, displaySet);
this.setState({
viewportData: updatedViewportData
});
this.props.setViewportSpecificData(viewportIndex, displaySet);
};
render() {
@@ -82,7 +95,7 @@ class ViewerMain extends Component {
<div className="ViewerMain">
<ConnectedLayoutManager
studies={this.props.studies}
viewportData={this.state.viewportData}
viewportData={this.getViewportData()}
setViewportData={this.setViewportData}
/>
</div>
@@ -90,6 +103,12 @@ class ViewerMain extends Component {
}
componentWillUnmount() {
// Clear the entire viewport specific data
const { viewportSpecificData } = this.props;
Object.keys(viewportSpecificData).forEach((viewportIndex) => {
this.props.clearViewportSpecificData(viewportIndex);
});
// Remove beforeUnload event handler...
//window.removeEventListener('beforeunload', unloadHandlers.beforeUnload);
// Destroy the synchronizer used to update reference lines
+21
View File
@@ -0,0 +1,21 @@
import cornerstoneTools from 'cornerstone-tools'
import updateTableWithNewMeasurementData from './updateTableWithNewMeasurementData'
export default function getMeasurementLocationCallback(
eventData,
tool,
options
) {
const { toolType } = tool
const { element } = eventData
const doneCallback = updateTableWithNewMeasurementData
const ToolInstance = cornerstoneTools.getToolForElement(element, toolType)
ToolInstance.configuration.getMeasurementLocationCallback(
tool,
eventData,
doneCallback,
options
)
}
+110
View File
@@ -0,0 +1,110 @@
import { OHIF } from 'ohif-core'
// TODO: Move this function to OHIF itself so we can use it on the OHIF measurment table (when it is finished)
/**
* Activates a set of measurements
*
* @param measurementData
* @param viewportsState
* @param timepointManagerState
* @param options
*/
export default function jumpToRowItem(
measurementData,
viewportsState,
timepointManagerState,
options = { invertViewportTimepointsOrder: false, childToolKey: null }
) {
const numViewports = viewportsState.layout.viewports.length
const numTimepoints = timepointManagerState.timepoints.length
const { measurements, timepoints } = timepointManagerState
const numViewportsToUpdate = Math.min(numTimepoints, numViewports)
const { toolType, measurementNumber } = measurementData
if (options.invertViewportTimepointsOrder) {
timepoints.reverse()
}
const measurementsForToolGroup = measurements[toolType]
// Retrieve the measurements data
const measurementsToJumpTo = []
for (let i = 0; i < numViewportsToUpdate; i++) {
const { timepointId } = timepoints[i]
const dataAtThisTimepoint = measurementsForToolGroup.find(entry => {
return (
entry.timepointId === timepointId &&
entry.measurementNumber === measurementNumber
)
})
if (!dataAtThisTimepoint) {
measurementsToJumpTo.push(null)
continue
}
let measurement = dataAtThisTimepoint
const { tool } = OHIF.measurements.MeasurementApi.getToolConfiguration(
toolType
)
if (options.childToolKey) {
measurement = dataAtThisTimepoint[options.childToolKey]
} else if (Array.isArray(tool.childTools)) {
const key = tool.childTools.find(key => !!dataAtThisTimepoint[key])
measurement = dataAtThisTimepoint[key]
}
measurementsToJumpTo.push(measurement)
}
// TODO: Add a single viewports state action which allows
// - viewportData to be set
// - layout to be set
// - activeViewport to be set
// Needs to update viewports.viewportData state to set image set data
const displaySetContainsSopInstance = (displaySet, sopInstanceUid) =>
displaySet.images.find(
image => image.getSOPInstanceUID() === sopInstanceUid
)
const viewportSpecificData = []
measurementsToJumpTo.forEach((data, viewportIndex) => {
// Skip if there is no measurement to jump
if (!data) {
return
}
const study = OHIF.utils.studyMetadataManager.get(data.studyInstanceUid)
if (!study) {
throw new Error('Study not found.')
}
const displaySet = study.findDisplaySet(displaySet => {
return displaySetContainsSopInstance(displaySet, data.sopInstanceUid)
})
if (!displaySet) {
throw new Error('Display set not found.')
}
displaySet.sopInstanceUid = data.sopInstanceUid
if (data.frameIndex) {
displaySet.frameIndex = data.frameIndex
}
viewportSpecificData.push({
viewportIndex,
displaySet,
})
})
return {
viewportSpecificData,
layout: [], // TODO: if we need to change layout, we should return this here
}
}
@@ -0,0 +1,29 @@
import OHIF from 'ohif-core'
import cornerstone from 'cornerstone-core'
export default function updateTableWithNewMeasurementData({
toolType,
measurementNumber,
location,
description,
}) {
// Update all measurements by measurement number
const measurementApi = OHIF.measurements.MeasurementApi.Instance
const measurements = measurementApi.tools[toolType].filter(
m => m.measurementNumber === measurementNumber
)
measurements.forEach(measurement => {
measurement.location = location
measurement.description = description
measurementApi.updateMeasurement(measurement.toolType, measurement)
})
measurementApi.syncMeasurementsAndToolData()
// Update images in all active viewports
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element)
})
}
+46
View File
@@ -0,0 +1,46 @@
export default function bounding(elementRef, currentPosition = {}) {
if (!elementRef.current) {
return
}
const currentElement = elementRef.current
const {
offsetParent,
offsetTop,
offsetHeight,
offsetLeft,
offsetWidth,
} = currentElement
let top = currentPosition.top || offsetTop
let left = currentPosition.left || offsetLeft
if (!offsetParent) {
return
}
let maxHeight = `${offsetParent.offsetHeight}px`
if (offsetHeight + top > offsetParent.offsetHeight) {
top = top - (offsetHeight + top - offsetParent.offsetHeight)
if (top < 0) {
top = 0
}
}
if (left + offsetWidth > offsetParent.offsetWidth) {
left = offsetParent.offsetWidth - offsetWidth
if (left < 0) {
left = 0
}
}
if (maxHeight && currentElement.style.maxHeight !== maxHeight) {
currentElement.style.maxHeight = maxHeight
}
if (currentElement.style.top !== `${top}px`) {
currentElement.style.top = `${top}px`
}
if (currentElement.style.left !== `${left}px`) {
currentElement.style.left = `${left}px`
}
}
+31 -11
View File
@@ -1,22 +1,42 @@
const defaultState = {
leftSidebarOpen: true,
rightSidebarOpen: false,
userPreferencesModalOpen: false
};
userPreferencesModalOpen: false,
labelling: {},
contextMenu: {},
}
const ui = (state = defaultState, action) => {
switch (action.type) {
case 'SET_LEFT_SIDEBAR_OPEN':
return Object.assign({}, state, { leftSidebarOpen: action.state });
return Object.assign({}, state, { leftSidebarOpen: action.state })
case 'SET_RIGHT_SIDEBAR_OPEN':
return Object.assign({}, state, { rightSidebarOpen: action.state });
return Object.assign({}, state, { rightSidebarOpen: action.state })
case 'SET_USER_PREFERENCES_MODAL_OPEN':
return Object.assign({}, state, {
userPreferencesModalOpen: action.state
});
default:
return state;
}
};
userPreferencesModalOpen: action.state,
})
case 'SET_LABELLING_FLOW_DATA':
const labelling = Object.assign({}, action.labellingFlowData)
export default ui;
return Object.assign({}, state, { labelling })
case 'SET_TOOL_CONTEXT_MENU_DATA':
const contextMenu = Object.assign({}, state.contextMenu)
contextMenu[action.viewportIndex] = Object.assign(
{},
action.toolContextMenuData
)
return Object.assign({}, state, { contextMenu })
case 'RESET_LABELLING_AND_CONTEXT_MENU':
return Object.assign({}, state, {
labelling: defaultState.labelling,
contextMenu: defaultState.contextMenu,
})
default:
return state
}
}
export default ui
+89
View File
@@ -0,0 +1,89 @@
import OHIF from 'ohif-core'
const Icons = 'icons.svg'
export default function setupButtons(store) {
const defaultButtons = [
{
command: 'StackScroll',
type: 'tool',
text: 'Stack Scroll',
svgUrl: `${Icons}#icon-tools-stack-scroll`,
active: false,
},
{
command: 'Zoom',
type: 'tool',
text: 'Zoom',
svgUrl: `${Icons}#icon-tools-zoom`,
active: false,
},
{
command: 'Wwwc',
type: 'tool',
text: 'Levels',
svgUrl: `${Icons}#icon-tools-levels`,
active: true,
},
{
command: 'Pan',
type: 'tool',
text: 'Pan',
svgUrl: `${Icons}#icon-tools-pan`,
active: false,
},
{
command: 'Length',
type: 'tool',
text: 'Length',
svgUrl: `${Icons}#icon-tools-measure-temp`,
active: false,
},
/*{
command: 'Annotate',
type: 'tool',
text: 'Annotate',
svgUrl: `${Icons}#icon-tools-measure-non-target`,
active: false
},*/
{
command: 'Angle',
type: 'tool',
text: 'Angle',
iconClasses: 'fa fa-angle-left',
active: false,
},
{
command: 'Bidirectional',
type: 'tool',
text: 'Bidirectional',
svgUrl: `${Icons}#icon-tools-measure-target`,
active: false,
},
{
command: 'Brush',
type: 'tool',
text: 'Brush',
iconClasses: 'fa fa-circle',
active: false,
},
{
command: 'FreehandMouse',
type: 'tool',
text: 'Freehand',
iconClasses: 'fa fa-star',
active: false,
},
{
command: 'reset',
type: 'command',
text: 'Reset',
svgUrl: `${Icons}#icon-tools-reset`,
active: false,
},
]
const buttonsAction = OHIF.redux.actions.setAvailableButtons(defaultButtons)
store.dispatch(buttonsAction)
}
+190
View File
@@ -0,0 +1,190 @@
import OHIF from 'ohif-core'
import updateTableWithNewMeasurementData from './lib/updateTableWithNewMeasurementData'
function getToolLabellingFlowCallback(store) {
const setLabellingFlowDataAction = labellingFlowData => ({
type: 'SET_LABELLING_FLOW_DATA',
labellingFlowData,
})
const setLabellingFlowData = labellingFlowData => {
store.dispatch(setLabellingFlowDataAction(labellingFlowData))
}
return function toolLabellingFlowCallback(
measurementData,
eventData,
doneCallback,
options = {}
) {
const updateLabelling = ({ location, response, description }) => {
// Update the measurement data with the labelling parameters
if (location) {
measurementData.location = location
}
if (description) {
measurementData.description = description
}
if (response) {
measurementData.response = response
}
updateTableWithNewMeasurementData(measurementData)
}
const labellingDoneCallback = () => {
setLabellingFlowData({ visible: false })
}
const labellingFlowData = {
visible: true,
eventData,
measurementData,
skipAddLabelButton: options.skipAddLabelButton,
editLocation: options.editLocation,
editDescription: options.editDescription,
editResponse: options.editResponse,
editDescriptionOnDialog: options.editDescriptionOnDialog,
labellingDoneCallback,
updateLabelling,
}
setLabellingFlowData(labellingFlowData)
}
}
const resetLabellingAndContextMenuAction = state => ({
type: 'RESET_LABELLING_AND_CONTEXT_MENU',
state,
})
const setToolContextMenuDataAction = (viewportIndex, toolContextMenuData) => ({
type: 'SET_TOOL_CONTEXT_MENU_DATA',
viewportIndex,
toolContextMenuData,
})
function getOnRightClickCallback(store) {
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
store.dispatch(resetLabellingAndContextMenuAction())
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
)
}
const getOnCloseCallback = viewportIndex => {
return function onClose() {
const toolContextMenuData = {
visible: false,
}
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
)
}
}
return function onRightClick(event) {
const eventData = event.detail
const viewportIndex = parseInt(eventData.element.dataset.viewportIndex, 10)
const toolContextMenuData = {
eventData,
isTouchEvent: false,
onClose: getOnCloseCallback(viewportIndex),
}
setToolContextMenuData(viewportIndex, toolContextMenuData)
}
}
function getOnTouchPressCallback(store) {
const setToolContextMenuData = (viewportIndex, toolContextMenuData) => {
store.dispatch(resetLabellingAndContextMenuAction())
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
)
}
const getOnCloseCallback = viewportIndex => {
return function onClose() {
const toolContextMenuData = {
visible: false,
}
store.dispatch(
setToolContextMenuDataAction(viewportIndex, toolContextMenuData)
)
}
}
return function onTouchPress(event) {
const eventData = event.detail
const viewportIndex = parseInt(eventData.element.dataset.viewportIndex, 10)
const toolContextMenuData = {
eventData,
isTouchEvent: true,
onClose: getOnCloseCallback(viewportIndex),
}
setToolContextMenuData(viewportIndex, toolContextMenuData)
}
}
function getResetLabellingAndContextMenu(store) {
return function resetLabellingAndContextMenu() {
store.dispatch(resetLabellingAndContextMenuAction())
}
}
export default function setupTools(store) {
const toolLabellingFlowCallback = getToolLabellingFlowCallback(store)
const availableTools = [
{ name: 'Pan', mouseButtonMasks: [1, 4] },
{ name: 'Zoom', mouseButtonMasks: [1, 2] },
{ name: 'Wwwc', mouseButtonMasks: [1] },
{
name: 'Length',
configuration: {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
},
mouseButtonMasks: [1],
},
{
name: 'Angle',
configuration: {
configuration: {
getMeasurementLocationCallback: toolLabellingFlowCallback,
},
},
mouseButtonMasks: [1],
},
{ name: 'StackScroll', mouseButtonMasks: [1] },
{ name: 'Brush', mouseButtonMasks: [1] },
{ name: 'FreehandMouse', mouseButtonMasks: [1] },
{ name: 'PanMultiTouch' },
{ name: 'ZoomTouchPinch' },
{ name: 'StackScrollMouseWheel' },
{ name: 'StackScrollMultiTouch' },
]
const onRightClick = getOnRightClickCallback(store)
const onTouchPress = getOnTouchPressCallback(store)
const onNewImage = getResetLabellingAndContextMenu(store)
const onMouseClick = getResetLabellingAndContextMenu(store)
const onTouchStart = getResetLabellingAndContextMenu(store)
const toolAction = OHIF.redux.actions.setExtensionData('cornerstone', {
availableTools,
onNewImage,
onRightClick,
onTouchPress,
onTouchStart,
onMouseClick,
})
store.dispatch(toolAction)
}
-7
View File
@@ -60,13 +60,6 @@ export default function(baseDirectory = '/') {
iconClasses: 'fa fa-angle-left',
active: false,
},
{
command: 'Bidirectional',
type: 'tool',
text: 'Bidirectional',
svgUrl: `${relativePathToIcons}#icon-tools-measure-target`,
active: false,
},
{
command: 'Brush',
type: 'tool',