import * as cornerstoneTools from 'cornerstone-tools' // State const getToolState = cornerstoneTools.getToolState const toolStyle = cornerstoneTools.toolStyle const toolColors = cornerstoneTools.toolColors // Drawing const getNewContext = cornerstoneTools.import('drawing/getNewContext') const draw = cornerstoneTools.import('drawing/draw') const setShadow = cornerstoneTools.import('drawing/setShadow') const drawLine = cornerstoneTools.import('drawing/drawLine') const drawLinkedTextBox = cornerstoneTools.import('drawing/drawLinkedTextBox') const drawHandles = cornerstoneTools.import('drawing/drawHandles') // util const lineSegDistance = cornerstoneTools.import('util/lineSegDistance') const lengthCursor = cornerstoneTools.import('tools/cursors') const getPixelSpacing = cornerstoneTools.import('util/getPixelSpacing') const throttle = cornerstoneTools.import('util/throttle') const getModule = cornerstoneTools.getModule /** * @public * @class LengthTool * @memberof Tools.Annotation * @classdesc Tool for measuring distances. * @extends Tools.Base.BaseAnnotationTool */ export default class LengthTool extends cornerstoneTools.LengthTool { constructor(props = {}) { const defaultProps = { name: 'Length', supportedInteractionTypes: ['Mouse', 'Touch'], svgCursor: lengthCursor, configuration: { drawHandles: true, drawHandlesOnHover: false, hideHandlesIfMoving: false, renderDashed: false, digits: 2 } } super(props, defaultProps) this.throttledUpdateCachedStats = throttle(this.updateCachedStats, 0) } createNewMeasurement(eventData) { const goodEventData = eventData && eventData.currentPoints && eventData.currentPoints.image if (!goodEventData) { console.log( `required eventData not supplied to tool ${this.name}'s createNewMeasurement` ) return } const { x, y } = eventData.currentPoints.image return { visible: true, active: true, color: undefined, invalidated: true, handles: { start: { x, y, highlight: true, active: false }, end: { x, y, highlight: true, active: true }, textBox: { active: false, hasMoved: false, movesIndependently: false, drawnIndependently: true, allowedOutsideImage: true, hasBoundingBox: true } } } } /** * * * @param {*} element * @param {*} data * @param {*} coords * @returns {Boolean} */ pointNearTool(element, data, coords) { const hasStartAndEndHandles = data && data.handles && data.handles.start && data.handles.end const validParameters = hasStartAndEndHandles if (!validParameters) { console.log( `invalid parameters supplied to tool ${this.name}'s pointNearTool` ) return false } if (data.visible === false) { return false } return ( lineSegDistance(element, data.handles.start, data.handles.end, coords) < 25 ) } updateCachedStats(image, element, data) { const { digits } = this.configuration const { rowPixelSpacing, colPixelSpacing } = getPixelSpacing(image) // Set rowPixelSpacing and columnPixelSpacing to 1 if they are undefined (or zero) const dx = (data.handles.end.x - data.handles.start.x) * (colPixelSpacing || 1) const dy = (data.handles.end.y - data.handles.start.y) * (rowPixelSpacing || 1) // Calculate the length, and create the text variable with the millimeters or pixels suffix const length = Math.sqrt(dx * dx + dy * dy) // Store the length inside the tool for outside access data.length = length.toFixed(digits) data.invalidated = false } renderToolData(evt) { const eventData = evt.detail const { handleRadius, drawHandlesOnHover, hideHandlesIfMoving, renderDashed, digits } = this.configuration const toolData = getToolState(evt.currentTarget, this.name) if (!toolData) { return } // We have tool data for this element - iterate over each one and draw it const context = getNewContext(eventData.canvasContext.canvas) const { image, element } = eventData const { rowPixelSpacing, colPixelSpacing } = getPixelSpacing(image) const lineWidth = toolStyle.getToolWidth() const lineDash = getModule('globalConfiguration').configuration.lineDash for (let i = 0; i < toolData.data.length; i++) { const data = toolData.data[i] if (data.visible === false) { continue } draw(context, context => { // Configurable shadow setShadow(context, this.configuration) const color = toolColors.getColorIfActive(data) const lineOptions = { color } if (renderDashed) { lineOptions.lineDash = lineDash } // Draw the measurement line drawLine( context, element, data.handles.start, data.handles.end, lineOptions ) // Draw the handles const handleOptions = { color, handleRadius, drawHandlesIfActive: drawHandlesOnHover, hideHandlesIfMoving } if (this.configuration.drawHandles) { drawHandles(context, eventData, data.handles, handleOptions) } if (!data.handles.textBox.hasMoved) { const coords = { x: Math.max(data.handles.start.x, data.handles.end.x) } // Depending on which handle has the largest x-value, // Set the y-value for the text box if (coords.x === data.handles.start.x) { coords.y = data.handles.start.y } else { coords.y = data.handles.end.y } data.handles.textBox.x = coords.x data.handles.textBox.y = coords.y } // Move the textbox slightly to the right and upwards // So that it sits beside the length tool handle const xOffset = 10 // Update textbox stats if (data.invalidated === true) { if (data.length) { this.throttledUpdateCachedStats(image, element, data) } else { this.updateCachedStats(image, element, data) } } const text = [] if (data.hasOwnProperty('remark')) { if (data.hasOwnProperty('status') && data.status) { text.push(`${data.remark}(${data.status})`) } else { text.push(data.remark) } } text.push(textBoxText(data, rowPixelSpacing, colPixelSpacing)) drawLinkedTextBox( context, element, data.handles.textBox, text, data.handles, textBoxAnchorPoints, color, lineWidth, xOffset, true ) }) } // - SideEffect: Updates annotation 'suffix' function textBoxText(annotation, rowPixelSpacing, colPixelSpacing) { const measuredValue = _sanitizeMeasuredValue(annotation.length) // Measured value is not defined, return empty string if (!measuredValue) { return '' } // Set the length text suffix depending on whether or not pixelSpacing is available let suffix = 'mm' if (!rowPixelSpacing || !colPixelSpacing) { suffix = 'pixels' } annotation.unit = suffix return `${measuredValue.toFixed(digits)} ${suffix}` } function textBoxAnchorPoints(handles) { const midpoint = { x: (handles.start.x + handles.end.x) / 2, y: (handles.start.y + handles.end.y) / 2 } return [handles.start, midpoint, handles.end] } } } /** * Attempts to sanitize a value by casting as a number; if unable to cast, * we return `undefined` * * @param {*} value * @returns a number or undefined */ function _sanitizeMeasuredValue(value) { const parsedValue = Number(value) const isNumber = !isNaN(parsedValue) return isNumber ? parsedValue : undefined }