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;