303 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Plaintext
		
	
	
			
		
		
	
	
			303 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
 | 
						|
}
 |