WIP reorganization

This commit is contained in:
Erik Ziegler committed 2018-12-12 09:10:39 +01:00
1 parent 6706dfe634
commit 054e37dfb7
266 files changed
+98 -620

No files matched your search

@@ -0,0 +1,163 @@
import { cornerstone, cornerstoneTools, cornerstoneMath } from 'meteor/ohif:cornerstone';
import { toolType } from './definitions';
import createNewMeasurement from './createNewMeasurement';
import mouseMoveCallback from './mouseMoveCallback';
import mouseDownCallback from './mouseDownCallback';
import doubleClickCallback from './doubleClickCallback';
import updatePerpendicularLineHandles from './updatePerpendicularLineHandles';
export default function(mouseEventData) {
const { element } = mouseEventData;
const $element = $(element);
const imagePlane = cornerstone.metaData.get('imagePlaneModule', mouseEventData.image.imageId);
let rowPixelSpacing;
let colPixelSpacing;
if (imagePlane) {
rowPixelSpacing = imagePlane.rowPixelSpacing || imagePlane.rowImagePixelSpacing;
colPixelSpacing = imagePlane.columnPixelSpacing || imagePlane.colImagePixelSpacing;
} else {
rowPixelSpacing = mouseEventData.image.rowPixelSpacing;
colPixelSpacing = mouseEventData.image.columnPixelSpacing;
}
// LT-29 Disable Target Measurements when pixel spacing is not available
if (!rowPixelSpacing || !colPixelSpacing) {
return;
}
function doneCallback() {
measurementData.active = false;
cornerstone.updateImage(element);
}
const measurementData = createNewMeasurement(mouseEventData);
measurementData.viewport = cornerstone.getViewport(element);
const tool = cornerstoneTools[toolType];
const config = tool.getConfiguration();
const { mouseDownActivateCallback } = tool;
// associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(element, toolType, measurementData);
const disableDefaultHandlers = () => {
// since we are dragging to another place to drop the end point, we can just activate
// the end point and let the moveHandle move it for us.
element.removeEventListener('cornerstonetoolsmousemove', mouseMoveCallback);
element.removeEventListener('cornerstonetoolsmousedown', mouseDownCallback);
element.removeEventListener('cornerstonetoolsmousedownactivate', mouseDownActivateCallback);
element.removeEventListener('cornerstonetoolsmousedoubleclick', doubleClickCallback);
};
disableDefaultHandlers();
// Update the perpendicular line handles position
const updateHandler = event => updatePerpendicularLineHandles(event.detail, measurementData);
element.addEventListener('cornerstonetoolsmousedrag', updateHandler);
element.addEventListener('cornerstonetoolsmouseup', updateHandler);
let cancelled = false;
const cancelAction = () => {
cancelled = true;
cornerstoneTools.removeToolState(element, toolType, measurementData);
};
// Add a flag for using Esc to cancel tool placement
const keyDownHandler = event => {
// If the Esc key was pressed, set the flag to true
if (event.which === 27) {
cancelAction();
}
// Don't propagate this keydown event so it can't interfere
// with anything outside of this tool
return false;
};
// Bind a one-time event listener for the Esc key
$element.one('keydown', keyDownHandler);
// Bind a mousedown handler to cancel the measurement if it's zero-sized
const mousedownHandler = () => {
const { start, end } = measurementData.handles;
if (!cornerstoneMath.point.distance(start, end)) {
cancelAction();
}
};
// Bind a one-time event listener for mouse down
$element.one('mousedown', mousedownHandler);
// Keep the current image and create a handler for new rendered images
const currentImage = cornerstone.getImage(element);
const currentViewport = cornerstone.getViewport(element);
const imageRenderedHandler = () => {
const newImage = cornerstone.getImage(element);
// Check if the rendered image changed during measurement creation and delete it if so
if (newImage.imageId !== currentImage.imageId) {
cornerstone.displayImage(element, currentImage, currentViewport);
cancelAction();
cornerstone.displayImage(element, newImage, currentViewport);
}
};
// Bind the event listener for image rendering
element.addEventListener('cornerstoneimagerendered', imageRenderedHandler);
// Bind the tool deactivation and enlargement handlers
element.addEventListener('cornerstonetoolstooldeactivated', cancelAction);
$element.one('ohif.viewer.viewport.toggleEnlargement', cancelAction);
cornerstone.updateImage(element);
const timestamp = new Date().getTime();
const { end, perpendicularStart } = measurementData.handles;
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, end, () => {
const { handles, longestDiameter, shortestDiameter } = measurementData;
const hasHandlesOutside = cornerstoneTools.anyHandlesOutsideImage(mouseEventData, handles);
const longestDiameterSize = parseFloat(longestDiameter) || 0;
const shortestDiameterSize = parseFloat(shortestDiameter) || 0;
const isTooSmal = (longestDiameterSize < 1) || (shortestDiameterSize < 1);
const isTooFast = (new Date().getTime() - timestamp) < 150;
if (cancelled || hasHandlesOutside || isTooSmal || isTooFast) {
// delete the measurement
measurementData.cancelled = true;
cornerstoneTools.removeToolState(element, toolType, measurementData);
} else {
// Set lesionMeasurementData Session
config.getMeasurementLocationCallback(measurementData, mouseEventData, doneCallback);
}
// Unbind the Esc keydown hook
$element.off('keydown', keyDownHandler);
// Unbind the mouse down hook
$element.off('mousedown', mousedownHandler);
// Unbind the event listener for image rendering
element.removeEventListener('cornerstoneimagerendered', imageRenderedHandler);
// Unbind the tool deactivation and enlargement handlers
element.removeEventListener('cornerstonetoolstooldeactivated', cancelAction);
$element.off('ohif.viewer.viewport.toggleEnlargement', cancelAction);
// perpendicular line is not connected to long-line
perpendicularStart.locked = false;
// Unbind the handlers to update perpendicular line
element.removeEventListener('cornerstonetoolsmousedrag', updateHandler);
element.removeEventListener('cornerstonetoolsmouseup', updateHandler);
// Disable the default handlers and re-enable again
disableDefaultHandlers();
element.addEventListener('cornerstonetoolsmousemove', mouseMoveCallback);
element.addEventListener('cornerstonetoolsmousedown', mouseDownCallback);
element.addEventListener('cornerstonetoolsmousedownactivate', mouseDownActivateCallback);
element.addEventListener('cornerstonetoolsmousedoubleclick', doubleClickCallback);
cornerstone.updateImage(element);
});
}
@@ -0,0 +1,59 @@
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
import { toolType } from './definitions';
import createNewMeasurement from './createNewMeasurement';
import updatePerpendicularLineHandles from './updatePerpendicularLineHandles';
export default function(touchEventData) {
const element = { touchEventData };
// LT-29 Disable Target Measurements when pixel spacing is not available
if (!touchEventData.image.rowPixelSpacing || !touchEventData.image.columnPixelSpacing) return;
const doneCallback = () => {
measurementData.active = false;
cornerstone.updateImage(element);
};
const measurementData = createNewMeasurement(touchEventData);
const { cancelled, handles } = measurementData;
const config = cornerstoneTools[toolType].getConfiguration();
// associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(element, toolType, measurementData);
// since we are dragging to another place to drop the end point, we can just activate
// the end point and let the moveHandle move it for us.
const { touchMoveHandle, tapCallback, touchDownActivateCallback } = cornerstoneTools[toolType];
element.removeEventListener('cornerstonetoolstouchdrag', touchMoveHandle);
element.removeEventListener('cornerstonetoolstap', tapCallback);
element.removeEventListener('cornerstonetoolsdragstartactive', touchDownActivateCallback);
// Update the perpendicular line handles position
const updateHandler = event => updatePerpendicularLineHandles(event.detail, measurementData);
element.addEventListener('cornerstonetoolstouchdrag', updateHandler);
element.addEventListener('cornerstonetoolstouchend', updateHandler);
cornerstone.updateImage(element);
const { end, perpendicularStart } = handles;
cornerstoneTools.moveNewHandleTouch(touchEventData, toolType, measurementData, end, () => {
if (cancelled || cornerstoneTools.anyHandlesOutsideImage(touchEventData, handles)) {
// delete the measurement
cornerstoneTools.removeToolState(element, toolType, measurementData);
} else {
// Set lesionMeasurementData Session
config.getMeasurementLocationCallback(measurementData, touchEventData, doneCallback);
}
// perpendicular line is not connected to long-line
perpendicularStart.locked = false;
// Unbind the handlers to update perpendicular line
element.removeEventListener('cornerstonetoolstouchdrag', updateHandler);
element.removeEventListener('cornerstonetoolstouchend', updateHandler);
element.addEventListener('cornerstonetoolstouchdrag', touchMoveHandle);
element.addEventListener('cornerstonetoolstap', tapCallback);
element.addEventListener('cornerstonetoolsdragstartactive', touchDownActivateCallback);
cornerstone.updateImage(element);
});
}
@@ -0,0 +1,32 @@
// Calculate the longest and shortest diameters for the given measurementData
export default function(eventData, measurementData) {
const { rowPixelSpacing, columnPixelSpacing } = eventData.image;
const { start, end, perpendicularStart, perpendicularEnd } = measurementData.handles;
// updatePerpendicularLineHandles(eventData, measurementData);
// Calculate the long axis length
const dx = (start.x - end.x) * (columnPixelSpacing || 1);
const dy = (start.y - end.y) * (rowPixelSpacing || 1);
let length = Math.sqrt(dx * dx + dy * dy);
// Calculate the short axis length
const wx = (perpendicularStart.x - perpendicularEnd.x) * (columnPixelSpacing || 1);
const wy = (perpendicularStart.y - perpendicularEnd.y) * (rowPixelSpacing || 1);
let width = Math.sqrt(wx * wx + wy * wy);
if (!width) {
width = 0;
}
// Length is always longer than width
if (width > length) {
const tempW = width;
const tempL = length;
length = tempW;
width = tempL;
}
// Set measurement text to show lesion table
measurementData.longestDiameter = length.toFixed(1);
measurementData.shortestDiameter = width.toFixed(1);
}
@@ -0,0 +1,41 @@
import { toolType } from './definitions';
const getHandle = (x, y, index, extraAttributes={}) => {
return Object.assign({
x,
y,
index,
drawnIndependently: false,
allowedOutsideImage: false,
highlight: true,
active: false
}, extraAttributes);
};
export default function(mouseEventData) {
const { x, y } = mouseEventData.currentPoints.image;
// Create the measurement data for this tool with the end handle activated
const measurementData = {
toolType,
isCreating: true,
visible: true,
active: true,
handles: {
start: getHandle(x, y, 0),
end: getHandle(x, y, 1, { active: true }),
perpendicularStart: getHandle(x, y, 2, { locked: true }),
perpendicularEnd: getHandle(x, y, 3),
textBox: getHandle(x - 50, y - 70, null, {
highlight: false,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true,
hasBoundingBox: true
})
},
longestDiameter: 0,
shortestDiameter: 0
};
return measurementData;
}
@@ -0,0 +1,7 @@
const toolType = 'bidirectional';
const distanceThreshold = 6;
export {
toolType,
distanceThreshold
};
@@ -0,0 +1,49 @@
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
import { toolType } from './definitions';
import pointNearTool from './pointNearTool';
export default function (event) {
const eventData = event.detail;
const { element } = eventData.element;
function doneCallback(data, deleteTool) {
if (deleteTool === true) {
cornerstoneTools.removeToolState(element, toolType, data);
cornerstone.updateImage(element);
}
}
const buttonMask = event.data && event.data.mouseButtonMask;
if (buttonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, buttonMask)) {
return false;
}
// Check if the element is enabled and stop here if not
try {
cornerstone.getEnabledElement(element);
} catch (error) {
return;
}
const config = cornerstoneTools.bidirectional.getConfiguration();
const coords = eventData.currentPoints.canvas;
const toolData = cornerstoneTools.getToolState(element, toolType);
// now check to see if there is a handle we can move
if (!toolData) return;
let data;
for (let i = 0; i < toolData.data.length; i++) {
data = toolData.data[i];
if (pointNearTool(element, data, coords)) {
data.active = true;
cornerstone.updateImage(element);
// Allow relabelling via a callback
config.changeMeasurementLocationCallback(data, eventData, doneCallback);
event.stopImmediatePropagation();
return false;
}
}
}
@@ -0,0 +1 @@
import './tool.js';
@@ -0,0 +1,42 @@
const swapAttribute = (a, b, attribute) => {
const originalA = a[attribute];
const originalB = b[attribute];
a[attribute] = originalB;
b[attribute] = originalA;
};
const swapHandles = (a, b) => {
swapAttribute(a, b, 'x');
swapAttribute(a, b, 'y');
swapAttribute(a, b, 'moving');
swapAttribute(a, b, 'hover');
swapAttribute(a, b, 'active');
swapAttribute(a, b, 'selected');
};
function invertHandles(eventData, measurementData, handle) {
const { rowPixelSpacing, columnPixelSpacing } = eventData.image;
const { handles } = measurementData;
const { start, end, perpendicularStart, perpendicularEnd } = handles;
// Calculate the long axis length
const dx = (start.x - end.x) * (columnPixelSpacing || 1);
const dy = (start.y - end.y) * (rowPixelSpacing || 1);
const length = Math.sqrt(dx * dx + dy * dy);
// Calculate the short axis length
const wx = (perpendicularStart.x - perpendicularEnd.x) * (columnPixelSpacing || 1);
const wy = (perpendicularStart.y - perpendicularEnd.y) * (rowPixelSpacing || 1);
const width = Math.sqrt(wx * wx + wy * wy) || 0;
if (width > length) {
swapHandles(start, end);
swapHandles(start, perpendicularStart);
swapHandles(end, perpendicularEnd);
return Object.values(handles).find(h => h.moving === true);
}
return handle;
}
export default invertHandles;
@@ -0,0 +1,151 @@
/* jshint -W083 */
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
import { toolType, distanceThreshold } from './definitions';
import mouseMoveCallback from './mouseMoveCallback';
import pointNearTool from './pointNearTool';
import moveHandle from './moveHandle';
import invertHandles from './invertHandles';
// Clear the selected state for the given handles object
const unselectAllHandles = handles => {
let imageNeedsUpdate = false;
Object.keys(handles).forEach(handleKey => {
if (handleKey === 'textBox') return;
handles[handleKey].selected = false;
imageNeedsUpdate = handles[handleKey].active || imageNeedsUpdate;
handles[handleKey].active = false;
});
return imageNeedsUpdate;
};
// Clear the bidirectional tool's selection for all tool handles
const clearBidirectionalSelection = event => {
let imageNeedsUpdate = false;
const toolData = cornerstoneTools.getToolState(event.target, 'bidirectional');
if (!toolData) return;
toolData.data.forEach(data => {
const unselectResult = unselectAllHandles(data.handles);
imageNeedsUpdate = imageNeedsUpdate || unselectResult;
});
return imageNeedsUpdate;
};
const setHandlesMovingState = (handles, state) => {
Object.keys(handles).forEach(handleKey => {
if (handleKey === 'textBox') return;
handles[handleKey].moving = state;
});
};
// mouseDownCallback is used to restrict behaviour of perpendicular-line
export default function(event) {
const eventData = event.detail;
let data;
const element = eventData.element;
const $element = $(element);
const options = cornerstoneTools.getToolOptions(toolType, element);
if (!cornerstoneTools.isMouseButtonEnabled(eventData.which, options.mouseButtonMask)) return;
// Add an event listener to clear the selected state when a measurement is activated
const activateEventKey = 'ViewerMeasurementsActivated';
$element.off(activateEventKey).on(activateEventKey, () => clearBidirectionalSelection(event));
// Clear selection on left mouse button click
if (eventData.which === 1) {
const imageNeedsUpdate = clearBidirectionalSelection(event);
if (imageNeedsUpdate) {
cornerstone.updateImage(element);
}
}
function handleDoneMove(handle) {
// Set the cursor back to its default
$element.css('cursor', '');
data.invalidated = true;
if (cornerstoneTools.anyHandlesOutsideImage(eventData, data.handles)) {
// delete the measurement
cornerstoneTools.removeToolState(element, toolType, data);
}
// Update the handles to keep selected state
if (handle) {
handle.moving = false;
handle.selected = true;
}
cornerstone.updateImage(element);
element.addEventListener('cornerstonetoolsmousemove', mouseMoveCallback);
}
const coords = eventData.startPoints.canvas;
const toolData = cornerstoneTools.getToolState(event.currentTarget, toolType);
if (!toolData) return;
// now check to see if there is a handle we can move
for (let i = 0; i < toolData.data.length; i++) {
data = toolData.data[i];
const handleParams = [element, data.handles, coords, distanceThreshold];
let handle = cornerstoneTools.getHandleNearImagePoint(...handleParams);
if (handle) {
handle.moving = true;
// Invert handles if needed
handle = invertHandles(eventData, data, handle);
// Hide the cursor to improve precision while resizing the line or set to move
// if dragging text box
$element.css('cursor', handle.hasBoundingBox ? 'move' : 'none');
element.removeEventListener('cornerstonetoolsmousemove', mouseMoveCallback);
data.active = true;
unselectAllHandles(data.handles);
moveHandle(eventData, toolType, data, handle, () => handleDoneMove(handle));
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
return;
}
}
// Now check to see if there is a line we can move
// Now check to see if we have a tool that we can move
const opt = {
deleteIfHandleOutsideImage: true,
preventHandleOutsideImage: false
};
const getDoneMovingCallback = handles => () => {
setHandlesMovingState(handles, false);
handleDoneMove();
};
for (let i = 0; i < toolData.data.length; i++) {
data = toolData.data[i];
if (pointNearTool(element, data, coords)) {
// Set the cursor to move
$element.css('cursor', 'move');
element.removeEventListener('cornerstonetoolsmousemove', mouseMoveCallback);
data.active = true;
unselectAllHandles(data.handles);
setHandlesMovingState(data.handles, true);
const doneMovingCallback = getDoneMovingCallback(data.handles);
const allHandlesParams = [event, data, toolData, toolType, opt, doneMovingCallback];
cornerstoneTools.moveAllHandles(...allHandlesParams);
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
return;
}
}
}
@@ -0,0 +1,67 @@
/* jshint -W083 */
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
import { toolType } from './definitions';
import pointNearTool from './pointNearTool';
// Replaces the cornerstoneTools.handleActivator function by skiping the active handle comparison
const handleActivator = (element, handles, canvasPoint, distanceThreshold=6) => {
const getHandle = cornerstoneTools.getHandleNearImagePoint;
const nearbyHandle = getHandle(element, handles, canvasPoint, distanceThreshold);
let handleActivatorChanged = false;
Object.keys(handles).forEach(handleKey => {
if (handleKey === 'textBox') return;
const handle = handles[handleKey];
const newActiveState = handle === nearbyHandle;
if (handle.active !== newActiveState) {
handleActivatorChanged = true;
}
handle.active = newActiveState;
});
return handleActivatorChanged;
};
// mouseMoveCallback is used to hide handles when mouse is away
export default function (event) {
const eventData = event.detail;
const { element } = eventData;
cornerstoneTools.toolCoordinates.setCoords(eventData);
// if we have no tool data for this element, do nothing
const toolData = cornerstoneTools.getToolState(element, toolType);
if (!toolData) return;
// We have tool data, search through all data and see if we can activate a handle
let imageNeedsUpdate = false;
for (let i = 0; i < toolData.data.length; i++) {
// get the cursor position in canvas coordinates
const coords = eventData.currentPoints.canvas;
const data = toolData.data[i];
const handleActivatorChanged = handleActivator(element, data.handles, coords);
Object.keys(data.handles).forEach(handleKey => {
if (handleKey === 'textBox') return;
const handle = data.handles[handleKey];
handle.hover = handle.active;
});
if (handleActivatorChanged) {
imageNeedsUpdate = true;
}
const nearToolAndInactive = pointNearTool(element, data, coords) && !data.active;
const notNearToolAndActive = !pointNearTool(element, data, coords) && data.active;
if (nearToolAndInactive || notNearToolAndActive) {
data.active = !data.active;
imageNeedsUpdate = true;
}
}
// Handle activation status changed, redraw the image
if (imageNeedsUpdate === true) {
cornerstone.updateImage(element);
}
}
@@ -0,0 +1,2 @@
import moveHandle from './moveHandle';
export default moveHandle;
@@ -0,0 +1,81 @@
import { cornerstone } from 'meteor/ohif:cornerstone';
import setHandlesPosition from './setHandlesPosition';
import calculateLongestAndShortestDiameters from '../calculateLongestAndShortestDiameters';
export default function (mouseEventData, toolType, data, handle, doneMovingCallback, preventHandleOutsideImage) {
const element = mouseEventData.element;
const distanceFromTool = {
x: handle.x - mouseEventData.currentPoints.image.x,
y: handle.y - mouseEventData.currentPoints.image.y
};
const mouseDragCallback = event => {
const eventData = event.detail;
handle.active = true;
if (handle.index === undefined || handle.index === null) {
handle.x = eventData.currentPoints.image.x + distanceFromTool.x;
handle.y = eventData.currentPoints.image.y + distanceFromTool.y;
} else {
setHandlesPosition(handle, eventData, data);
}
if (preventHandleOutsideImage) {
handle.x = Math.max(handle.x, 0);
handle.x = Math.min(handle.x, eventData.image.width);
handle.y = Math.max(handle.y, 0);
handle.y = Math.min(handle.y, eventData.image.height);
}
cornerstone.updateImage(element);
const measurementModifiedHandler = () => {
const eventType = 'cornerstonetoolsmeasurementmodified';
const modifiedEventData = {
toolType,
element,
measurementData: data
};
calculateLongestAndShortestDiameters(mouseEventData, data);
cornerstone.triggerEvent(element, eventType, modifiedEventData);
element.removeEventListener('cornerstoneimagerendered', measurementModifiedHandler);
};
// Wait on image render before triggering the modified event
element.addEventListener('cornerstoneimagerendered', measurementModifiedHandler);
};
element.addEventListener('cornerstonetoolsmousedrag', mouseDragCallback);
const currentImage = cornerstone.getImage(element);
const imageRenderedHandler = () => {
const newImage = cornerstone.getImage(element);
// Check if the rendered image changed during measurement modifying and stop it if so
if (newImage.imageId !== currentImage.imageId) {
mouseUpCallback();
}
};
// Bind the event listener for image rendering
element.addEventListener('cornerstoneimagerendered', imageRenderedHandler);
const mouseUpCallback = () => {
element.removeEventListener('cornerstonetoolsmousedrag', mouseDragCallback);
element.removeEventListener('cornerstonetoolsmouseup', mouseUpCallback);
element.removeEventListener('cornerstonetoolsmouseclick', mouseUpCallback);
element.removeEventListener('cornerstoneimagerendered', imageRenderedHandler);
cornerstone.updateImage(element);
if (typeof doneMovingCallback === 'function') {
doneMovingCallback();
}
};
element.addEventListener('cornerstonetoolsmouseup', mouseUpCallback);
element.addEventListener('cornerstonetoolsmouseclick', mouseUpCallback);
}
@@ -0,0 +1,60 @@
import { cornerstoneMath } from 'meteor/ohif:cornerstone';
// Move long-axis start point
export default function(eventData, data) {
const { distance } = cornerstoneMath.point;
const { start, end, perpendicularStart, perpendicularEnd } = data.handles;
const { image } = eventData.currentPoints;
const longLine = {
start: {
x: start.x,
y: start.y
},
end: {
x: end.x,
y: end.y
}
};
const perpendicularLine = {
start: {
x: perpendicularStart.x,
y: perpendicularStart.y
},
end: {
x: perpendicularEnd.x,
y: perpendicularEnd.y
}
};
const intersection = cornerstoneMath.lineSegment.intersectLine(longLine, perpendicularLine);
const distanceFromPerpendicularP1 = distance(perpendicularStart, intersection);
const distanceFromPerpendicularP2 = distance(perpendicularEnd, intersection);
const distanceToLineP2 = distance(end, intersection);
const newLineLength = distance(end, image);
if (newLineLength <= distanceToLineP2) {
return false;
}
const dx = (end.x - image.x) / newLineLength;
const dy = (end.y - image.y) / newLineLength;
const k = distanceToLineP2 / newLineLength;
const newIntersection = {
x: end.x + ((image.x - end.x) * k),
y: end.y + ((image.y - end.y) * k)
};
perpendicularStart.x = newIntersection.x - distanceFromPerpendicularP1 * dy;
perpendicularStart.y = newIntersection.y + distanceFromPerpendicularP1 * dx;
perpendicularEnd.x = newIntersection.x + distanceFromPerpendicularP2 * dy;
perpendicularEnd.y = newIntersection.y - distanceFromPerpendicularP2 * dx;
return true;
}
@@ -0,0 +1,60 @@
import { cornerstoneMath } from 'meteor/ohif:cornerstone';
// Move long-axis end point
export default function(eventData, data) {
const { distance } = cornerstoneMath.point;
const { start, end, perpendicularStart, perpendicularEnd } = data.handles;
const { image } = eventData.currentPoints;
const longLine = {
start: {
x: start.x,
y: start.y
},
end: {
x: end.x,
y: end.y
}
};
const perpendicularLine = {
start: {
x: perpendicularStart.x,
y: perpendicularStart.y
},
end: {
x: perpendicularEnd.x,
y: perpendicularEnd.y
}
};
const intersection = cornerstoneMath.lineSegment.intersectLine(longLine, perpendicularLine);
const distanceFromPerpendicularP1 = distance(perpendicularStart, intersection);
const distanceFromPerpendicularP2 = distance(perpendicularEnd, intersection);
const distanceToLineP2 = distance(start, intersection);
const newLineLength = distance(start, image);
if (newLineLength <= distanceToLineP2) {
return false;
}
const dx = (start.x - image.x) / newLineLength;
const dy = (start.y - image.y) / newLineLength;
const k = distanceToLineP2 / newLineLength;
const newIntersection = {
x: start.x + ((image.x - start.x) * k),
y: start.y + ((image.y - start.y) * k)
};
perpendicularStart.x = newIntersection.x + distanceFromPerpendicularP1 * dy;
perpendicularStart.y = newIntersection.y - distanceFromPerpendicularP1 * dx;
perpendicularEnd.x = newIntersection.x - distanceFromPerpendicularP2 * dy;
perpendicularEnd.y = newIntersection.y + distanceFromPerpendicularP2 * dx;
return true;
}
@@ -0,0 +1,84 @@
import { cornerstoneMath } from 'meteor/ohif:cornerstone';
// Move perpendicular line start point
export default function(eventData, data) {
const { distance } = cornerstoneMath.point;
const { start, end, perpendicularStart, perpendicularEnd } = data.handles;
const fudgeFactor = 1;
const fixedPoint = perpendicularEnd;
const movedPoint = eventData.currentPoints.image;
const distanceFromFixed = cornerstoneMath.lineSegment.distanceToPoint(data.handles, fixedPoint);
const distanceFromMoved = cornerstoneMath.lineSegment.distanceToPoint(data.handles, movedPoint);
const distanceBetweenPoints = distance(fixedPoint, movedPoint);
const total = distanceFromFixed + distanceFromMoved;
if (distanceBetweenPoints <= distanceFromFixed) {
return false;
}
const length = distance(start, end);
if (length === 0) {
return false;
}
const dx = (start.x - end.x) / length;
const dy = (start.y - end.y) / length;
const adjustedLineP1 = {
x: start.x - fudgeFactor * dx,
y: start.y - fudgeFactor * dy
};
const adjustedLineP2 = {
x: end.x + fudgeFactor * dx,
y: end.y + fudgeFactor * dy
};
perpendicularStart.x = movedPoint.x;
perpendicularStart.y = movedPoint.y;
perpendicularEnd.x = movedPoint.x - total * dy;
perpendicularEnd.y = movedPoint.y + total * dx;
const longLine = {
start: {
x: start.x,
y: start.y
},
end: {
x: end.x,
y: end.y
}
};
const perpendicularLine = {
start: {
x: perpendicularStart.x,
y: perpendicularStart.y
},
end: {
x: perpendicularEnd.x,
y: perpendicularEnd.y
}
};
const intersection = cornerstoneMath.lineSegment.intersectLine(longLine, perpendicularLine);
if (!intersection) {
if (distance(movedPoint, start) > distance(movedPoint, end)) {
perpendicularStart.x = adjustedLineP2.x + distanceFromMoved * dy;
perpendicularStart.y = adjustedLineP2.y - distanceFromMoved * dx;
perpendicularEnd.x = perpendicularStart.x - total * dy;
perpendicularEnd.y = perpendicularStart.y + total * dx;
} else {
perpendicularStart.x = adjustedLineP1.x + distanceFromMoved * dy;
perpendicularStart.y = adjustedLineP1.y - distanceFromMoved * dx;
perpendicularEnd.x = perpendicularStart.x - total * dy;
perpendicularEnd.y = perpendicularStart.y + total * dx;
}
}
return true;
}
@@ -0,0 +1,82 @@
import { cornerstoneMath } from 'meteor/ohif:cornerstone';
// Move perpendicular line end point
export default function(eventData, data) {
const { distance } = cornerstoneMath.point;
const { start, end, perpendicularStart, perpendicularEnd } = data.handles;
const fudgeFactor = 1;
const fixedPoint = perpendicularStart;
const movedPoint = eventData.currentPoints.image;
const distanceFromFixed = cornerstoneMath.lineSegment.distanceToPoint(data.handles, fixedPoint);
const distanceFromMoved = cornerstoneMath.lineSegment.distanceToPoint(data.handles, movedPoint);
const distanceBetweenPoints = distance(fixedPoint, movedPoint);
const total = distanceFromFixed + distanceFromMoved;
if (distanceBetweenPoints <= distanceFromFixed) {
return false;
}
const length = distance(start, end);
const dx = (start.x - end.x) / length;
const dy = (start.y - end.y) / length;
const adjustedLineP1 = {
x: start.x - fudgeFactor * dx,
y: start.y - fudgeFactor * dy
};
const adjustedLineP2 = {
x: end.x + fudgeFactor * dx,
y: end.y + fudgeFactor * dy
};
perpendicularStart.x = movedPoint.x + total * dy;
perpendicularStart.y = movedPoint.y - total * dx;
perpendicularEnd.x = movedPoint.x;
perpendicularEnd.y = movedPoint.y;
perpendicularEnd.locked = false;
perpendicularStart.locked = false;
const longLine = {
start: {
x: start.x,
y: start.y
},
end: {
x: end.x,
y: end.y
}
};
const perpendicularLine = {
start: {
x: perpendicularStart.x,
y: perpendicularStart.y
},
end: {
x: perpendicularEnd.x,
y: perpendicularEnd.y
}
};
const intersection = cornerstoneMath.lineSegment.intersectLine(longLine, perpendicularLine);
if (!intersection) {
if (distance(movedPoint, start) > distance(movedPoint, end)) {
perpendicularEnd.x = adjustedLineP2.x - distanceFromMoved * dy;
perpendicularEnd.y = adjustedLineP2.y + distanceFromMoved * dx;
perpendicularStart.x = perpendicularEnd.x + total * dy;
perpendicularStart.y = perpendicularEnd.y - total * dx;
} else {
perpendicularEnd.x = adjustedLineP1.x - distanceFromMoved * dy;
perpendicularEnd.y = adjustedLineP1.y + distanceFromMoved * dx;
perpendicularStart.x = perpendicularEnd.x + total * dy;
perpendicularStart.y = perpendicularEnd.y - total * dx;
}
}
return true;
}
@@ -0,0 +1,140 @@
import { cornerstoneMath } from 'meteor/ohif:cornerstone';
import perpendicularBothFixedLeft from './perpendicularBothFixedLeft';
import perpendicularBothFixedRight from './perpendicularBothFixedRight';
import perpendicularLeftFixedPoint from './perpendicularLeftFixedPoint';
import perpendicularRightFixedPoint from './perpendicularRightFixedPoint';
// Sets position of handles(start, end, perpendicularStart, perpendicularEnd)
export default function(handle, eventData, data) {
let movedPoint,
outOfBounds,
result,
intersection,
d1,
d2;
let longLine = {},
perpendicularLine = {};
if (handle.index === 0) {
// if long-axis start point is moved
result = perpendicularBothFixedLeft(eventData, data);
if (result) {
handle.x = eventData.currentPoints.image.x;
handle.y = eventData.currentPoints.image.y;
} else {
eventData.currentPoints.image.x = handle.x;
eventData.currentPoints.image.y = handle.y;
}
} else if (handle.index === 1) {
// if long-axis end point is moved
result = perpendicularBothFixedRight(eventData, data);
if (result) {
handle.x = eventData.currentPoints.image.x;
handle.y = eventData.currentPoints.image.y;
} else {
eventData.currentPoints.image.x = handle.x;
eventData.currentPoints.image.y = handle.y;
}
} else if (handle.index === 2) {
outOfBounds = false;
// if perpendicular start point is moved
longLine.start = {
x: data.handles.start.x,
y: data.handles.start.y
};
longLine.end = {
x: data.handles.end.x,
y: data.handles.end.y
};
perpendicularLine.start = {
x: data.handles.perpendicularEnd.x,
y: data.handles.perpendicularEnd.y
};
perpendicularLine.end = {
x: eventData.currentPoints.image.x,
y: eventData.currentPoints.image.y
};
intersection = cornerstoneMath.lineSegment.intersectLine(longLine, perpendicularLine);
if (!intersection) {
perpendicularLine.end = {
x: data.handles.perpendicularStart.x,
y: data.handles.perpendicularStart.y
};
intersection = cornerstoneMath.lineSegment.intersectLine(longLine, perpendicularLine);
d1 = cornerstoneMath.point.distance(intersection, data.handles.start);
d2 = cornerstoneMath.point.distance(intersection, data.handles.end);
if (!intersection || d1 < 3 || d2 < 3) {
outOfBounds = true;
}
}
movedPoint = false;
if (!outOfBounds) {
movedPoint = perpendicularLeftFixedPoint(eventData, data);
if (!movedPoint) {
eventData.currentPoints.image.x = data.handles.perpendicularStart.x;
eventData.currentPoints.image.y = data.handles.perpendicularStart.y;
}
}
} else if (handle.index === 3) {
outOfBounds = false;
// if perpendicular end point is moved
longLine.start = {
x: data.handles.start.x,
y: data.handles.start.y
};
longLine.end = {
x: data.handles.end.x,
y: data.handles.end.y
};
perpendicularLine.start = {
x: data.handles.perpendicularStart.x,
y: data.handles.perpendicularStart.y
};
perpendicularLine.end = {
x: eventData.currentPoints.image.x,
y: eventData.currentPoints.image.y
};
intersection = cornerstoneMath.lineSegment.intersectLine(longLine, perpendicularLine);
if (!intersection) {
perpendicularLine.end = {
x: data.handles.perpendicularEnd.x,
y: data.handles.perpendicularEnd.y
};
intersection = cornerstoneMath.lineSegment.intersectLine(longLine, perpendicularLine);
d1 = cornerstoneMath.point.distance(intersection, data.handles.start);
d2 = cornerstoneMath.point.distance(intersection, data.handles.end);
if (!intersection || d1 < 3 || d2 < 3) {
outOfBounds = true;
}
}
movedPoint = false;
if (!outOfBounds) {
movedPoint = perpendicularRightFixedPoint(eventData, data);
if (!movedPoint) {
eventData.currentPoints.image.x = data.handles.perpendicularEnd.x;
eventData.currentPoints.image.y = data.handles.perpendicularEnd.y;
}
}
}
}
@@ -0,0 +1,22 @@
import { cornerstoneTools } from 'meteor/ohif:cornerstone';
// Add a proxy to cornerstoneTools.drawHandles function to change the handles' active state base on
// the hover, moving and selected states
export default function(context, eventData, handles, color, options) {
Object.keys(handles).forEach(handleKey => {
if (handleKey === 'textBox') return;
const handle = handles[handleKey];
handle.drawnIndependently = handle.moving;
if (handle.selected) {
handle.active = handle.hover;
} else {
if (handle.hover) {
handle.active = true;
} else {
handle.active = false;
}
}
});
cornerstoneTools.drawHandles(context, eventData, handles, color, options);
}
@@ -0,0 +1,16 @@
import { cornerstone } from 'meteor/ohif:cornerstone';
// draw perpendicular line
export default function(context, element, data, color, lineWidth) {
// Draw perpendicular line
const { perpendicularStart, perpendicularEnd } = data.handles;
const perpendicularStartCanvas = cornerstone.pixelToCanvas(element, perpendicularStart);
const perpendicularEndCanvas = cornerstone.pixelToCanvas(element, perpendicularEnd);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.moveTo(perpendicularStartCanvas.x, perpendicularStartCanvas.y);
context.lineTo(perpendicularEndCanvas.x, perpendicularEndCanvas.y);
context.stroke();
}
@@ -0,0 +1,39 @@
import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core';
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
// Draw a line marker over the selected arm
export default function(eventData, handles, color) {
const lib = OHIF.lesiontracker.bidirectional;
const { canvasContext, element } = eventData;
const handleKey = lib.getSelectedHandleKey(handles);
if (!handleKey) return;
const handle = handles[handleKey];
// Used a big distance (1km) to fill the entire line
const mmStep = -1000000;
// Get the line's start and end points
const fakeImage = {
columnPixelSpacing: eventData.viewport.scale,
rowPixelSpacing: eventData.viewport.scale
};
const pointA = lib.repositionBidirectionalArmHandle(fakeImage, handles, handleKey, mmStep, 0);
const pointB = _.pick(handle, ['x', 'y']);
// Stop here if pointA is not present
if (!pointA) return;
// Get the canvas coordinates for the line var perpendicularStartCanvas = cornerstone.pixelToCanvas(element, data.handles.perpendicularStart);
const canvasPointA = cornerstone.pixelToCanvas(element, pointA);
const canvasPointB = cornerstone.pixelToCanvas(element, pointB);
// Draw the line marker
canvasContext.beginPath();
canvasContext.strokeStyle = color;
canvasContext.lineWidth = cornerstoneTools.toolStyle.getToolWidth();
canvasContext.moveTo(canvasPointA.x, canvasPointA.y);
canvasContext.lineTo(canvasPointB.x, canvasPointB.y);
canvasContext.stroke();
}
@@ -0,0 +1,2 @@
import onImageRendered from './onImageRendered';
export default onImageRendered;
@@ -0,0 +1,204 @@
import { cornerstone, cornerstoneMath, cornerstoneTools } from 'meteor/ohif:cornerstone';
import { OHIF } from 'meteor/ohif:core';
import { toolType } from '../definitions';
import drawHandles from './drawHandles';
import calculateLongestAndShortestDiameters from '../calculateLongestAndShortestDiameters';
import updatePerpendicularLineHandles from '../updatePerpendicularLineHandles';
import drawPerpendicularLine from './drawPerpendicularLine';
import drawSelectedMarker from './drawSelectedMarker';
export default function onImageRendered(event) {
const eventData = event.detail;
const { element, canvasContext } = eventData;
// if we have no toolData for this element, return immediately as there is nothing to do
const toolData = cornerstoneTools.getToolState(element, toolType);
if (!toolData) return;
const imagePlane = cornerstone.metaData.get('imagePlaneModule', eventData.image.imageId);
let rowPixelSpacing;
let colPixelSpacing;
if (imagePlane) {
rowPixelSpacing = imagePlane.rowPixelSpacing || imagePlane.rowImagePixelSpacing;
colPixelSpacing = imagePlane.columnPixelSpacing || imagePlane.colImagePixelSpacing;
} else {
rowPixelSpacing = eventData.image.rowPixelSpacing;
colPixelSpacing = eventData.image.columnPixelSpacing;
}
// LT-29 Disable Target Measurements when pixel spacing is not available
if (!rowPixelSpacing || !colPixelSpacing) {
return;
}
// we have tool data for this element - iterate over each one and draw it
const context = canvasContext.canvas.getContext('2d');
context.setTransform(1, 0, 0, 1, 0, 0);
let color;
const lineWidth = cornerstoneTools.toolStyle.getToolWidth();
const config = cornerstoneTools[toolType].getConfiguration();
for (let i = 0; i < toolData.data.length; i++) {
const data = toolData.data[i];
if (data.visible === false) continue;
const { start, end, perpendicularStart, perpendicularEnd, textBox } = data.handles;
const strokeWidth = lineWidth;
context.save();
// configurable shadow from CornerstoneTools
const { shadow } = config;
if (shadow && shadow.shadow) {
context.shadowColor = shadow.shadowColor || '#000000';
context.shadowOffsetX = shadow.shadowOffsetX || 1;
context.shadowOffsetY = shadow.shadowOffsetY || 1;
}
const activeColor = cornerstoneTools.toolColors.getActiveColor();
if (data.active) {
color = activeColor;
} else {
color = cornerstoneTools.toolColors.getToolColor();
}
// Update the perpendicular handles to draw it correctly
updatePerpendicularLineHandles(eventData, data);
// Draw the line
const { pixelToCanvas } = cornerstone;
const handleStartCanvas = pixelToCanvas(element, start);
const handleEndCanvas = pixelToCanvas(element, end);
const handlePerpendicularStartCanvas = pixelToCanvas(element, perpendicularStart);
const handlePerpendicularEndCanvas = pixelToCanvas(element, perpendicularEnd);
const canvasTextLocation = pixelToCanvas(element, textBox);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = strokeWidth;
context.moveTo(handleStartCanvas.x, handleStartCanvas.y);
context.lineTo(handleEndCanvas.x, handleEndCanvas.y);
context.stroke();
// Draw perpendicular line
drawPerpendicularLine(context, element, data, color, strokeWidth);
// Draw the handles
const handlesColor = color;
drawHandles(context, eventData, data.handles, handlesColor, { drawHandlesIfActive: true });
// Draw the selected marker
drawSelectedMarker(eventData, data.handles, '#FF9999');
// Calculate the longest and shortest diameters, storing it in the respective attributes
calculateLongestAndShortestDiameters(eventData, data);
if (data.measurementNumber) {
// Draw the textbox
let suffix = ' mm';
if (!rowPixelSpacing || !colPixelSpacing) {
suffix = ' pixels';
}
const lengthText = ' L ' + data.longestDiameter + suffix;
const widthText = ' W ' + data.shortestDiameter + suffix;
let textLines = [`Target ${data.measurementNumber}`, lengthText, widthText];
// Append extra text lines when applies
if (data.additionalData && Array.isArray(data.additionalData.extraTextLines)) {
textLines = textLines.concat(data.additionalData.extraTextLines);
}
const boundingBox = cornerstoneTools.drawTextBox(
context,
textLines,
canvasTextLocation.x,
canvasTextLocation.y,
color,
config.textBox
);
textBox.boundingBox = boundingBox;
OHIF.cornerstone.repositionTextBox(eventData, data, config.textBox);
// Draw linked line as dashed
const link = {
start: {},
end: {}
};
const longLine = {
start: handleStartCanvas,
end: handleEndCanvas
};
const perpendicularLine = {
start: handlePerpendicularStartCanvas,
end: handlePerpendicularEndCanvas
};
// Check if the perpendicular line has some length (start and end are not equal)
// Note: this check is needed to prevent NaN value on the intersection result
const { distance } = cornerstoneMath.point;
const lineHasLength = distance(perpendicularLine.start, perpendicularLine.end) > 0;
// Define the lines intersection point
let linesIntersection;
if (lineHasLength) {
// As the line has length, define it as the intersection between the lines
const { intersectLine } = cornerstoneMath.lineSegment;
linesIntersection = intersectLine(longLine, perpendicularLine);
} else {
// As the line has no length, the tool is in its start position
linesIntersection = longLine.start;
}
const points = [
handleStartCanvas,
handleEndCanvas,
handlePerpendicularStartCanvas,
handlePerpendicularEndCanvas,
linesIntersection
];
link.end.x = canvasTextLocation.x;
link.end.y = canvasTextLocation.y;
link.start = cornerstoneMath.point.findClosestPoint(points, link.end);
const boundingBoxPoints = [ {
// Top middle point of bounding box
x: boundingBox.left + boundingBox.width / 2,
y: boundingBox.top
}, {
// Left middle point of bounding box
x: boundingBox.left,
y: boundingBox.top + boundingBox.height / 2
}, {
// Bottom middle point of bounding box
x: boundingBox.left + boundingBox.width / 2,
y: boundingBox.top + boundingBox.height
}, {
// Right middle point of bounding box
x: boundingBox.left + boundingBox.width,
y: boundingBox.top + boundingBox.height / 2
},
];
link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.setLineDash([ 2, 3 ]);
context.moveTo(link.start.x, link.start.y);
context.lineTo(link.end.x, link.end.y);
context.stroke();
}
context.restore();
}
}
@@ -0,0 +1,33 @@
import { cornerstone, cornerstoneMath, cornerstoneTools } from 'meteor/ohif:cornerstone';
import { distanceThreshold } from './definitions';
const pointNearPerpendicular = (element, handles, coords) => {
const lineSegment = {
start: cornerstone.pixelToCanvas(element, handles.perpendicularStart),
end: cornerstone.pixelToCanvas(element, handles.perpendicularEnd)
};
const distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
return (distanceToPoint < distanceThreshold);
};
export default function(element, data, coords) {
const { handles } = data;
const lineSegment = {
start: cornerstone.pixelToCanvas(element, handles.start),
end: cornerstone.pixelToCanvas(element, handles.end)
};
const distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
if (cornerstoneTools.pointInsideBoundingBox(handles.textBox, coords)) {
return true;
}
if (pointNearPerpendicular(element, handles, coords)) {
return true;
}
return (distanceToPoint < distanceThreshold);
}
@@ -0,0 +1,47 @@
import { Viewerbase } from 'meteor/ohif:viewerbase';
import { cornerstoneTools } from 'meteor/ohif:cornerstone';
import { toolType } from './definitions';
import createNewMeasurement from './createNewMeasurement';
import addNewMeasurement from './addNewMeasurement';
import addNewMeasurementTouch from './addNewMeasurementTouch';
import onImageRendered from './onImageRendered';
import pointNearTool from './pointNearTool';
import mouseDownCallback from './mouseDownCallback';
import mouseMoveCallback from './mouseMoveCallback';
function createToolInterface() {
const toolInterface = { toolType };
const baseInterface = {
createNewMeasurement,
onImageRendered,
pointNearTool,
toolType
};
toolInterface.mouse = cornerstoneTools.mouseButtonTool(Object.assign({
addNewMeasurement,
mouseDownCallback,
mouseMoveCallback
}, baseInterface));
toolInterface.touch = cornerstoneTools.touchTool(Object.assign({
addNewMeasurement: addNewMeasurementTouch
}, baseInterface));
return toolInterface;
}
const toolInterface = createToolInterface();
cornerstoneTools[toolType] = toolInterface.mouse;
cornerstoneTools[toolType + 'Touch'] = toolInterface.touch;
// Define an empty location callback
const emptyLocationCallback = (measurementData, eventData, doneCallback) => doneCallback();
const { shadowConfig, textBoxConfig } = Viewerbase.toolManager.getToolDefaultStates();
cornerstoneTools[toolType].setConfiguration({
getMeasurementLocationCallback: emptyLocationCallback,
changeMeasurementLocationCallback: emptyLocationCallback,
textBox: textBoxConfig,
shadow: shadowConfig
});
@@ -0,0 +1,40 @@
// Update the perpendicular line handles
export default function(eventData, data) {
if (!data.handles.perpendicularStart.locked) return;
let startX, startY, endX, endY;
const { start, end } = data.handles;
if (start.x === end.x && start.y === end.y) {
startX = start.x;
startY = start.y;
endX = end.x;
endY = end.y;
} else {
// mid point of long-axis line
const mid = {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2
};
// Length of long-axis
const dx = (start.x - end.x) * (eventData.image.columnPixelSpacing || 1);
const dy = (start.y - end.y) * (eventData.image.rowPixelSpacing || 1);
const length = Math.sqrt(dx * dx + dy * dy);
const vectorX = (start.x - end.x) / length;
const vectorY = (start.y - end.y) / length;
const perpendicularLineLength = length / 2;
startX = mid.x + (perpendicularLineLength / 2) * vectorY;
startY = mid.y - (perpendicularLineLength / 2) * vectorX;
endX = mid.x - (perpendicularLineLength / 2) * vectorY;
endY = mid.y + (perpendicularLineLength / 2) * vectorX;
}
data.handles.perpendicularStart.x = startX;
data.handles.perpendicularStart.y = startY;
data.handles.perpendicularEnd.x = endX;
data.handles.perpendicularEnd.y = endY;
}
@@ -0,0 +1,83 @@
import { OHIF } from 'meteor/ohif:core';
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
// Delete a lesion if Ctrl+D or DELETE is pressed while a lesion is selected
const keys = {
D: 68,
DELETE: 46
};
// Defined the toolTypes for which the delete dialog will be displayed when the keys are pressed
const toolTypes = [
'bidirectional',
'targetCR',
'targetUN',
'nonTarget',
'length',
'ellipticalRoi',
'rectangleRoi'
];
// Flag to prevent dialog from being displayed twice
let locked = false;
// Handler to unlock the keydown handling
const unlock = () => {
locked = false;
};
function removeMeasurementTimepoint(data, index, toolType, element) {
let { imageId } = data;
if (!imageId) {
const enabledElement = cornerstone.getEnabledElement(element);
imageId = enabledElement.image.imageId;
}
cornerstoneTools.removeToolState(element, toolType, data);
cornerstone.updateImage(element);
}
function keyDownCallback(event) {
const eventData = event.detail;
const keyCode = eventData.which;
// Stop here if the locked flag is set to true
if (locked) return;
if (keyCode === keys.DELETE ||
(keyCode === keys.D && eventData.event.ctrlKey === true)) {
const nearbyToolData = OHIF.viewerbase.toolManager.getNearbyToolData(eventData.element, eventData.currentPoints.canvas, toolTypes);
if (!nearbyToolData || nearbyToolData.tool.isCreating) return;
const dialogSettings = {
class: 'themed',
title: 'Delete measurements',
message: 'Are you sure you want to delete this measurement?',
position: eventData.currentPoints.page
};
// Set the locked flag to true
locked = true;
// TODO= Refactor this so the confirmation dialog is an
// optional settable callback in the tool's configuration
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(() => {
unlock();
removeMeasurementTimepoint(nearbyToolData.tool,
nearbyToolData.index,
nearbyToolData.toolType,
eventData.element
);
// Notify that viewer suffered changes
OHIF.measurements.triggerTimepointUnsavedChanges('deleted');
}).catch(unlock);
}
}
// module/private exports
const tool = cornerstoneTools.keyboardTool(keyDownCallback);
tool.toolTypes = toolTypes;
cornerstoneTools.deleteLesionKeyboardTool = tool;
@@ -0,0 +1,15 @@
import { OHIF } from 'meteor/ohif:core';
OHIF.viewerbase.getImageDownloadDialogAnnotationTools = () => {
const { measurementTools } = OHIF.measurements.MeasurementApi.getConfiguration();
const resultSet = new Set();
Object.values(measurementTools).forEach(toolGroup => {
toolGroup.childTools.forEach(tool => {
if (tool.childTools) return;
resultSet.add(tool.cornerstoneToolType);
});
});
return Array.from(resultSet);
};
@@ -0,0 +1,5 @@
import './bidirectionalTool';
import './imageDownload.js';
import './nonTargetTool.js';
import './deleteLesionKeyboardTool.js';
import './qualitativeTargetTools.js';
@@ -0,0 +1,474 @@
import { OHIF } from 'meteor/ohif:core';
import { Viewerbase } from 'meteor/ohif:viewerbase';
import { cornerstone, cornerstoneMath, cornerstoneTools } from 'meteor/ohif:cornerstone';
const toolType = 'nonTarget';
const toolDefaultStates = Viewerbase.toolManager.getToolDefaultStates();
const shadowConfig = toolDefaultStates.shadowConfig;
const textBoxConfig = toolDefaultStates.textBoxConfig;
const configuration = Object.assign({}, shadowConfig, {
getMeasurementLocationCallback,
changeMeasurementLocationCallback,
drawHandles: false,
drawHandlesOnHover: true,
arrowFirst: true,
textBox: textBoxConfig
});
// Used to cancel tool placement
const keys = {
ESC: 27
};
const getPosition = eventData => {
const event = eventData.event;
return {
x: event.clientX,
y: event.clientY
};
};
// Define a callback to get your text annotation
// This could be used, e.g. to open a modal
function getMeasurementLocationCallback(measurementData, eventData) {
if (OHIF.lesiontracker.removeMeasurementIfInvalid(measurementData, eventData)) {
return;
}
delete measurementData.isCreating;
OHIF.ui.showDialog('dialogNonTargetMeasurement', {
position: getPosition(eventData),
title: 'Select Lesion Location',
element: eventData.element,
measurementData
});
}
function changeMeasurementLocationCallback(measurementData, eventData) {
if (OHIF.lesiontracker.removeMeasurementIfInvalid(measurementData, eventData)) {
return;
}
OHIF.ui.showDialog('dialogNonTargetMeasurement', {
position: getPosition(eventData),
title: 'Change Lesion Location',
element: eventData.element,
measurementData,
edit: true
});
}
/// --- Mouse Tool --- ///
///////// BEGIN ACTIVE TOOL ///////
function addNewMeasurement(mouseEventData) {
const { element } = mouseEventData;
const $element = $(element);
function doneCallback() {
measurementData.active = true;
cornerstone.updateImage(element);
}
const measurementData = createNewMeasurement(mouseEventData);
measurementData.viewport = cornerstone.getViewport(element);
const tool = cornerstoneTools[toolType];
const config = tool.getConfiguration();
// associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(element, toolType, measurementData);
const disableDefaultHandlers = () => {
// since we are dragging to another place to drop the end point, we can just activate
// the end point and let the moveHandle move it for us.
element.removeEventListener('cornerstonetoolsmousemove', tool.mouseMoveCallback);
element.removeEventListener('cornerstonetoolsmousedown', tool.mouseDownCallback);
element.removeEventListener('cornerstonetoolsmousedownactivate', tool.mouseDownActivateCallback);
element.removeEventListener('cornerstonetoolsmousedoubleclick', doubleClickCallback);
};
disableDefaultHandlers();
// Add a flag for using Esc to cancel tool placement
let cancelled = false;
const cancelAction = () => {
cancelled = true;
cornerstoneTools.removeToolState(element, toolType, measurementData);
};
// Add a flag for using Esc to cancel tool placement
const keyDownHandler = event => {
// If the Esc key was pressed, set the flag to true
if (event.which === keys.ESC) {
cancelAction();
}
// Don't propagate this keydown event so it can't interfere
// with anything outside of this tool
return false;
};
// Bind a one-time event listener for the Esc key
$(element).one('keydown', keyDownHandler);
// Bind a mousedown handler to cancel the measurement if it's zero-sized
const mousedownHandler = () => {
const { start, end } = measurementData.handles;
if (!cornerstoneMath.point.distance(start, end)) {
cancelAction();
}
};
// Bind a one-time event listener for mouse down
$element.one('mousedown', mousedownHandler);
// Keep the current image and create a handler for new rendered images
const currentImage = cornerstone.getImage(element);
const currentViewport = cornerstone.getViewport(element);
const imageRenderedHandler = () => {
const newImage = cornerstone.getImage(element);
// Check if the rendered image changed during measurement creation and delete it if so
if (newImage.imageId !== currentImage.imageId) {
cornerstone.displayImage(element, currentImage, currentViewport);
cancelAction();
cornerstone.displayImage(element, newImage, currentViewport);
}
};
// Bind the event listener for image rendering
element.addEventListener('cornerstoneimagerendered', imageRenderedHandler);
// Bind the tool deactivation and enlargement handlers
element.addEventListener('cornerstonetoolstooldeactivated', cancelAction);
$element.one('ohif.viewer.viewport.toggleEnlargement', cancelAction);
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() {
if (cancelled || cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
// delete the measurement
cornerstoneTools.removeToolState(mouseEventData.element, toolType, measurementData);
} else {
config.getMeasurementLocationCallback(measurementData, mouseEventData, doneCallback);
}
// Unbind the Esc keydown hook
$element.off('keydown', keyDownHandler);
// Unbind the mouse down hook
$element.off('mousedown', mousedownHandler);
// Unbind the event listener for image rendering
element.removeEventListener('cornerstoneimagerendered', imageRenderedHandler);
// Unbind the tool deactivation and enlargement handlers
element.removeEventListener('cornerstonetoolstooldeactivated', cancelAction);
$element.off('ohif.viewer.viewport.toggleEnlargement', cancelAction);
// Disable the default handlers and re-enable again
disableDefaultHandlers();
element.addEventListener('cornerstonetoolsmousemove', tool.mouseMoveCallback);
element.addEventListener('cornerstonetoolsmousedown', tool.mouseDownCallback);
element.addEventListener('cornerstonetoolsmousedownactivate', tool.mouseDownActivateCallback);
element.addEventListener('cornerstonetoolsmousedoubleclick', doubleClickCallback);
cornerstone.updateImage(element);
});
}
function createNewMeasurement(mouseEventData) {
const imageId = mouseEventData.image.imageId;
// Get studyInstanceUid
const study = cornerstone.metaData.get('study', imageId);
const studyInstanceUid = study.studyInstanceUid;
const patientId = study.patientId;
// Get seriesInstanceUid
const series = cornerstone.metaData.get('series', imageId);
const seriesInstanceUid = series.seriesInstanceUid;
// create the measurement data for this tool with the end handle activated
const measurementData = {
isCreating: true,
visible: true,
active: true,
handles: {
start: {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
allowedOutsideImage: true,
highlight: true,
active: false
},
end: {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
allowedOutsideImage: true,
highlight: true,
active: false
},
textBox: {
x: mouseEventData.currentPoints.image.x - 50,
y: mouseEventData.currentPoints.image.y - 50,
active: false,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true,
hasBoundingBox: true
}
},
imageId: imageId,
seriesInstanceUid: seriesInstanceUid,
studyInstanceUid: studyInstanceUid,
patientId: patientId,
response: '',
isTarget: false,
toolType: 'nonTarget'
};
return measurementData;
}
///////// END ACTIVE TOOL ///////
function pointNearTool(element, data, coords) {
const lineSegment = {
start: cornerstone.pixelToCanvas(element, data.handles.start),
end: cornerstone.pixelToCanvas(element, data.handles.end)
};
const distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
if (cornerstoneTools.pointInsideBoundingBox(data.handles.textBox, coords)) {
return true;
}
return distanceToPoint < 25;
}
///////// BEGIN IMAGE RENDERING ///////
function onImageRendered(e) {
const eventData = e.detail;
const { element } = eventData;
// if we have no toolData for this element, return immediately as there is nothing to do
const toolData = cornerstoneTools.getToolState(element, toolType);
if (!toolData) {
return;
}
// we have tool data for this element - iterate over each one and draw it
const context = eventData.canvasContext.canvas.getContext('2d');
context.setTransform(1, 0, 0, 1, 0, 0);
let color;
const lineWidth = cornerstoneTools.toolStyle.getToolWidth();
const config = cornerstoneTools.nonTarget.getConfiguration();
for (let i = 0; i < toolData.data.length; i++) {
const data = toolData.data[i];
if (data.visible === false) {
continue;
}
context.save();
// configurable shadow from CornerstoneTools
if (config && config.shadow) {
context.shadowColor = config.shadowColor || '#000000';
context.shadowOffsetX = config.shadowOffsetX || 1;
context.shadowOffsetY = config.shadowOffsetY || 1;
}
if (data.active) {
color = cornerstoneTools.toolColors.getActiveColor();
} else {
color = cornerstoneTools.toolColors.getToolColor();
}
// Draw the arrow
const handleStartCanvas = cornerstone.pixelToCanvas(element, data.handles.start);
const handleEndCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
const canvasTextLocation = cornerstone.pixelToCanvas(element, data.handles.textBox);
cornerstoneTools.drawArrow(context, handleEndCanvas, handleStartCanvas, color, lineWidth);
if (config.drawHandles) {
cornerstoneTools.drawHandles(context, eventData, data.handles, color);
} else if (config.drawHandlesOnHover && data.handles.start.active) {
cornerstoneTools.drawHandles(context, eventData, [ data.handles.start ], color);
} else if (config.drawHandlesOnHover && data.handles.end.active) {
cornerstoneTools.drawHandles(context, eventData, [ data.handles.end ], color);
}
// Draw the text
if (data.measurementNumber) {
const textLine = `Non-Target ${data.measurementNumber}`;
const boundingBox = cornerstoneTools.drawTextBox(context, textLine, canvasTextLocation.x, canvasTextLocation.y, color, config.textBox);
data.handles.textBox.boundingBox = boundingBox;
OHIF.cornerstone.repositionTextBox(eventData, data, config.textBox);
// Draw linked line as dashed
const link = {
start: {},
end: {}
};
const midpointCanvas = {
x: (handleStartCanvas.x + handleEndCanvas.x) / 2,
y: (handleStartCanvas.y + handleEndCanvas.y) / 2,
};
const points = [ handleStartCanvas, handleEndCanvas, midpointCanvas ];
link.end.x = canvasTextLocation.x;
link.end.y = canvasTextLocation.y;
link.start = cornerstoneMath.point.findClosestPoint(points, link.end);
const boundingBoxPoints = [ {
// Top middle point of bounding box
x: boundingBox.left + boundingBox.width / 2,
y: boundingBox.top
}, {
// Left middle point of bounding box
x: boundingBox.left,
y: boundingBox.top + boundingBox.height / 2
}, {
// Bottom middle point of bounding box
x: boundingBox.left + boundingBox.width / 2,
y: boundingBox.top + boundingBox.height
}, {
// Right middle point of bounding box
x: boundingBox.left + boundingBox.width,
y: boundingBox.top + boundingBox.height / 2
},
];
link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.setLineDash([ 2, 3 ]);
context.moveTo(link.start.x, link.start.y);
context.lineTo(link.end.x, link.end.y);
context.stroke();
}
context.restore();
}
}
// ---- Touch tool ----
///////// BEGIN ACTIVE TOOL ///////
function addNewMeasurementTouch(touchEventData) {
const element = touchEventData.element;
function doneCallback() {
measurementData.active = true;
cornerstone.updateImage(element);
}
const measurementData = createNewMeasurement(touchEventData);
cornerstoneTools.addToolState(element, toolType, measurementData);
element.removeEventListener('cornerstonetoolstouchdrag', cornerstoneTools.nonTargetTouch.touchMoveHandle);
element.removeEventListener('cornerstonetoolsdragstartactive', cornerstoneTools.nonTargetTouch.touchDownActivateCallback);
element.removeEventListener('cornerstonetoolstap', cornerstoneTools.nonTargetTouch.tapCallback);
const config = cornerstoneTools.nonTarget.getConfiguration();
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandleTouch(touchEventData, toolType, measurementData, measurementData.handles.end, function() {
cornerstone.updateImage(element);
if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
// delete the measurement
cornerstoneTools.removeToolState(element, toolType, measurementData);
}
config.getMeasurementLocationCallback(measurementData, touchEventData, doneCallback);
element.addEventListener('cornerstonetoolstouchdrag', cornerstoneTools.nonTargetTouch.touchMoveHandle);
element.addEventListener('cornerstonetoolsdragstartactive', cornerstoneTools.nonTargetTouch.touchDownActivateCallback);
element.addEventListener('cornerstonetoolstap', cornerstoneTools.nonTargetTouch.tapCallback);
});
}
function doubleClickCallback(e) {
const eventData = e.detail;
const { element } = eventData;
let data;
function doneCallback(data, deleteTool) {
if (deleteTool === true) {
cornerstoneTools.removeToolState(element, toolType, data);
cornerstone.updateImage(element);
return;
}
data.active = false;
cornerstone.updateImage(element);
}
if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
return false;
}
// Check if the element is enabled and stop here if not
try {
cornerstone.getEnabledElement(element);
} catch (error) {
return;
}
const config = cornerstoneTools.nonTarget.getConfiguration();
const coords = eventData.currentPoints.canvas;
const toolData = cornerstoneTools.getToolState(element, toolType);
// now check to see if there is a handle we can move
if (!toolData) {
return;
}
for (let i = 0; i < toolData.data.length; i++) {
data = toolData.data[i];
if (pointNearTool(element, data, coords)) {
data.active = true;
cornerstone.updateImage(element);
// Allow relabelling via a callback
config.changeMeasurementLocationCallback(data, eventData, doneCallback);
e.stopImmediatePropagation();
return false;
}
}
}
cornerstoneTools.nonTarget = cornerstoneTools.mouseButtonTool({
addNewMeasurement,
createNewMeasurement,
onImageRendered,
pointNearTool,
toolType,
mouseDoubleClickCallback: doubleClickCallback
});
cornerstoneTools.nonTarget.setConfiguration(configuration);
cornerstoneTools.nonTargetTouch = cornerstoneTools.touchTool({
addNewMeasurement: addNewMeasurementTouch,
createNewMeasurement,
onImageRendered,
pointNearTool,
toolType
// pressCallback: doubleClickCallback
});
@@ -0,0 +1,498 @@
import { OHIF } from 'meteor/ohif:core';
import { Viewerbase } from 'meteor/ohif:viewerbase';
import { cornerstone, cornerstoneMath, cornerstoneTools } from 'meteor/ohif:cornerstone';
const toolDefaultStates = Viewerbase.toolManager.getToolDefaultStates();
const textBoxConfig = toolDefaultStates.textBoxConfig;
const configuration = {
getMeasurementLocationCallback,
changeMeasurementLocationCallback,
drawHandles: false,
drawHandlesOnHover: true,
arrowFirst: true,
textBox: textBoxConfig
};
// Used to cancel tool placement
const keys = { ESC: 27 };
// Define a callback to get your text annotation
// This could be used, e.g. to open a modal
function getMeasurementLocationCallback(measurementData, eventData, doneCallback) {
doneCallback(window.prompt('Enter your lesion location:'));
}
function changeMeasurementLocationCallback(measurementData, eventData, doneCallback) {
doneCallback(window.prompt('Change your lesion location:'));
}
function createQualitativeTargetTool(toolType, responseText='') {
const toolInterface = { toolType };
const response = responseText;
/// --- Mouse Tool --- ///
///////// BEGIN ACTIVE TOOL ///////
function addNewMeasurement(mouseEventData) {
const { element } = mouseEventData;
const $element = $(element);
function doneCallback() {
measurementData.active = true;
cornerstone.updateImage(element);
}
const measurementData = createNewMeasurement(mouseEventData);
measurementData.viewport = cornerstone.getViewport(element);
const tool = cornerstoneTools[toolType];
const config = tool.getConfiguration();
// associate this data with this imageId so we can render it and manipulate it
cornerstoneTools.addToolState(element, toolType, measurementData);
const disableDefaultHandlers = () => {
// since we are dragging to another place to drop the end point, we can just activate
// the end point and let the moveHandle move it for us.
element.removeEventListener('cornerstonetoolsmousemove', tool.mouseMoveCallback);
element.removeEventListener('cornerstonetoolsmousedown', tool.mouseDownCallback);
element.removeEventListener('cornerstonetoolsmousedownactivate', tool.mouseDownActivateCallback);
element.removeEventListener('cornerstonetoolsmousedoubleclick', doubleClickCallback);
};
disableDefaultHandlers();
// Add a flag for using Esc to cancel tool placement
let cancelled = false;
const cancelAction = () => {
cancelled = true;
cornerstoneTools.removeToolState(element, toolType, measurementData);
};
// Add a flag for using Esc to cancel tool placement
const keyDownHandler = event => {
// If the Esc key was pressed, set the flag to true
if (event.which === keys.ESC) {
cancelAction();
}
// Don't propagate this keydown event so it can't interfere
// with anything outside of this tool
return false;
};
// Bind a one-time event listener for the Esc key
$(element).one('keydown', keyDownHandler);
// Bind a mousedown handler to cancel the measurement if it's zero-sized
const mousedownHandler = () => {
const { start, end } = measurementData.handles;
if (!cornerstoneMath.point.distance(start, end)) {
cancelAction();
}
};
// Bind a one-time event listener for mouse down
$element.one('mousedown', mousedownHandler);
// Keep the current image and create a handler for new rendered images
const currentImage = cornerstone.getImage(element);
const currentViewport = cornerstone.getViewport(element);
const imageRenderedHandler = () => {
const newImage = cornerstone.getImage(element);
// Check if the rendered image changed during measurement creation and delete it if so
if (newImage.imageId !== currentImage.imageId) {
cornerstone.displayImage(element, currentImage, currentViewport);
cancelAction();
cornerstone.displayImage(element, newImage, currentViewport);
}
};
// Bind the event listener for image rendering
element.addEventListener('cornerstoneimagerendered', imageRenderedHandler);
// Bind the tool deactivation and enlargement handlers
element.addEventListener('cornerstonetoolstooldeactivated', cancelAction);
$element.one('ohif.viewer.viewport.toggleEnlargement', cancelAction);
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() {
if (cancelled || cornerstoneTools.anyHandlesOutsideImage(mouseEventData, measurementData.handles)) {
// delete the measurement
cornerstoneTools.removeToolState(mouseEventData.element, toolType, measurementData);
} else {
config.getMeasurementLocationCallback(measurementData, mouseEventData, doneCallback);
}
// Unbind the Esc keydown hook
$element.off('keydown', keyDownHandler);
// Unbind the mouse down hook
$element.off('mousedown', mousedownHandler);
// Unbind the event listener for image rendering
element.removeEventListener('cornerstoneimagerendered', imageRenderedHandler);
// Unbind the tool deactivation and enlargement handlers
element.removeEventListener('cornerstonetoolstooldeactivated', cancelAction);
$element.off('ohif.viewer.viewport.toggleEnlargement', cancelAction);
// Disable the default handlers and re-enable again
disableDefaultHandlers();
element.addEventListener('cornerstonetoolsmousemove', tool.mouseMoveCallback);
element.addEventListener('cornerstonetoolsmousedown', tool.mouseDownCallback);
element.addEventListener('cornerstonetoolsmousedownactivate', tool.mouseDownActivateCallback);
element.addEventListener('cornerstonetoolsmousedoubleclick', doubleClickCallback);
cornerstone.updateImage(element);
});
}
function createNewMeasurement(mouseEventData) {
const imageId = mouseEventData.image.imageId;
// Get studyInstanceUid
const study = cornerstone.metaData.get('study', imageId);
const studyInstanceUid = study.studyInstanceUid;
const patientId = study.patientId;
// Get seriesInstanceUid
const series = cornerstone.metaData.get('series', imageId);
const seriesInstanceUid = series.seriesInstanceUid;
// create the measurement data for this tool with the end handle activated
const measurementData = {
isCreating: true,
visible: true,
active: true,
handles: {
start: {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
allowedOutsideImage: true,
highlight: true,
active: false
},
end: {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
allowedOutsideImage: true,
highlight: true,
active: false
},
textBox: {
x: mouseEventData.currentPoints.image.x - 50,
y: mouseEventData.currentPoints.image.y - 50,
active: false,
movesIndependently: false,
drawnIndependently: true,
allowedOutsideImage: true,
hasBoundingBox: true
}
},
imageId: imageId,
seriesInstanceUid: seriesInstanceUid,
studyInstanceUid: studyInstanceUid,
patientId: patientId,
response: response,
isTarget: true,
toolType: toolType
};
return measurementData;
}
///////// END ACTIVE TOOL ///////
function pointNearTool(element, data, coords) {
const lineSegment = {
start: cornerstone.pixelToCanvas(element, data.handles.start),
end: cornerstone.pixelToCanvas(element, data.handles.end)
};
const distanceToPoint = cornerstoneMath.lineSegment.distanceToPoint(lineSegment, coords);
if (cornerstoneTools.pointInsideBoundingBox(data.handles.textBox, coords)) {
return true;
}
return distanceToPoint < 25;
}
function drawDottedArrow(context, start, end, color, lineWidth) {
//variables to be used when creating the arrow
const headLength = 10;
const angle = Math.atan2(end.y - start.y, end.x - start.x);
//starting path of the arrow from the start square to the end square and drawing the stroke
context.beginPath();
context.moveTo(start.x, start.y);
context.lineTo(end.x, end.y);
context.strokeStyle = color;
context.lineWidth = lineWidth;
//context.setLineDash([ 2, 3 ]);
context.stroke();
//starting a new path from the head of the arrow to one of the sides of the point
context.beginPath();
context.moveTo(end.x, end.y);
context.lineTo(end.x - headLength * Math.cos(angle - Math.PI / 7), end.y - headLength * Math.sin(angle - Math.PI / 7));
//path from the side point of the arrow, to the other side point
context.lineTo(end.x - headLength * Math.cos(angle + Math.PI / 7), end.y - headLength * Math.sin(angle + Math.PI / 7));
//path from the side point back to the tip of the arrow, and then again to the opposite side point
context.lineTo(end.x, end.y);
context.lineTo(end.x - headLength * Math.cos(angle - Math.PI / 7), end.y - headLength * Math.sin(angle - Math.PI / 7));
//draws the paths created above
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.stroke();
context.fillStyle = color;
context.fill();
}
///////// BEGIN IMAGE RENDERING ///////
function onImageRendered(e) {
const eventData = e.detail;
const { element } = eventData;
// if we have no toolData for this element, return immediately as there is nothing to do
const toolData = cornerstoneTools.getToolState(element, toolType);
if (!toolData) return;
// we have tool data for this element - iterate over each one and draw it
const context = eventData.canvasContext.canvas.getContext('2d');
context.setTransform(1, 0, 0, 1, 0, 0);
let color;
const lineWidth = cornerstoneTools.toolStyle.getToolWidth();
const config = cornerstoneTools[toolType].getConfiguration();
for (let i = 0; i < toolData.data.length; i++) {
const data = toolData.data[i];
if (data.visible === false) {
continue;
}
context.save();
// configurable shadow from CornerstoneTools
if (config && config.shadow) {
context.shadowColor = config.shadowColor || '#000000';
context.shadowOffsetX = config.shadowOffsetX || 1;
context.shadowOffsetY = config.shadowOffsetY || 1;
}
if (data.active) {
color = cornerstoneTools.toolColors.getActiveColor();
} else {
color = cornerstoneTools.toolColors.getToolColor();
}
// Draw the arrow
const handleStartCanvas = cornerstone.pixelToCanvas(element, data.handles.start);
const handleEndCanvas = cornerstone.pixelToCanvas(element, data.handles.end);
const canvasTextLocation = cornerstone.pixelToCanvas(element, data.handles.textBox);
drawDottedArrow(context, handleEndCanvas, handleStartCanvas, color, lineWidth);
if (config.drawHandles) {
cornerstoneTools.drawHandles(context, eventData, data.handles, color);
} else if (config.drawHandlesOnHover && data.handles.start.active) {
cornerstoneTools.drawHandles(context, eventData, [data.handles.start], color);
} else if (config.drawHandlesOnHover && data.handles.end.active) {
cornerstoneTools.drawHandles(context, eventData, [data.handles.end], color);
}
// Draw the text
if (data.measurementNumber) {
const textLines = [`Target ${data.measurementNumber}`, response];
const boundingBox = cornerstoneTools.drawTextBox(
context,
textLines,
canvasTextLocation.x,
canvasTextLocation.y,
color,
config.textBox
);
data.handles.textBox.boundingBox = boundingBox;
OHIF.cornerstone.repositionTextBox(eventData, data, config.textBox);
// Draw linked line as dashed
const link = {
start: {},
end: {}
};
const midpointCanvas = {
x: (handleStartCanvas.x + handleEndCanvas.x) / 2,
y: (handleStartCanvas.y + handleEndCanvas.y) / 2,
};
const points = [ handleStartCanvas, handleEndCanvas, midpointCanvas ];
link.end.x = canvasTextLocation.x;
link.end.y = canvasTextLocation.y;
link.start = cornerstoneMath.point.findClosestPoint(points, link.end);
const boundingBoxPoints = [ {
// Top middle point of bounding box
x: boundingBox.left + boundingBox.width / 2,
y: boundingBox.top
}, {
// Left middle point of bounding box
x: boundingBox.left,
y: boundingBox.top + boundingBox.height / 2
}, {
// Bottom middle point of bounding box
x: boundingBox.left + boundingBox.width / 2,
y: boundingBox.top + boundingBox.height
}, {
// Right middle point of bounding box
x: boundingBox.left + boundingBox.width,
y: boundingBox.top + boundingBox.height / 2
},
];
link.end = cornerstoneMath.point.findClosestPoint(boundingBoxPoints, link.start);
context.beginPath();
context.strokeStyle = color;
context.lineWidth = lineWidth;
context.setLineDash([ 2, 3 ]);
context.moveTo(link.start.x, link.start.y);
context.lineTo(link.end.x, link.end.y);
context.stroke();
}
context.restore();
}
}
// ---- Touch tool ----
///////// BEGIN ACTIVE TOOL ///////
function addNewMeasurementTouch(touchEventData) {
const { element } = touchEventData;
function doneCallback() {
measurementData.active = true;
cornerstone.updateImage(element);
}
const measurementData = createNewMeasurement(touchEventData);
cornerstoneTools.addToolState(element, toolType, measurementData);
const touchTool = cornerstoneTools[toolType + 'Touch'];
element.removeEventListener('cornerstonetoolstouchdrag', touchTool.touchMoveHandle);
element.removeEventListener('cornerstonetoolsdragstartactive', touchTool.touchDownActivateCallback);
element.removeEventListener('cornerstonetoolstap', touchTool.tapCallback);
const config = cornerstoneTools[toolType].getConfiguration();
cornerstone.updateImage(element);
cornerstoneTools.moveNewHandleTouch(touchEventData, toolType, measurementData, measurementData.handles.end, function() {
cornerstone.updateImage(element);
if (cornerstoneTools.anyHandlesOutsideImage(touchEventData, measurementData.handles)) {
// delete the measurement
cornerstoneTools.removeToolState(element, toolType, measurementData);
}
config.getMeasurementLocationCallback(measurementData, touchEventData, doneCallback);
element.addEventListener('cornerstonetoolstouchdrag', touchTool.touchMoveHandle);
element.addEventListener('cornerstonetoolsdragstartactive', touchTool.touchDownActivateCallback);
element.addEventListener('cornerstonetoolstap', touchTool.tapCallback);
});
}
function doubleClickCallback(e) {
const eventData = e.detail;
const { element } = eventData;
let data;
function doneCallback(data, deleteTool) {
if (deleteTool === true) {
cornerstoneTools.removeToolState(element, toolType, data);
cornerstone.updateImage(element);
return;
}
data.active = false;
cornerstone.updateImage(element);
}
if (e.data && e.data.mouseButtonMask && !cornerstoneTools.isMouseButtonEnabled(eventData.which, e.data.mouseButtonMask)) {
return false;
}
// Check if the element is enabled and stop here if not
try {
cornerstone.getEnabledElement(element);
} catch (error) {
return;
}
const config = cornerstoneTools[toolType].getConfiguration();
const coords = eventData.currentPoints.canvas;
const toolData = cornerstoneTools.getToolState(element, toolType);
// now check to see if there is a handle we can move
if (!toolData) {
return;
}
for (let i = 0; i < toolData.data.length; i++) {
data = toolData.data[i];
if (pointNearTool(element, data, coords)) {
data.active = true;
cornerstone.updateImage(element);
// Allow relabelling via a callback
config.changeMeasurementLocationCallback(data, eventData, doneCallback);
e.stopImmediatePropagation();
return false;
}
}
}
toolInterface.mouse = cornerstoneTools.mouseButtonTool({
addNewMeasurement: addNewMeasurement,
createNewMeasurement: createNewMeasurement,
onImageRendered: onImageRendered,
pointNearTool: pointNearTool,
toolType: toolType,
mouseDoubleClickCallback: doubleClickCallback
});
toolInterface.touch = cornerstoneTools.touchTool({
addNewMeasurement: addNewMeasurementTouch,
createNewMeasurement: createNewMeasurement,
onImageRendered: onImageRendered,
pointNearTool: pointNearTool,
toolType: toolType
// pressCallback: doubleClickCallback
});
return toolInterface;
}
const targetCRInterface = createQualitativeTargetTool('targetCR', 'CR');
cornerstoneTools.targetCR = targetCRInterface.mouse;
cornerstoneTools.targetCR.setConfiguration(configuration);
cornerstoneTools.targetCRTouch = targetCRInterface.touch;
const targetUNInterface = createQualitativeTargetTool('targetUN', 'UN');
cornerstoneTools.targetUN = targetUNInterface.mouse;
cornerstoneTools.targetUN.setConfiguration(configuration);
cornerstoneTools.targetUNTouch = targetUNInterface.touch;
OHIF.lesiontracker.createQualitativeTargetTool = createQualitativeTargetTool;