itk-viewer-bootstrap-ui
Version:
[](https://badge.fury.io/js/itk-viewer-bootstrap-ui)  [ • 7.64 kB
JSX
import React, { useEffect, useRef, useState } from 'react'
import { useSelector } from '@xstate/react'
import { interpolationIconDataUri } from '@itk-viewer/icons'
import ColorMapIconSelector from './ColorMapIconSelector'
import WindowLevelReset from './WindowLevelReset'
import '../style.css'
import Button from 'react-bootstrap/Button'
import Form from 'react-bootstrap/Form'
import Image from 'react-bootstrap/Image'
import OverlayTrigger from 'react-bootstrap/OverlayTrigger'
import Tooltip from 'react-bootstrap/Tooltip'
import cn from 'classnames'
import getSelectedImageContext from './getSelectedImageContext'
function ColorRangeInput(props) {
const { service } = props
const colorRangeInput = useRef(null)
const interpolationButton = useRef(null)
const [input1Tooltip, setInput1Tooltip] = useState('Color range min')
const [input2Tooltip, setInput2Tooltip] = useState('Color range max')
const send = service.send
const name = useSelector(
service,
(state) => state.context.images.selectedName
)
const interpolationEnabled = useSelector(
service,
(state) =>
state.context.images.actorContext.get(state.context.images.selectedName)
.interpolationEnabled
)
const colorRanges = useSelector(
service,
(state) =>
state.context.images.actorContext.get(state.context.images.selectedName)
.colorRanges
)
const colorRangeBounds = useSelector(
service,
(state) =>
state.context.images.actorContext.get(state.context.images.selectedName)
.colorRangeBounds
)
const selectedComponent = useSelector(
service,
(state) =>
state.context.images.actorContext.get(state.context.images.selectedName)
.selectedComponent
)
const windowLevelEnabled = useSelector(
service,
(state) =>
state.context.images.actorContext.get(state.context.images.selectedName)
.windowLevelEnabled
)
const colorRangesSelected = useSelector(service, (state) =>
state.context.images.actorContext
.get(state.context.images.selectedName)
.colorRanges.get(selectedComponent)
)
const boundsSelected = useSelector(service, (state) =>
state.context.images.actorContext
.get(state.context.images.selectedName)
.colorRangeBounds.get(selectedComponent)
)
const actorContext = useSelector(service, (state) =>
getSelectedImageContext(state)
)
const imageType = actorContext.image?.imageType.componentType
useEffect(() => {
service.machine.context.images.colorRangeInputRow = colorRangeInput.current
}, [service.machine.context.images])
useEffect(() => {
setInput1Tooltip(windowLevelEnabled ? 'Window width' : 'Color range min')
setInput2Tooltip(windowLevelEnabled ? 'Window level' : 'Color range max')
}, [windowLevelEnabled])
const toggleInterpolate = () => {
send({
type: 'TOGGLE_IMAGE_INTERPOLATION',
data: name
})
}
const currentRange =
colorRanges.size && colorRangesSelected ? colorRangesSelected : [0, 1]
const currentRangeMin = currentRange[0]
const currentRangeMax = currentRange[1]
const currentWidth = currentRange[1] - currentRange[0]
const currentLevel = (currentRange[1] + currentRange[0]) / 2
const currentBounds = colorRangeBounds.size ? boundsSelected : [0, 1]
let [rangeMin, rangeMax] = [0, 0]
if (
actorContext.componentVisibilities.length === 1 &&
actorContext.selectedComponent
) {
;[rangeMin, rangeMax] =
actorContext.image.scaleInfo[actorContext.loadedScale].ranges[
actorContext.selectedComponent
]
} else if (
actorContext.componentVisibilities.length > 1 &&
actorContext.colorRangeBounds.get(actorContext.selectedComponent)
) {
;[rangeMin, rangeMax] = actorContext.colorRangeBounds.get(
actorContext.selectedComponent
)
}
const rangeStep =
imageType?.slice(0, 5) === 'float' ? (rangeMax - rangeMin) / 200 : 1
const windowingStep =
10 ** Math.ceil(Math.log((currentBounds[1] - currentBounds[0]) / 1000))
const [in1, setIn1] = useState('')
const [in2, setIn2] = useState('')
// update input box after machine changes
useEffect(() => {
if (!windowLevelEnabled) setIn1(currentRangeMin)
}, [currentRangeMin, windowLevelEnabled])
useEffect(() => {
if (!windowLevelEnabled) setIn2(currentRangeMax)
}, [currentRangeMax, windowLevelEnabled])
useEffect(() => {
if (windowLevelEnabled) setIn1(currentWidth)
}, [currentWidth, windowLevelEnabled])
useEffect(() => {
if (windowLevelEnabled) setIn2(currentLevel)
}, [currentLevel, windowLevelEnabled])
const isValidBounds = (input1, input2) => {
if (isNaN(input1) || isNaN(input2)) {
return false
}
if (windowLevelEnabled) {
return input1 > 0
}
return input1 < input2
}
const inputChanged = (input1, input2) => {
let rangeMin
let rangeMax
if (windowLevelEnabled) {
rangeMin = input2 - input1 / 2
rangeMax = input2 + input1 / 2
} else {
const bounds = boundsSelected
rangeMax = input2 >= bounds[1] ? bounds[1] : input2
rangeMin = input1 <= bounds[0] ? bounds[0] : input1
}
send({
type: 'IMAGE_COLOR_RANGE_CHANGED',
data: {
name,
component: selectedComponent,
range: [rangeMin, rangeMax]
}
})
}
const input1Changed = (val) => {
setIn1(val)
let input1 = parseFloat(val)
let input2 = windowLevelEnabled ? currentLevel : currentRangeMax
if (isValidBounds(input1, input2)) {
inputChanged(input1, input2)
}
}
const input2Changed = (val) => {
setIn2(val)
let input1 = windowLevelEnabled ? currentWidth : currentRangeMin
let input2 = parseFloat(val)
if (isValidBounds(input1, input2)) {
inputChanged(input1, input2)
}
}
const invalidNumber = !isValidBounds(in1, in2)
return (
colorRanges.size !== 0 && (
<div
ref={colorRangeInput}
className="uiRow"
style={{ background: 'rgba(127, 127, 127, 0.5)' }}
>
<OverlayTrigger
transition={false}
overlay={<Tooltip>Interpolation</Tooltip>}
>
<Button
className={cn('icon-button', {
checked: interpolationEnabled
})}
onClick={() => {
toggleInterpolate()
}}
variant="secondary"
ref={interpolationButton}
>
<Image src={interpolationIconDataUri}></Image>
</Button>
</OverlayTrigger>
<OverlayTrigger
transition={false}
overlay={<Tooltip>{input1Tooltip}</Tooltip>}
>
<Form.Control
className={cn('numberInput', {
invalidNumber
})}
type="number"
value={in1}
onChange={(e) => {
input1Changed(e.target.value)
}}
step={windowLevelEnabled ? windowingStep : rangeStep}
/>
</OverlayTrigger>
<ColorMapIconSelector {...props} />
<OverlayTrigger
transition={false}
overlay={<Tooltip>{input2Tooltip}</Tooltip>}
>
<Form.Control
className={cn('numberInput', {
invalidNumber
})}
type="number"
value={in2}
onChange={(e) => {
input2Changed(e.target.value)
}}
step={windowLevelEnabled ? windowingStep : rangeStep}
/>
</OverlayTrigger>
<WindowLevelReset {...props} />
</div>
)
)
}
export default ColorRangeInput