305 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Plaintext
		
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Plaintext
		
	
	
| 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, 110)
 | |
|   }
 | |
| 
 | |
|   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
 | |
| }
 |