import { connect } from 'react-redux'; import { MeasurementTable } from '@ohif/ui'; import OHIF from '@ohif/core'; import moment from 'moment'; import cornerstone from 'cornerstone-core'; import DICOMSR from './../../lib/DICOMSR'; import jumpToRowItem from './jumpToRowItem.js'; const { setViewportSpecificData } = OHIF.redux.actions; const { MeasurementApi } = OHIF.measurements; /** * Takes a list of objects and a property and return the list grouped by the property * * @param {Array} list - The objects to be grouped by * @param {string} props - The property to group the objects * @returns {Object} */ function groupBy(list, props) { return list.reduce((a, b) => { (a[b[props]] = a[b[props]] || []).push(b); return a; }, {}); } /** * Takes a list of tools grouped and return all tools separately * * @param {Array} [toolGroups=[]] - The grouped tools * @returns {Array} - The list of all tools on all groups */ function getAllTools(toolGroups = []) { let tools = []; toolGroups.forEach( toolGroup => (tools = tools.concat(toolGroup.childTools)) ); return tools; } /** * Takes measurementData and build the measurement text to be used into the table * * @param {Object} [measurementData={}] * @param {string} measurementData.location - The measurement location * @param {string} measurementData.description - The measurement description * @returns {string} */ function getMeasurementText(measurementData = {}) { const defaultText = '...'; const { location = '', description = '' } = measurementData; const result = location + (description ? ` (${description})` : ''); return result || defaultText; } /** * Takes a list of measurements grouped by measurement numbers and return each measurement data by available timepoint * * @param {Array} measurementNumberList - The list of measurements * @param {Array} timepoints - The list of available timepoints * @param {Function} displayFunction - The function that builds the display text by each tool * @returns */ 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; } /** * Take a measurement toolName and return if any warnings * * @param {string} toolName - The tool name * @returns {string} */ function getWarningsForMeasurement(toolName) { const isToolSupported = DICOMSR.isToolSupported(toolName); return { hasWarnings: !isToolSupported, warningTitle: isToolSupported ? '' : 'Unsupported Tool', warningList: isToolSupported ? [] : [`${toolName} cannot be persisted at this time`], }; } /** * Take measurements from MeasurementAPI structure and convert into a measurementTable structure * * @param {Object} toolCollections - The list of all measurement grouped by groupTool and toolName * @param {Array} timepoints - The list of available timepoints * @returns */ function convertMeasurementsToTableData(toolCollections, timepoints) { const config = OHIF.measurements.MeasurementApi.getConfiguration(); const toolGroups = config.measurementTools; const tools = getAllTools(toolGroups); 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; const { hasWarnings, warningTitle, warningList, } = getWarningsForMeasurement(toolType); //check if all measurements with same measurementNumber will have same LABEL const tableMeasurement = { itemNumber: lesionNamingNumber, label: getMeasurementText(measurementData), measurementId, measurementNumber, lesionNamingNumber, toolType, hasWarnings, warningTitle, warningList, isSplitLesion: false, //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; } /** * Take a list of available timepoints and return a list header information for each timepoint * * @param {Array} timepoints - The list of available timepoints * @param {string} timepoints[].latestDate - The date of the last study taken on the timepoint * @returns {{label: string, key: string, date: string}[]} */ function convertTimepointsToTableData(timepoints) { if (!timepoints || !timepoints.length) { return []; } return [ { label: 'Study date:', key: 'StudyDate', date: moment(timepoints[0].latestDate).format('DD-MMM-YY'), }, ]; } /** * Takes server type and return a function or undefined * * @param {string} serverType - The server type * @returns {undefined|Function} */ function getSaveFunction(serverType) { if (serverType === 'dicomWeb') { return () => { const measurementApi = OHIF.measurements.MeasurementApi.Instance; const promise = measurementApi.storeMeasurements(); return promise; }; } } const mapStateToProps = state => { const { timepointManager, servers } = state; const { timepoints, measurements } = timepointManager; const activeServer = servers.servers.find(a => a.active === true); const saveFunction = getSaveFunction(activeServer.type); return { timepoints: convertTimepointsToTableData(timepoints), measurementCollection: convertMeasurementsToTableData( measurements, timepoints ), timepointManager: state.timepointManager, viewports: state.viewports, saveFunction, }; }; const mapDispatchToProps = (dispatch, ownProps) => { return { dispatchRelabel: (event, measurementData, viewportsState) => { event.persist(); 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 { toolType, measurementId } = measurementData; const tool = MeasurementApi.Instance.tools[toolType].find(measurement => { return measurement._id === measurementId; }); // Clone the tool not to set empty location initially const toolForLocation = Object.assign({}, tool, { location: null }); if (ownProps.onRelabel) { ownProps.onRelabel(toolForLocation); } }, dispatchEditDescription: (event, measurementData, viewportsState) => { event.persist(); 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 { toolType, measurementId } = measurementData; const tool = MeasurementApi.Instance.tools[toolType].find(measurement => { return measurement._id === measurementId; }); if (ownProps.onEditDescription) { ownProps.onEditDescription(tool); } }, 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) => { const { timepoints, saveFunction, measurementCollection } = propsFromState; const { onSaveComplete, selectedMeasurementNumber } = ownProps; return { timepoints, saveFunction, measurementCollection, onSaveComplete, 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;