- Bump cornerstone-tools to 3.9.0, ohif-core to 0.5.2 and react-viewerbase to 0.4.1 - Add bidirectional, elliptical, circle and rectangle roi tools - Support labeling and context-menu for all annotation tools - Sort measurements by lesion naming number to show the last added measurement in the end in measurement table
354 lines
10 KiB
JavaScript
354 lines
10 KiB
JavaScript
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
|
|
}, {})
|
|
}
|
|
|
|
function getAllTools() {
|
|
const config = OHIF.measurements.MeasurementApi.getConfiguration()
|
|
let tools = []
|
|
config.measurementTools.forEach(
|
|
toolGroup => (tools = tools.concat(toolGroup.childTools))
|
|
)
|
|
|
|
return tools
|
|
}
|
|
|
|
function getMeasurementText(measurementData) {
|
|
const { location, description } = measurementData
|
|
let text = '...'
|
|
if (location) {
|
|
text = location
|
|
if (description) {
|
|
text += `(${description})`
|
|
}
|
|
}
|
|
return text
|
|
}
|
|
|
|
function getDataForEachMeasurementNumber(
|
|
measurementNumberList,
|
|
timepoints,
|
|
displayFunction
|
|
) {
|
|
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)
|
|
}
|
|
data.push(eachData)
|
|
})
|
|
})
|
|
|
|
return data
|
|
}
|
|
|
|
function convertMeasurementsToTableData(toolCollections, timepoints) {
|
|
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: [],
|
|
}
|
|
})
|
|
|
|
Object.keys(toolCollections).forEach(toolId => {
|
|
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')
|
|
|
|
Object.keys(groupedMeasurements).forEach(groupedMeasurementsIndex => {
|
|
const measurementNumberList =
|
|
groupedMeasurements[groupedMeasurementsIndex]
|
|
const measurementData = measurementNumberList[0]
|
|
const {
|
|
measurementNumber,
|
|
lesionNamingNumber,
|
|
toolType,
|
|
} = measurementData
|
|
const measurementId = measurementData._id
|
|
|
|
//check if all measurements with same measurementNumber will have same LABEL
|
|
const tableMeasurement = {
|
|
itemNumber: lesionNamingNumber,
|
|
label: getMeasurementText(measurementData),
|
|
measurementId,
|
|
measurementNumber,
|
|
lesionNamingNumber,
|
|
toolType,
|
|
hasWarnings: false, //TODO
|
|
warningTitle: '', //TODO
|
|
isSplitLesion: false, //TODO
|
|
warningList: [], //TODO
|
|
data: getDataForEachMeasurementNumber(
|
|
measurementNumberList,
|
|
timepoints,
|
|
displayFunction
|
|
),
|
|
}
|
|
|
|
// find the group object for the tool
|
|
const toolGroupMeasurements = tableMeasurements.find(group => {
|
|
return group.groupId === tool.toolGroup
|
|
})
|
|
// inject the new measurement for this measurementNumer
|
|
toolGroupMeasurements.measurements.push(tableMeasurement)
|
|
})
|
|
})
|
|
|
|
// Sort measurements by lesion naming number
|
|
tableMeasurements.forEach(tm => {
|
|
tm.measurements.sort((m1, m2) =>
|
|
m1.lesionNamingNumber > m2.lesionNamingNumber ? 1 : -1
|
|
)
|
|
})
|
|
|
|
return tableMeasurements
|
|
}
|
|
|
|
function convertTimepointsToTableData(timepoints) {
|
|
if (!timepoints || !timepoints.length) {
|
|
return []
|
|
}
|
|
|
|
return [
|
|
{
|
|
label: 'Study date:',
|
|
date: moment(timepoints[0].latestDate).format('DD-MMM-YY'),
|
|
},
|
|
]
|
|
}
|
|
|
|
const mapStateToProps = state => {
|
|
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,
|
|
mapDispatchToProps,
|
|
mergeProps
|
|
)(MeasurementTable)
|
|
|
|
export default ConnectedMeasurementTable
|