423 lines
13 KiB
JavaScript
423 lines
13 KiB
JavaScript
import { connect } from 'react-redux';
|
|
import { MeasurementTable } from '@ohif/ui';
|
|
import OHIF, { DICOMSR } from '@ohif/core';
|
|
import moment from 'moment';
|
|
import cornerstone from 'cornerstone-core';
|
|
|
|
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 => {
|
|
if (enabledElement.image) {
|
|
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;
|