406 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Plaintext
		
	
	
			
		
		
	
	
			406 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Plaintext
		
	
	
| /* eslint no-alert: 0 */
 | |
| import * as cornerstoneTools from 'cornerstone-tools'
 | |
| const external = cornerstoneTools.external
 | |
| const EVENTS = cornerstoneTools.EVENTS
 | |
| 
 | |
| const toolStyle = cornerstoneTools.toolStyle
 | |
| const textStyle = cornerstoneTools.textStyle
 | |
| const toolColors = cornerstoneTools.toolColors
 | |
| 
 | |
| const pointInsideBoundingBox = cornerstoneTools.import('util/pointInsideBoundingBox')
 | |
| const lineSegDistance = cornerstoneTools.import('util/lineSegDistance')
 | |
| const triggerEvent = cornerstoneTools.import('util/triggerEvent')
 | |
| const moveNewHandle = cornerstoneTools.import('manipulators/moveNewHandle')
 | |
| 
 | |
| const addToolState = cornerstoneTools.addToolState
 | |
| const getToolState = cornerstoneTools.getToolState
 | |
| const removeToolState = cornerstoneTools.removeToolState
 | |
| 
 | |
| // Drawing
 | |
| const drawLinkedTextBox = cornerstoneTools.import('drawing/drawLinkedTextBox')
 | |
| const getNewContext = cornerstoneTools.import('drawing/getNewContext')
 | |
| const draw = cornerstoneTools.import('drawing/draw')
 | |
| // import textBoxWidth from './textBoxWidth'
 | |
| const setShadow = cornerstoneTools.import('drawing/setShadow')
 | |
| const drawArrow = cornerstoneTools.import('drawing/drawArrow')
 | |
| const drawHandles = cornerstoneTools.import('drawing/drawHandles')
 | |
| const { arrowAnnotateCursor } = cornerstoneTools.import('tools/cursors')
 | |
| const getModule = cornerstoneTools.getModule
 | |
| 
 | |
| /**
 | |
|  * @public
 | |
|  * @class ArrowAnnotateTool
 | |
|  * @memberof Tools.Annotation
 | |
|  * @classdesc Create and position an arrow and label
 | |
|  * @extends Tools.Base.BaseAnnotationTool
 | |
|  */
 | |
