Merge branch 'react' into docs/orthanc-proxy
This commit is contained in:
33 files changed
+2219
-151
No files matched your search
+7
-21
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user