diff --git a/package.json b/package.json
index 2c99fbe31..b42c7107c 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
"hammerjs": "^2.0.8",
"lodash.isequal": "4.5.0",
"moment": "^2.24.0",
- "ohif-core": "0.4.1",
+ "ohif-core": "0.4.2",
"ohif-cornerstone-extension": "^0.0.28",
"ohif-dicom-html-extension": "^0.0.2",
"ohif-dicom-microscopy-extension": "^0.0.5",
diff --git a/public/icons.svg b/public/icons.svg
index e8abd6eb4..3b0690b4f 100644
--- a/public/icons.svg
+++ b/public/icons.svg
@@ -237,4 +237,10 @@
+
+
+
+
+
+
diff --git a/src/App.js b/src/App.js
index e9dff75ce..9ec2fa3d7 100644
--- a/src/App.js
+++ b/src/App.js
@@ -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: [],
+}
/** 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(),
diff --git a/src/components/EditDescriptionDialog/EditDescriptionDialog.css b/src/components/EditDescriptionDialog/EditDescriptionDialog.css
new file mode 100644
index 000000000..9d3de80fd
--- /dev/null
+++ b/src/components/EditDescriptionDialog/EditDescriptionDialog.css
@@ -0,0 +1,6 @@
+.editDescriptionDialog {
+ position: absolute;
+ z-index: 300;
+ width: 320px;
+ transition: all 300ms linear;
+}
diff --git a/src/components/EditDescriptionDialog/EditDescriptionDialog.js b/src/components/EditDescriptionDialog/EditDescriptionDialog.js
new file mode 100644
index 000000000..a90fcfa19
--- /dev/null
+++ b/src/components/EditDescriptionDialog/EditDescriptionDialog.js
@@ -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 (
+
+
+
+ )
+ }
+
+ onClose = () => {
+ this.props.onCancel()
+ }
+
+ onConfirm = () => {
+ this.props.onUpdate(this.state.description)
+ }
+
+ handleChange = event => {
+ this.setState({ description: event.target.value })
+ }
+}
diff --git a/src/components/Labelling/LabellingFlow.js b/src/components/Labelling/LabellingFlow.js
new file mode 100644
index 000000000..0f6185d7c
--- /dev/null
+++ b/src/components/Labelling/LabellingFlow.js
@@ -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 (
+
+
+ {this.labellingStateFragment()}
+
+
+ )
+ }
+
+ labellingStateFragment = () => {
+ const {
+ skipAddLabelButton,
+ editLocation,
+ description,
+ locationLabel,
+ } = this.state
+
+ if (!skipAddLabelButton) {
+ return (
+ <>
+
+ >
+ )
+ } else {
+ if (editLocation) {
+ return (
+
+ )
+ } else {
+ return (
+ <>
+
+
+
+
+
{locationLabel}
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+ }
+ }
+ }
+
+ 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)
+ }
+}
diff --git a/src/components/Labelling/LabellingManager.css b/src/components/Labelling/LabellingManager.css
new file mode 100644
index 000000000..5a14e1cb4
--- /dev/null
+++ b/src/components/Labelling/LabellingManager.css
@@ -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;
+}
diff --git a/src/components/Labelling/LabellingManager.js b/src/components/Labelling/LabellingManager.js
new file mode 100644
index 000000000..e293202ce
--- /dev/null
+++ b/src/components/Labelling/LabellingManager.js
@@ -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 (
+
+ )
+ }
+
+ if (editLocation || editDescription) {
+ return (
+
+ )
+ }
+ }
+
+ 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,
+ }
+}
diff --git a/src/components/Labelling/LabellingTransition.css b/src/components/Labelling/LabellingTransition.css
new file mode 100644
index 000000000..bb46bd9b9
--- /dev/null
+++ b/src/components/Labelling/LabellingTransition.css
@@ -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;
+}
diff --git a/src/components/Labelling/LabellingTransition.js b/src/components/Labelling/LabellingTransition.js
new file mode 100644
index 000000000..dbba1702d
--- /dev/null
+++ b/src/components/Labelling/LabellingTransition.js
@@ -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 (
+
+ {this.props.children}
+
+ )
+ }
+}
diff --git a/src/components/Labelling/OHIFLabellingData.js b/src/components/Labelling/OHIFLabellingData.js
new file mode 100644
index 000000000..060caa78a
--- /dev/null
+++ b/src/components/Labelling/OHIFLabellingData.js
@@ -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
diff --git a/src/components/Labelling/labellingPositionUtils.js b/src/components/Labelling/labellingPositionUtils.js
new file mode 100644
index 000000000..50fdc8e0a
--- /dev/null
+++ b/src/components/Labelling/labellingPositionUtils.js
@@ -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
+}
diff --git a/src/components/SimpleDialog/SimpleDialog.css b/src/components/SimpleDialog/SimpleDialog.css
new file mode 100644
index 000000000..a40b6ab17
--- /dev/null
+++ b/src/components/SimpleDialog/SimpleDialog.css
@@ -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;
+}
diff --git a/src/components/SimpleDialog/SimpleDialog.js b/src/components/SimpleDialog/SimpleDialog.js
new file mode 100644
index 000000000..3cb54fe89
--- /dev/null
+++ b/src/components/SimpleDialog/SimpleDialog.js
@@ -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 (
+
+ )
+ }
+}
+
+SimpleDialog.propTypes = {
+ headerTitle: PropTypes.string.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onConfirm: PropTypes.func.isRequired,
+}
+
+export default SimpleDialog
diff --git a/src/connectedComponents/ConnectedLabellingOverlay.js b/src/connectedComponents/ConnectedLabellingOverlay.js
new file mode 100644
index 000000000..dd2a728f1
--- /dev/null
+++ b/src/connectedComponents/ConnectedLabellingOverlay.js
@@ -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
diff --git a/src/connectedComponents/ConnectedMeasurementTable.js b/src/connectedComponents/ConnectedMeasurementTable.js
index 4c2df680e..c474f601a 100644
--- a/src/connectedComponents/ConnectedMeasurementTable.js
+++ b/src/connectedComponents/ConnectedMeasurementTable.js
@@ -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
diff --git a/src/connectedComponents/ConnectedToolContextMenu.js b/src/connectedComponents/ConnectedToolContextMenu.js
new file mode 100644
index 000000000..429ea347d
--- /dev/null
+++ b/src/connectedComponents/ConnectedToolContextMenu.js
@@ -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
diff --git a/src/connectedComponents/ConnectedViewerMain.js b/src/connectedComponents/ConnectedViewerMain.js
new file mode 100644
index 000000000..211a99146
--- /dev/null
+++ b/src/connectedComponents/ConnectedViewerMain.js
@@ -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;
diff --git a/src/connectedComponents/FlexboxLayout.js b/src/connectedComponents/FlexboxLayout.js
index ead9bc3e1..6e826b8fe 100644
--- a/src/connectedComponents/FlexboxLayout.js
+++ b/src/connectedComponents/FlexboxLayout.js
@@ -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 {
-
+
- );
+ )
}
}
-export default FlexboxLayout;
+export default FlexboxLayout
diff --git a/src/connectedComponents/LabellingOverlay.js b/src/connectedComponents/LabellingOverlay.js
new file mode 100644
index 000000000..564e35f73
--- /dev/null
+++ b/src/connectedComponents/LabellingOverlay.js
@@ -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
+ }
+}
+
+export default LabellingOverlay
diff --git a/src/connectedComponents/ToolContextMenu.css b/src/connectedComponents/ToolContextMenu.css
new file mode 100644
index 000000000..42bd2ca6f
--- /dev/null
+++ b/src/connectedComponents/ToolContextMenu.css
@@ -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;
+}
diff --git a/src/connectedComponents/ToolContextMenu.js b/src/connectedComponents/ToolContextMenu.js
new file mode 100644
index 000000000..b37902d39
--- /dev/null
+++ b/src/connectedComponents/ToolContextMenu.js
@@ -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 (
+
+
+
+ )
+ })
+
+ const position = {
+ top: `${eventData.currentPoints.canvas.y}px`,
+ left: `${eventData.currentPoints.canvas.x}px`,
+ }
+
+ return (
+
+ )
+ }
+
+ 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
diff --git a/src/connectedComponents/Viewer.js b/src/connectedComponents/Viewer.js
index 76ee40075..8176d9fd6 100644
--- a/src/connectedComponents/Viewer.js
+++ b/src/connectedComponents/Viewer.js
@@ -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 {
{/**/}
{/**/}
+
>
);
diff --git a/src/connectedComponents/ViewerMain.js b/src/connectedComponents/ViewerMain.js
index f33f60185..93dd9395b 100644
--- a/src/connectedComponents/ViewerMain.js
+++ b/src/connectedComponents/ViewerMain.js
@@ -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 {
@@ -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
diff --git a/src/lib/getMeasurementLocationCallback.js b/src/lib/getMeasurementLocationCallback.js
new file mode 100644
index 000000000..3a03bc9b3
--- /dev/null
+++ b/src/lib/getMeasurementLocationCallback.js
@@ -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
+ )
+}
diff --git a/src/lib/jumpToRowItem.js b/src/lib/jumpToRowItem.js
new file mode 100644
index 000000000..9b668c3c7
--- /dev/null
+++ b/src/lib/jumpToRowItem.js
@@ -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
+ }
+}
diff --git a/src/lib/updateTableWithNewMeasurementData.js b/src/lib/updateTableWithNewMeasurementData.js
new file mode 100644
index 000000000..534ccdc8d
--- /dev/null
+++ b/src/lib/updateTableWithNewMeasurementData.js
@@ -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)
+ })
+}
diff --git a/src/lib/utils/bounding.js b/src/lib/utils/bounding.js
new file mode 100644
index 000000000..6784e5155
--- /dev/null
+++ b/src/lib/utils/bounding.js
@@ -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`
+ }
+}
diff --git a/src/redux/ui.js b/src/redux/ui.js
index 74205b3ec..a58a596f5 100644
--- a/src/redux/ui.js
+++ b/src/redux/ui.js
@@ -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
diff --git a/src/setupButtons.js b/src/setupButtons.js
new file mode 100644
index 000000000..418f8db37
--- /dev/null
+++ b/src/setupButtons.js
@@ -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)
+}
diff --git a/src/setupTools.js b/src/setupTools.js
new file mode 100644
index 000000000..8133780dc
--- /dev/null
+++ b/src/setupTools.js
@@ -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)
+}
diff --git a/src/utils/getDefaultToolbarButtons.js b/src/utils/getDefaultToolbarButtons.js
index 657e057b7..84ff9e958 100644
--- a/src/utils/getDefaultToolbarButtons.js
+++ b/src/utils/getDefaultToolbarButtons.js
@@ -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',
diff --git a/yarn.lock b/yarn.lock
index ca5101c36..5df38551e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9603,10 +9603,10 @@ octokit-pagination-methods@^1.1.0:
resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4"
integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==
-ohif-core@0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/ohif-core/-/ohif-core-0.4.1.tgz#3a2b0b92b22f8da80b04fe487dc1faf6c0c2ed6c"
- integrity sha512-QJRnx3NDf45a655JZQMCNj9tLtyh5YaH7X/tGPsA0Y2zPWEKxWKAQQDUE+TCZNsec135mB+xxYw++a/GkqLX0A==
+ohif-core@0.4.2:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/ohif-core/-/ohif-core-0.4.2.tgz#1ed2db0cddbd9ac15d5c97b9c789793d791be258"
+ integrity sha512-Wddeta9hKZDe1yyPp2QgtxhL0nM3vSudq52gRXcIVmtl0xCHzRrWqPqCTX10R3BU8OUXsHEF66RQ2gSVJmjH7g==
dependencies:
"@babel/runtime" "^7.2.0"
ajv "^6.10.0"