| export default class ArrowAnnotateTool extends cornerstoneTools.ArrowAnnotateTool {
 | |
|   constructor(props = {}) {
 | |
|     const defaultProps = {
 | |
|       name: 'ArrowAnnotate',
 | |
|       supportedInteractionTypes: ['Mouse', 'Touch'],
 | |
|       configuration: {
 | |
|         getTextCallback,
 | |
|         changeTextCallback,
 | |
|         drawHandles: false,
 | |
|         drawHandlesOnHover: true,
 | |
|         hideHandlesIfMoving: false,
 | |
|         arrowFirst: true,
 | |
|         renderDashed: false,
 | |
|         allowEmptyLabel: false
 | |
|       },
 | |
|       svgCursor: arrowAnnotateCursor
 | |
|     }
 | |
| 
 | |
|     super(props, defaultProps)
 | |
|     this.preventNewMeasurement = false
 | |
|   }
 | |
|   textBoxWidth(context, text, padding) {
 | |
|     const font = textStyle.getFont()
 | |
|     const origFont = context.font
 | |
| 
 | |
|     if (font && font !== origFont) {
 | |
|       context.font = font
 | |
|     }
 | |
|     const width = context.measureText(text).width
 | |
| 
 | |
|     if (font && font !== origFont) {
 | |
|       context.font = origFont
 | |
|     }
 | |
| 
 | |
|     return width + 2 * padding
 | |
|   }
 | |
| 
 | |
|   createNewMeasurement(evt) {
 | |
|     // Create the measurement data for this tool with the end handle activated
 | |
|     return {
 | |
|       visible: true,
 | |
|       active: true,
 | |
|       color: undefined,
 | |
|       text: '',
 | |
|       handles: {
 | |
|         start: {
 | |
|           x: evt.detail.currentPoints.image.x,
 | |
|           y: evt.detail.currentPoints.image.y,
 | |
|           highlight: true,
 | |
|           active: false
 | |
|         },
 | |
|         end: {
 | |
|           x: evt.detail.currentPoints.image.x,
 | |
|           y: evt.detail.currentPoints.image.y,
 | |
|           highlight: true,
 | |
|           active: false
 | |
|         },
 | |
|         textBox: {
 | |
|           active: false,
 | |
|           hasMoved: false,
 | |
|           movesIndependently: false,
 | |
|           drawnIndependently: true,
 | |
|           allowedOutsideImage: true,
 | |
|           hasBoundingBox: true
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   pointNearTool(element, data, coords) {
 | |
|     if (data.visible === false) {
 | |
|       return false
 | |
|     }
 | |
| 
 | |
|     return (
 | |
|       lineSegDistance(element, data.handles.start, data.handles.end, coords) <
 | |
|       25
 | |
|     )
 | |
|   }
 | |
| 
 | |
|   updateCachedStats() {
 | |
|     // Implementing to satisfy BaseAnnotationTool
 | |
|   }
 | |
| 
 | |
|   renderToolData(evt) {
 | |
|     const { element, enabledElement } = evt.detail
 | |
|     const {
 | |
|       handleRadius,
 | |
|       drawHandlesOnHover,
 | |
|       hideHandlesIfMoving,
 | |
|       renderDashed
 | |
|     } = this.configuration
 | |
| 
 | |
|     // If we have no toolData for this element, return immediately as there is nothing to do
 | |
|     const toolData = getToolState(element, this.name)
 | |
| 
 | |
|     if (!toolData) {
 | |
|       return
 | |
|     }
 | |
| 
 | |
|     // We have tool data for this element - iterate over each one and draw it
 | |
|     const canvas = evt.detail.canvasContext.canvas
 | |
|     const context = getNewContext(canvas)
 | |
| 
 | |
|     const lineWidth = toolStyle.getToolWidth()
 | |
| 
 | |
|     let lineDash
 | |
| 
 | |
|     if (renderDashed) {
 | |
|       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 => {
 | |
|         setShadow(context, this.configuration)
 | |
| 
 | |
|         const color = toolColors.getColorIfActive(data)
 | |
| 
 | |
|         // Draw the arrow
 | |
|         const handleStartCanvas = external.cornerstone.pixelToCanvas(
 | |
|           element,
 | |
|           data.handles.start
 | |
|         )
 | |
|         const handleEndCanvas = external.cornerstone.pixelToCanvas(
 | |
|           element,
 | |
|           data.handles.end
 | |
|         )
 | |
| 
 | |
|         // Config.arrowFirst = false;
 | |
|         if (this.configuration.arrowFirst) {
 | |
|           drawArrow(
 | |
|             context,
 | |
|             handleEndCanvas,
 | |
|             handleStartCanvas,
 | |
|             color,
 | |
|             lineWidth,
 | |
|             lineDash
 | |
|           )
 | |
|         } else {
 | |
|           drawArrow(
 | |
|             context,
 | |
|             handleStartCanvas,
 | |
|             handleEndCanvas,
 | |
|             color,
 | |
|             lineWidth,
 | |
|             lineDash
 | |
|           )
 | |
|         }
 | |
| 
 | |
|         const handleOptions = {
 | |
|           color,
 | |
|           handleRadius,
 | |
|           drawHandlesIfActive: drawHandlesOnHover,
 | |
|           hideHandlesIfMoving
 | |
|         }
 | |
|         if (this.configuration.drawHandles) {
 | |
|           drawHandles(context, evt.detail, data.handles, handleOptions)
 | |
|         }
 | |
|         const text = []
 | |
| 
 | |
|         if (data.hasOwnProperty('remark')) {
 | |
|           text.push(data.remark)
 | |
|         }
 | |
|         text.push(textBoxText(data))
 | |
| 
 | |
|         // Draw the text
 | |
|         if (text && text !== '') {
 | |
|           // Calculate the text coordinates.
 | |
|           const padding = 5
 | |
|           const textWidth = this.textBoxWidth(context, text, padding)
 | |
|           const textHeight = textStyle.getFontSize() + 10
 | |
| 
 | |
|           let distance = Math.max(textWidth, textHeight) / 2 + 5
 | |
| 
 | |
|           if (handleEndCanvas.x < handleStartCanvas.x) {
 | |
|             distance = -distance
 | |
|           }
 | |
| 
 | |
|           if (!data.handles.textBox.hasMoved) {
 | |
|             let textCoords
 | |
| 
 | |
|             if (this.configuration.arrowFirst) {
 | |
|               textCoords = {
 | |
|                 x: handleEndCanvas.x - textWidth / 2 + distance,
 | |
|                 y: handleEndCanvas.y - textHeight / 2
 | |
|               }
 | |
|             } else {
 | |
|               // If the arrow is at the End position, the text should
 | |
|               // Be placed near the Start position
 | |
|               textCoords = {
 | |
|                 x: handleStartCanvas.x - textWidth / 2 - distance,
 | |
|                 y: handleStartCanvas.y - textHeight / 2
 | |
|               }
 | |
|             }
 | |
| 
 | |
|             const transform = external.cornerstone.internal.getTransform(
 | |
|               enabledElement
 | |
|             )
 | |
| 
 | |
|             transform.invert()
 | |
| 
 | |
|             const coords = transform.transformPoint(textCoords.x, textCoords.y)
 | |
| 
 | |
|             data.handles.textBox.x = coords.x
 | |
|             data.handles.textBox.y = coords.y
 | |
|           }
 | |
| 
 | |
|           drawLinkedTextBox(
 | |
|             context,
 | |
|             element,
 | |
|             data.handles.textBox,
 | |
|             text,
 | |
|             data.handles,
 | |
|             textBoxAnchorPoints,
 | |
|             color,
 | |
|             lineWidth,
 | |
|             0,
 | |
|             false
 | |
|           )
 | |
|         }
 | |
|       })
 | |
|     }
 | |
| 
 | |
|     function textBoxText(data) {
 | |
|       return data.text
 | |
|     }
 | |
| 
 | |
|     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]
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   addNewMeasurement(evt, interactionType) {
 | |
|     const element = evt.detail.element
 | |
|     const measurementData = this.createNewMeasurement(evt)
 | |
| 
 | |
|     const { allowEmptyLabel } = this.configuration
 | |
| 
 | |
|     // Associate this data with this imageId so we can render it and manipulate it
 | |
|     addToolState(element, this.name, measurementData)
 | |
|     external.cornerstone.updateImage(element)
 | |
| 
 | |
|     moveNewHandle(
 | |
|       evt.detail,
 | |
|       this.name,
 | |
|       measurementData,
 | |
|       measurementData.handles.end,
 | |
|       this.options,
 | |
|       interactionType,
 | |
|       success => {
 | |
|         if (success) {
 | |
|           if (!allowEmptyLabel && measurementData.text === undefined) {
 | |
|             this.configuration.getTextCallback(text => {
 | |
|               if (text || allowEmptyLabel) {
 | |
|                 measurementData.text = text
 | |
|                 measurementData.active = false
 | |
| 
 | |
|                 const modifiedEventData = {
 | |
|                   toolName: this.name,
 | |
|                   toolType: this.name, // Deprecation notice: toolType will be replaced by toolName
 | |
|                   element,
 | |
|                   measurementData
 | |
|                 }
 | |
| 
 | |
|                 external.cornerstone.updateImage(element)
 | |
|                 triggerEvent(
 | |
|                   element,
 | |
|                   EVENTS.MEASUREMENT_COMPLETED,
 | |
|                   modifiedEventData
 | |
|                 )
 | |
|               } else {
 | |
|                 removeToolState(element, this.name, measurementData)
 | |
|               }
 | |
|             }, evt.detail)
 | |
|           }
 | |
|         } else {
 | |
|           removeToolState(element, this.name, measurementData)
 | |
|         }
 | |
|         triggerEvent(element, EVENTS.MEASUREMENT_COMPLETED, {
 | |
|           toolName: this.name,
 | |
|           toolType: this.name, // Deprecation notice: toolType will be replaced by toolName
 | |
|           element,
 | |
|           measurementData
 | |
|         })
 | |
|         external.cornerstone.updateImage(element)
 | |
|       }
 | |
|     )
 | |
|   }
 | |
| 
 | |
|   doubleClickCallback(evt) {
 | |
|     return this._updateTextForNearbyAnnotation(evt)
 | |
|   }
 | |
| 
 | |
|   touchPressCallback(evt) {
 | |
|     return this._updateTextForNearbyAnnotation(evt)
 | |
|   }
 | |
| 
 | |
|   _updateTextForNearbyAnnotation(evt) {
 | |
|     const element = evt.detail.element
 | |
|     const coords = evt.detail.currentPoints.canvas
 | |
|     const toolState = getToolState(element, this.name)
 | |
| 
 | |
|     if (!toolState) {
 | |
|       return false
 | |
|     }
 | |
| 
 | |
|     for (let i = 0; i < toolState.data.length; i++) {
 | |
|       const data = toolState.data[i]
 | |
| 
 | |
|       if (
 | |
|         this.pointNearTool(element, data, coords) ||
 | |
|         pointInsideBoundingBox(data.handles.textBox, coords)
 | |
|       ) {
 | |
|         data.active = true
 | |
|         external.cornerstone.updateImage(element)
 | |
| 
 | |
|         // Allow relabelling via a callback
 | |
|         this.configuration.changeTextCallback(
 | |
|           data,
 | |
|           evt.detail,
 | |
|           this._doneChangingTextCallback.bind(this, element, data)
 | |
|         )
 | |
| 
 | |
|         evt.stopImmediatePropagation()
 | |
|         evt.preventDefault()
 | |
|         evt.stopPropagation()
 | |
| 
 | |
|         return true
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _doneChangingTextCallback(element, measurementData, updatedText, deleteTool) {
 | |
|     if (deleteTool === true) {
 | |
|       removeToolState(element, this.name, measurementData)
 | |
|     } else {
 | |
|       measurementData.text = updatedText
 | |
|     }
 | |
| 
 | |
|     measurementData.active = false
 | |
|     external.cornerstone.updateImage(element)
 | |
| 
 | |
|     triggerEvent(element, EVENTS.MEASUREMENT_MODIFIED, {
 | |
|       toolName: this.name,
 | |
|       toolType: this.name, // Deprecation notice: toolType will be replaced by toolName
 | |
|       element,
 | |
|       measurementData
 | |
|     })
 | |
|   }
 | |
| }
 | |
| 
 | |
| function getTextCallback(doneChangingTextCallback) {
 | |
|   doneChangingTextCallback(prompt('Enter your annotation:'))
 | |
| }
 | |
| 
 | |
| function changeTextCallback(data, eventData, doneChangingTextCallback) {
 | |
|   doneChangingTextCallback(prompt('Change your annotation:'))
 | |
| }
 |