WIP reorganization
This commit is contained in:
1 parent
6706dfe634
commit
054e37dfb7
266 files changed
+98
-620
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
|
||||
const LocationSchema = new SimpleSchema({
|
||||
id: {
|
||||
type: String,
|
||||
label: 'Location ID'
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
label: 'Location Name'
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
label: 'Selected',
|
||||
defaultValue: false
|
||||
},
|
||||
isNodal: {
|
||||
type: Boolean,
|
||||
label: 'Nodal Location',
|
||||
defaultValue: false
|
||||
}
|
||||
});
|
||||
|
||||
LesionLocations = new Meteor.Collection(null);
|
||||
LesionLocations.attachSchema(LocationSchema);
|
||||
LesionLocations._debugName = 'LesionLocations';
|
||||
|
||||
var organGroups = [
|
||||
'Abdominal/Chest Wall',
|
||||
'Adrenal',
|
||||
'Bladder',
|
||||
'Bone',
|
||||
'Brain',
|
||||
'Breast',
|
||||
'Colon',
|
||||
'Esophagus',
|
||||
'Extremities',
|
||||
'Gallbladder',
|
||||
'Kidney',
|
||||
'Liver',
|
||||
'Lung',
|
||||
'Lymph Node',
|
||||
'Mediastinum/Hilum',
|
||||
'Muscle',
|
||||
'Neck',
|
||||
'Other: Soft Tissue',
|
||||
'Ovary',
|
||||
'Pancreas',
|
||||
'Pelvis',
|
||||
'Peritoneum/Omentum',
|
||||
'Prostate',
|
||||
'Retroperitoneum',
|
||||
'Small Bowel',
|
||||
'Spleen',
|
||||
'Stomach',
|
||||
'Subcutaneous'];
|
||||
|
||||
function nameToID(name) {
|
||||
// http://stackoverflow.com/questions/29258016/remove-special-symbols-and-extra-spaces-and-make-it-camel-case-javascript
|
||||
return name
|
||||
.trim() //might need polyfill if you need to support older browsers
|
||||
.toLowerCase() //lower case everything
|
||||
.replace(/([^A-Z0-9]+)(.)/ig, //match multiple non-letter/numbers followed by any character
|
||||
function(match) {
|
||||
return arguments[2].toUpperCase(); //3rd index is the character we need to transform uppercase
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
organGroups.forEach(function(organGroup) {
|
||||
const id = nameToID(organGroup);
|
||||
|
||||
// Check if the name has 'node' in it, if so, it is nodal
|
||||
let isNodal = false;
|
||||
if (id.toLowerCase().indexOf('node') > -1) {
|
||||
isNodal = true;
|
||||
}
|
||||
|
||||
LesionLocations.insert({
|
||||
id: id,
|
||||
location: organGroup,
|
||||
selected: false,
|
||||
isNodal: isNodal
|
||||
});
|
||||
});
|
||||
|
||||
export { LesionLocations };
|
||||
@@ -0,0 +1,56 @@
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
|
||||
const ResponseSchema = new SimpleSchema({
|
||||
text: {
|
||||
type: String,
|
||||
label: 'Text'
|
||||
},
|
||||
code: {
|
||||
type: String,
|
||||
label: 'Code'
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
label: 'Selected',
|
||||
defaultValue: false
|
||||
}
|
||||
});
|
||||
|
||||
LocationResponses = new Meteor.Collection(null);
|
||||
LocationResponses.attachSchema(ResponseSchema);
|
||||
LocationResponses._debugName = 'LocationResponses';
|
||||
|
||||
LocationResponses.insert({
|
||||
text: 'Complete response',
|
||||
code: 'CR'
|
||||
});
|
||||
|
||||
LocationResponses.insert({
|
||||
text: 'Progressive disease',
|
||||
code: 'PD'
|
||||
});
|
||||
|
||||
LocationResponses.insert({
|
||||
text: 'Stable disease',
|
||||
code: 'SD'
|
||||
});
|
||||
|
||||
LocationResponses.insert({
|
||||
text: 'Present',
|
||||
code: 'Present'
|
||||
});
|
||||
|
||||
LocationResponses.insert({
|
||||
text: 'Not Evaluable',
|
||||
code: 'NE'
|
||||
});
|
||||
|
||||
LocationResponses.insert({
|
||||
text: 'Non-CR/Non-PD',
|
||||
code: 'NN'
|
||||
});
|
||||
|
||||
LocationResponses.insert({
|
||||
text: 'Excluded from Assessment',
|
||||
code: 'EX'
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
import './LesionLocations.js';
|
||||
import './LocationResponses.js';
|
||||
+163
@@ -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);
|
||||
});
|
||||
}
|
||||
+59
@@ -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);
|
||||
});
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
+41
@@ -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;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
const toolType = 'bidirectional';
|
||||
const distanceThreshold = 6;
|
||||
|
||||
export {
|
||||
toolType,
|
||||
distanceThreshold
|
||||
};
|
||||
+49
@@ -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';
|
||||
+42
@@ -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;
|
||||
+151
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import moveHandle from './moveHandle';
|
||||
export default moveHandle;
|
||||
+81
@@ -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);
|
||||
}
|
||||
+60
@@ -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;
|
||||
}
|
||||
+60
@@ -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;
|
||||
}
|
||||
+84
@@ -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;
|
||||
}
|
||||
+82
@@ -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;
|
||||
}
|
||||
+140
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -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);
|
||||
}
|
||||
+16
@@ -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();
|
||||
}
|
||||
+39
@@ -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();
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import onImageRendered from './onImageRendered';
|
||||
export default onImageRendered;
|
||||
+204
@@ -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();
|
||||
}
|
||||
}
|
||||
+33
@@ -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
|
||||
});
|
||||
+40
@@ -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;
|
||||
@@ -0,0 +1,16 @@
|
||||
<template name="dialogNonTargetMeasurement">
|
||||
{{#dialogForm (extend this
|
||||
dialogClass='modal-sm'
|
||||
schema=(choose this.schema instance.schema)
|
||||
api=instance.api
|
||||
)}}
|
||||
{{>inputSelect labelClass='form-group' key='location' hideSearch=true
|
||||
options=(clone placeholder='Choose a lesion location')}}
|
||||
{{>inputSelect labelClass='form-group' key='response' hideSearch=true
|
||||
options=(clone placeholder='Choose a lesion location response')}}
|
||||
{{#if this.edit}}
|
||||
<hr>
|
||||
{{#button class='btn btn-danger' action='remove'}}Remove{{/button}}
|
||||
{{/if}}
|
||||
{{/dialogForm}}
|
||||
</template>
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
||||
|
||||
Template.dialogNonTargetMeasurement.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.measurementTypeId = 'nonTarget';
|
||||
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
instance.schema = new SimpleSchema({
|
||||
location: config.schema.nonTargetLocation,
|
||||
response: config.schema.nonTargetResponse
|
||||
});
|
||||
|
||||
// Remove the measurement from the collection
|
||||
instance.removeMeasurement = () => {
|
||||
const measurementApi = instance.viewerData.measurementApi;
|
||||
measurementApi.deleteMeasurements('nonTargets', {
|
||||
toolItemId: instance.data.measurementData._id
|
||||
});
|
||||
|
||||
// Sync the new measurement data with cornerstone tools
|
||||
|
||||
// Commenting this out for now, we need the timepointApi
|
||||
//const baseline = timepointApi.baseline();
|
||||
//measurementApi.sortMeasurements(baseline.timepointId);
|
||||
|
||||
// Refresh the image with the measurement removed
|
||||
cornerstone.updateImage(instance.data.element);
|
||||
};
|
||||
|
||||
// Close the current dialog
|
||||
instance.closeDialog = () => instance.$('.form-action.close').trigger('click');
|
||||
|
||||
instance.api = {
|
||||
// Confirm the deletion of the current non-target measurement
|
||||
remove() {
|
||||
const dialogSettings = {
|
||||
position: instance.data.position,
|
||||
title: 'Remove Measurement',
|
||||
message: 'Are you sure you want to remove this Non-Target measurement?'
|
||||
};
|
||||
|
||||
OHIF.ui.showDialog('dialogConfirm', dialogSettings)
|
||||
.then(instance.removeMeasurement);
|
||||
|
||||
instance.closeDialog();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.dialogNonTargetMeasurement.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
const form = instance.$('form').data('component');
|
||||
|
||||
const viewerMain = $(instance.data.element).closest('.viewerMain')[0];
|
||||
instance.viewerData = Blaze.getData(viewerMain);
|
||||
|
||||
const measurementApi = instance.viewerData.measurementApi;
|
||||
const timepointApi = instance.viewerData.timepointApi;
|
||||
|
||||
const collection = measurementApi.tools[instance.measurementTypeId];
|
||||
|
||||
const measurementData = instance.data.measurementData;
|
||||
|
||||
// Get the current inserted measurement from the collection
|
||||
const currentMeasurement = collection.findOne({ _id: measurementData._id });
|
||||
|
||||
// Check if it's a edition or creation
|
||||
if (instance.data.edit) {
|
||||
// Set the data that is already defined for current measurement
|
||||
form.value(currentMeasurement);
|
||||
} else {
|
||||
// LT-112 Non-target response shall default to non-measurable on baseline, present on follow-up
|
||||
const timepoint = timepointApi.study(measurementData.studyInstanceUid)[0];
|
||||
const response = timepoint && timepoint.timepointType === 'baseline' ? 'Present' : '';
|
||||
|
||||
// Get a previously inserted Non-target
|
||||
const previousMeasurement = collection.findOne({
|
||||
_id: { $not: measurementData._id },
|
||||
measurementNumber: currentMeasurement.measurementNumber
|
||||
});
|
||||
|
||||
// Change the location for current measurement if it's not the first one
|
||||
const location = previousMeasurement && previousMeasurement.location;
|
||||
|
||||
// Set the default location and response
|
||||
form.value({
|
||||
location,
|
||||
response
|
||||
});
|
||||
|
||||
// Synchronize the measurement number with the one inserted in the collection
|
||||
measurementData.measurementNumber = currentMeasurement.measurementNumber;
|
||||
|
||||
// Refresh the image with the measurement number
|
||||
cornerstone.updateImage(instance.data.element);
|
||||
}
|
||||
|
||||
// Delete the measurement from collection when dialog is closed and not on edit mode
|
||||
instance.data.promise.catch(() => {
|
||||
if (instance.data.edit) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.removeMeasurement();
|
||||
});
|
||||
|
||||
// Update the location and response after confirming the dialog data
|
||||
instance.data.promise.then(formData => {
|
||||
measurementData.response = formData.response;
|
||||
measurementData.location = formData.location;
|
||||
|
||||
// Update the response for current measurement
|
||||
collection.update({
|
||||
_id: measurementData._id,
|
||||
}, {
|
||||
$set: { response: formData.response }
|
||||
});
|
||||
|
||||
// Change the location for all Non-Target measurements with the same measurementNumber
|
||||
collection.update({
|
||||
measurementNumber: currentMeasurement.measurementNumber
|
||||
}, {
|
||||
$set: { location: formData.location }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import './dialog/nonTargetMeasurement.html';
|
||||
import './dialog/nonTargetMeasurement.js';
|
||||
|
||||
import './timepointBrowser';
|
||||
|
||||
import './trialOptionsModal/irRCDescription.html';
|
||||
import './trialOptionsModal/recistDescription.html';
|
||||
|
||||
import './trialOptionsModal/trialOptionsModal.html';
|
||||
import './trialOptionsModal/trialOptionsModal.js';
|
||||
|
||||
import './longitudinal';
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* Loads multiple unassociated studies in the Viewer
|
||||
*/
|
||||
const getAssociationAssessment = () => {
|
||||
// default result value
|
||||
const assessment = {
|
||||
selected: 0,
|
||||
associated: 0
|
||||
};
|
||||
|
||||
// check if timepointApi is available
|
||||
const timepointApi = OHIF.studylist.timepointApi;
|
||||
if (timepointApi) {
|
||||
// Get a Cursor pointing to the selected Studies from the StudyList
|
||||
const selectedStudies = OHIF.studylist.getSelectedStudies();
|
||||
if (selectedStudies.length > 0) {
|
||||
assessment.selected = selectedStudies.length;
|
||||
// Loop through the selected Studies and return true if at least one study has no association.
|
||||
for (let i = selectedStudies.length - 1; i >= 0; --i) {
|
||||
let study = selectedStudies[i],
|
||||
timepoints = timepointApi.study(study.studyInstanceUid);
|
||||
if (timepoints && timepoints.length > 0) {
|
||||
assessment.associated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return assessment;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all present study / timepoint associations from the Clinical Trial
|
||||
*/
|
||||
const removeTimepointAssociations = event => {
|
||||
const dialogSettings = {
|
||||
title: 'Remove Association',
|
||||
message: 'Measurements related to this Study and Timepoint will be erased. Do you really want to delete this association?',
|
||||
confirmClass: 'btn-danger',
|
||||
position: {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
};
|
||||
|
||||
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(() => {
|
||||
// Get a Cursor pointing to the selected Studies from the StudyList
|
||||
const selectedStudies = OHIF.studylist.getSelectedStudies();
|
||||
|
||||
// Find the Timepoint that was previously referenced
|
||||
const timepointApi = OHIF.studylist.timepointApi;
|
||||
if (!timepointApi) {
|
||||
OHIF.log.error('Remove Study/Timepoint Association: No Timepoint API found.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop through the Cursor of Selected Studies
|
||||
selectedStudies.forEach(study => {
|
||||
const studyInstanceUid = study.studyInstanceUid;
|
||||
const timepoints = timepointApi.study(studyInstanceUid);
|
||||
const timepointIds = timepoints.map(t => t.timepointId);
|
||||
timepointApi.disassociateStudy(timepointIds, studyInstanceUid);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Meteor.startup(() => {
|
||||
if (!OHIF.studylist) return;
|
||||
|
||||
OHIF.studylist.dropdown.setItems([{
|
||||
action: OHIF.studylist.viewStudies,
|
||||
text: 'View',
|
||||
separatorAfter: true
|
||||
}, {
|
||||
action: () => OHIF.ui.showDialog('dialogStudyAssociation'),
|
||||
text: 'Associate',
|
||||
disabled: () => {
|
||||
const assessment = getAssociationAssessment();
|
||||
return assessment.selected < 1;
|
||||
}
|
||||
}, {
|
||||
action: removeTimepointAssociations,
|
||||
text: 'Remove Association',
|
||||
separatorAfter: true,
|
||||
disabled: () => {
|
||||
const assessment = getAssociationAssessment();
|
||||
return assessment.selected < 1 || assessment.selected !== assessment.associated;
|
||||
}
|
||||
}, {
|
||||
action: OHIF.studylist.viewSeriesDetails,
|
||||
text: 'View Series Details'
|
||||
}, {
|
||||
text: 'Anonymize',
|
||||
disabled: true
|
||||
}, {
|
||||
text: 'Send',
|
||||
disabled: true,
|
||||
separatorAfter: true
|
||||
}, {
|
||||
action: OHIF.studylist.exportSelectedStudies,
|
||||
text: 'Export',
|
||||
title: 'Export Selected Studies'
|
||||
}, {
|
||||
text: 'Delete',
|
||||
disabled: true
|
||||
}]);
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
// Longitudinal Components imports
|
||||
import './longitudinalStudyListStudy/longitudinalStudyListStudy.html';
|
||||
import './longitudinalStudyListStudy/longitudinalStudyListStudy.styl';
|
||||
import './longitudinalStudyListStudy/longitudinalStudyListStudy.js';
|
||||
|
||||
import './longitudinalViewportOverlay/imageViewportIcons.html';
|
||||
import './longitudinalViewportOverlay/longitudinalViewportOverlay.html';
|
||||
import './longitudinalViewportOverlay/longitudinalViewportOverlay.js';
|
||||
import './longitudinalViewportOverlay/longitudinalViewportOverlay.styl';
|
||||
|
||||
import './dropdown.js';
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<template name="longitudinalStudyListStudy">
|
||||
<tr class="studylistStudy noselect {{#if this.selected}}active{{/if}}">
|
||||
<td>
|
||||
{{formatPN patientName}}
|
||||
</td>
|
||||
<td>
|
||||
{{patientId}}
|
||||
</td>
|
||||
<td>
|
||||
{{accessionNumber}}
|
||||
</td>
|
||||
<td>
|
||||
{{formatDA studyDate}} {{ #if timepointName }}({{timepointName}}){{ /if }}
|
||||
</td>
|
||||
<td>
|
||||
{{modalities}}
|
||||
</td>
|
||||
<td>
|
||||
{{studyDescription}}
|
||||
</td>
|
||||
<td>
|
||||
{{numberOfStudyRelatedInstances}}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
// Use Aldeed's meteor-template-extension package to replace the
|
||||
// default StudyListStudy template.
|
||||
// See https://github.com/aldeed/meteor-template-extension
|
||||
const defaultTemplate = 'studylistStudy';
|
||||
|
||||
if (OHIF.studylist) {
|
||||
Template.longitudinalStudyListStudy.replaces(defaultTemplate);
|
||||
|
||||
// Add the TimepointName helper to the default template. The
|
||||
// HTML of this template is replaced with that of longitudinalStudyListStudy
|
||||
Template[defaultTemplate].helpers({
|
||||
timepointName() {
|
||||
const instance = Template.instance();
|
||||
const timepointApi = OHIF.studylist.timepointApi;
|
||||
if (!timepointApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timepoint = timepointApi.study(instance.data.studyInstanceUid)[0];
|
||||
if (!timepoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
return timepointApi.name(timepoint);
|
||||
}
|
||||
});
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
.studylistStudy
|
||||
cursor: pointer
|
||||
+1
@@ -0,0 +1 @@
|
||||
<template name="imageViewportIcons"></template>
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<template name="longitudinalViewportOverlay">
|
||||
<div class="imageViewerViewportOverlay noselect {{#if gt numImages 1}}controlsVisible{{/if}}">
|
||||
{{#unless tagDisplaySpecified}}
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{patientId}}</div>
|
||||
<div>{{patientName}}</div>
|
||||
<div>{{getGenderAndAge}}</div>
|
||||
<div class="icons-section">
|
||||
{{#if linked}}
|
||||
<svg class="icon-link">
|
||||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-viewport-link"}}></use>
|
||||
</svg>
|
||||
{{/if}}
|
||||
{{>imageViewportIcons this}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="topcenter dicomTag">
|
||||
<div class='timepointName'>{{timepointName}}</div>
|
||||
</div>
|
||||
<div class="topright dicomTag">
|
||||
<div>{{seriesDescription}}</div>
|
||||
<div>{{studyInfo 'AccessionNumber'}}</div>
|
||||
<div>{{formatDA (studyInfo 'AcquisitionDate')}} {{formatTM (studyInfo 'AcquisitionTime')}}</div>
|
||||
<div>{{#if seriesNumber}}S: {{seriesNumber}}{{#if gt numImages 1}},{{/if}} {{/if}}{{#if gt numImages 1}}I: {{imageIndex}}/{{numImages}}{{/if}}</div>
|
||||
</div>
|
||||
<div class="bottomright dicomTag">
|
||||
<div class="stationName">{{studyInfo 'StationName'}}</div>
|
||||
<div class="manufacturerModel">{{studyInfo 'ManufacturersModelName'}}</div>
|
||||
<div class="institutionName">{{studyInfo 'InstitutionName'}}</div>
|
||||
</div>
|
||||
<div class="bottomleft dicomTag">
|
||||
<div>
|
||||
<span>{{#if location}}Loc: {{formatNumberPrecision location 2}} mm{{/if}}</span>
|
||||
<span>{{#if thickness}}Thick: {{formatNumberPrecision thickness 2}} mm{{/if}}</span>
|
||||
<span>{{#if spacingBetweenSlices}}Spacing: {{formatNumberPrecision spacingBetweenSlices 2}} mm{{/if}}</span>
|
||||
</div>
|
||||
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 2}}x{{/if}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if tagDisplayRightOnly}}
|
||||
<div class="topright dicomTag">
|
||||
<div>{{formatPN patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div class='timepointName'>{{timepointName}}</div>
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomright dicomTag">
|
||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
||||
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
|
||||
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
|
||||
<div class="compressionIndicator">{{compression}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if tagDisplayLeftOnly}}
|
||||
<div class="topleft dicomTag">
|
||||
<div>{{formatPN patientName}}</div>
|
||||
<div>{{patientId}}</div>
|
||||
<div class='timepointName'>{{timepointName}}</div>
|
||||
<div>{{studyDescription}}</div>
|
||||
<div>{{formatDA studyDate}} {{formatTM studyTime}}</div>
|
||||
</div>
|
||||
<div class="bottomleft dicomTag">
|
||||
<div>{{#if seriesNumber}}Ser: {{seriesNumber}}{{/if}}</div>
|
||||
<div>{{#if gt numImages 1}}Img: {{instanceNumber}} ({{imageIndex}}/{{numImages}}){{/if}}</div>
|
||||
<div>{{#if frameRate}}{{formatNumberPrecision frameRate 2}} FPS{{/if}}</div>
|
||||
<div>{{imageDimensions}}</div>
|
||||
<div>{{seriesDescription}}</div>
|
||||
<div>{{#if zoom}}Zoom: {{formatNumberPrecision zoom 0}}%{{/if}}</div>
|
||||
<div class="compressionIndicator">{{compression}}</div>
|
||||
<div>{{wwwc}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if gt numImages 1}}
|
||||
{{>imageControls imageIndex=imageIndex numImages=numImages}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Session } from 'meteor/session';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { moment } from 'meteor/momentjs:moment';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
||||
|
||||
// Use Aldeed's meteor-template-extension package to replace the
|
||||
// default viewportOverlay template.
|
||||
// See https://github.com/aldeed/meteor-template-extension
|
||||
const defaultTemplate = 'viewportOverlay';
|
||||
Template.longitudinalViewportOverlay.replaces(defaultTemplate);
|
||||
|
||||
Template[defaultTemplate].onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
instance.instanceMetadata = new ReactiveVar();
|
||||
|
||||
const { DICOMTagDescriptions } = OHIF.viewerbase;
|
||||
instance.getValueByTagKeyword = tagKeyword => {
|
||||
const instanceMetadata = instance.instanceMetadata.get();
|
||||
const tagObject = DICOMTagDescriptions.find(tagKeyword);
|
||||
if (!instanceMetadata || !tagObject) return;
|
||||
return instanceMetadata.getRawValue(tagObject.tag);
|
||||
};
|
||||
});
|
||||
|
||||
Template[defaultTemplate].onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const { studyInstanceUid, seriesInstanceUid } = instance.data;
|
||||
|
||||
instance.autorun(computation => {
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
if (computation.firstRun) return;
|
||||
computation.stop();
|
||||
const imageIndex = instance.getImageIndex();
|
||||
|
||||
if (!studyInstanceUid || !seriesInstanceUid) {
|
||||
return;
|
||||
}
|
||||
|
||||
OHIF.studies.loadStudy(studyInstanceUid).then(study => {
|
||||
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
|
||||
const seriesMetadata = studyMetadata.getSeriesByUID(seriesInstanceUid);
|
||||
const instanceMetadata = seriesMetadata.getInstanceByIndex(imageIndex);
|
||||
if (!instanceMetadata) return;
|
||||
instance.instanceMetadata.set(instanceMetadata);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add the TimepointName helper to the default template. The
|
||||
// HTML of this template is replaced with that of longitudinalViewportOverlay
|
||||
Template[defaultTemplate].helpers({
|
||||
studyInfo(tagKeyword) {
|
||||
const instance = Template.instance();
|
||||
instance.instanceMetadata.dep.depend();
|
||||
return instance.getValueByTagKeyword(tagKeyword);
|
||||
},
|
||||
|
||||
getGenderAndAge() {
|
||||
const instance = Template.instance();
|
||||
const values = [];
|
||||
values.push(instance.getValueByTagKeyword('PatientSex'));
|
||||
|
||||
const patientAge = instance.getValueByTagKeyword('PatientAge');
|
||||
const patientBirthDate = instance.getValueByTagKeyword('PatientBirthDate');
|
||||
if (patientAge) {
|
||||
values.push(patientAge);
|
||||
} else if (patientBirthDate) {
|
||||
const date = moment(patientBirthDate, 'YYYYMMDD');
|
||||
const yearDiff = moment().diff(date, 'years');
|
||||
if (yearDiff) {
|
||||
values.push((yearDiff + 'Y').padStart(4, '0'));
|
||||
} else {
|
||||
const monthDiff = moment().diff(date, 'months');
|
||||
if (monthDiff) {
|
||||
values.push((monthDiff + 'M').padStart(4, '0'));
|
||||
} else {
|
||||
const dayDiff = moment().diff(date, 'days') || 0;
|
||||
values.push((dayDiff + 'D').padStart(4, '0'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values.filter(value => !!value).join(', ');
|
||||
},
|
||||
|
||||
thickness() {
|
||||
const instance = Template.instance();
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
|
||||
return instance.getValueByTagKeyword('SliceThickness');
|
||||
},
|
||||
|
||||
location() {
|
||||
const instance = Template.instance();
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
|
||||
const sliceLocation = instance.getValueByTagKeyword('SliceLocation');
|
||||
const tablePosition = instance.getValueByTagKeyword('TablePosition');
|
||||
const imagePositionPatient = instance.getValueByTagKeyword('ImagePositionPatient');
|
||||
return sliceLocation || tablePosition || imagePositionPatient;
|
||||
},
|
||||
|
||||
spacingBetweenSlices() {
|
||||
const instance = Template.instance();
|
||||
Session.get('CornerstoneNewImage' + instance.data.viewportIndex);
|
||||
|
||||
// TODO: Otherwise, displays a value derived from successive values
|
||||
// of Image Position (Patient) (0020,0032) perpendicular to
|
||||
// the Image Orientation (Patient) (0020,0037)
|
||||
|
||||
return instance.getValueByTagKeyword('SpacingBetweenSlices');
|
||||
},
|
||||
|
||||
zoom() {
|
||||
const instance = Template.instance();
|
||||
const { viewportIndex } = instance.data;
|
||||
const { getElementIfNotEmpty } = OHIF.viewerbase;
|
||||
Session.get('CornerstoneImageRendered' + viewportIndex);
|
||||
|
||||
const element = getElementIfNotEmpty(viewportIndex);
|
||||
if (!element) return;
|
||||
|
||||
const viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) return;
|
||||
|
||||
return viewport.scale;
|
||||
},
|
||||
|
||||
wwwc() {
|
||||
const instance = Template.instance();
|
||||
const { viewportIndex } = instance.data;
|
||||
const { getElementIfNotEmpty, wlPresets } = OHIF.viewerbase;
|
||||
Session.get('CornerstoneImageRendered' + viewportIndex);
|
||||
wlPresets.changeObserver.depend();
|
||||
|
||||
const element = getElementIfNotEmpty(viewportIndex);
|
||||
if (!element) return;
|
||||
|
||||
const viewport = cornerstone.getViewport(element);
|
||||
if (!viewport) return;
|
||||
|
||||
const ww = viewport.voi.windowWidth.toFixed(0);
|
||||
const wc = viewport.voi.windowCenter.toFixed(0);
|
||||
const result = [`W: ${ww}, L: ${wc}`];
|
||||
|
||||
// Check if there's a preset with this W/L
|
||||
const preset = _.findWhere(OHIF.viewer.wlPresets, {
|
||||
ww: parseInt(ww),
|
||||
wc: parseInt(wc)
|
||||
});
|
||||
|
||||
// Append the preset name to the result if found
|
||||
if (preset) {
|
||||
result .push(`(${preset.id})`);
|
||||
}
|
||||
|
||||
return result.join(' ');
|
||||
},
|
||||
|
||||
timepointName() {
|
||||
const instance = Template.instance();
|
||||
const studyInstanceUid = instance.data.studyInstanceUid;
|
||||
|
||||
const timepointApi = OHIF.viewer.timepointApi;
|
||||
if (!timepointApi) return;
|
||||
|
||||
const timepoints = timepointApi.study(studyInstanceUid);
|
||||
if (!timepoints || !timepoints.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timepoint = timepoints[0];
|
||||
|
||||
return timepointApi.name(timepoint);
|
||||
},
|
||||
|
||||
linked() {
|
||||
const linkedViewports = Session.get('StackImagePositionOffsetSynchronizerLinkedViewports') || [];
|
||||
return (linkedViewports.indexOf(this.viewportIndex) !== -1);
|
||||
}
|
||||
});
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
@require '{ohif:design}/app'
|
||||
|
||||
.imageViewerViewportOverlay .icons-section .icon-link
|
||||
transform(translateY(3px))
|
||||
@@ -0,0 +1,10 @@
|
||||
import './item.html';
|
||||
import './item.js';
|
||||
import './list.html';
|
||||
import './list.styl';
|
||||
import './quickSwitch.html';
|
||||
import './quickSwitch.js';
|
||||
import './sidebar.html';
|
||||
import './sidebar.js';
|
||||
import './studies.html';
|
||||
import './studies.js';
|
||||
@@ -0,0 +1,21 @@
|
||||
<template name="timepointBrowserItem">
|
||||
<div class="timepoint-browser-item" data-id="{{this.timepoint.timepointId}}">
|
||||
<div class="timepoint-item p-y-2">
|
||||
<div class="timepoint-details clearfix">
|
||||
<div class="timepoint-title pull-left">
|
||||
{{this.timepointApi.title timepoint}}
|
||||
</div>
|
||||
<div class="timepoint-expand-icon pull-right">
|
||||
<i class="fa fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="timepoint-date pull-right m-r-1">{{formatDA this.timepoint.earliestDate 'D-MMM-YYYY'}}</div>
|
||||
</div>
|
||||
<div class="timepoint-summary">{{choose instance.summary.get 'Click to load'}}</div>
|
||||
</div>
|
||||
{{#if this.timepointChildTemplate}}
|
||||
{{#if instance.studiesData.get}}
|
||||
{{>Template.dynamic template=this.timepointChildTemplate data=(clone this studiesInformation=instance.studiesData.get)}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { $ } from 'meteor/jquery';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.timepointBrowserItem.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { timepoint, timepointApi } = instance.data;
|
||||
const { timepointId } = timepoint;
|
||||
|
||||
const hasStudiesData = !!(timepoint.studiesData && timepoint.studiesData.length);
|
||||
instance.summary = new ReactiveVar('');
|
||||
instance.studiesData = new ReactiveVar(hasStudiesData ? timepoint.studiesData : null);
|
||||
|
||||
const updateStudiesData = newDocument => {
|
||||
const newTimepoint = newDocument || timepointApi.timepoints.findOne({ timepointId });
|
||||
if (newTimepoint && newTimepoint.studiesData && newTimepoint.studiesData.length) {
|
||||
instance.studiesData.set(newTimepoint.studiesData);
|
||||
}
|
||||
};
|
||||
|
||||
timepointApi.timepoints.find({ timepointId }).observe({ changed: updateStudiesData });
|
||||
|
||||
// Build the modalities summary of all timepoint's studies
|
||||
instance.setModalitiesSummary = () => {
|
||||
const studiesData = instance.studiesData.get();
|
||||
if (!studiesData) return;
|
||||
|
||||
const modalities = {};
|
||||
studiesData.forEach(study => {
|
||||
const modality = study.modalities || 'UN';
|
||||
modalities[modality] = modalities[modality] + 1 || 1;
|
||||
});
|
||||
|
||||
const summary = [];
|
||||
_.each(modalities, (count, modality) => summary.push(`${count} ${modality}`));
|
||||
|
||||
instance.summary.set(summary.join(', '));
|
||||
};
|
||||
|
||||
const filter = { studyInstanceUid: timepoint.studyInstanceUids };
|
||||
instance.loadStudies = () => OHIF.studies.searchStudies(filter).then(studiesData => {
|
||||
instance.studiesData.set(studiesData);
|
||||
timepointApi.timepoints.update(timepoint._id, { $set: { studiesData } });
|
||||
instance.setModalitiesSummary();
|
||||
}).catch(error => {
|
||||
const text = 'An error has occurred while retrieving studies information';
|
||||
OHIF.ui.notifications.danger({ text });
|
||||
OHIF.log.error(error);
|
||||
instance.summary.set('Failed');
|
||||
});
|
||||
|
||||
updateStudiesData();
|
||||
instance.autorun(() => {
|
||||
const studiesData = instance.studiesData.get();
|
||||
if (studiesData) {
|
||||
instance.setModalitiesSummary();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.timepointBrowserItem.events({
|
||||
'ohif.measurements.timepoint.load .timepoint-item'(event, instance) {
|
||||
instance.loadStudies();
|
||||
},
|
||||
|
||||
'click .timepoint-item'(event, instance) {
|
||||
const element = event.currentTarget.parentElement;
|
||||
const $element = $(element);
|
||||
|
||||
const triggerClick = () => {
|
||||
$element.trigger('ohif.measurements.timepoint.click', instance.data.timepoint);
|
||||
};
|
||||
|
||||
if (!instance.studiesData.get()) {
|
||||
instance.summary.set('Loading...');
|
||||
instance.loadStudies().then(() => Tracker.afterFlush(triggerClick));
|
||||
} else {
|
||||
triggerClick();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<template name="timepointBrowserList">
|
||||
<div class="timepoint-browser-list {{this.class}}">
|
||||
{{#each timepoint in this.timepoints}}
|
||||
{{>timepointBrowserItem (clone this timepoint=timepoint)}}
|
||||
<hr>
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
@require '{ohif:design}/app'
|
||||
|
||||
.timepoint-switch
|
||||
theme('border-bottom', '1px solid $uiBorderColor')
|
||||
|
||||
.timepoint-browser-item
|
||||
|
||||
.timepoint-item
|
||||
cursor: pointer
|
||||
font-size: 13px
|
||||
|
||||
&:not(.active) .timepoint-browser-studies
|
||||
max-height: 0 !important
|
||||
|
||||
.timepoint-studies-container
|
||||
opacity: 0
|
||||
transform(translateY(-100%))
|
||||
|
||||
.timepoint-browser-studies
|
||||
overflow: hidden
|
||||
transition(max-height 0.3s ease)
|
||||
|
||||
.timepoint-studies-container
|
||||
opacity: 1
|
||||
transition(opacity 0.3s ease\, transform 0.3s ease)
|
||||
transform(translateY(0))
|
||||
transform-origin(50% 0%)
|
||||
|
||||
.timepoint-date
|
||||
opacity: 1
|
||||
|
||||
&.active
|
||||
.timepoint-summary
|
||||
max-height: 0
|
||||
opacity: 0
|
||||
|
||||
.timepoint-date
|
||||
opacity: 0
|
||||
|
||||
.timepoint-expand-icon i
|
||||
transform: rotateX(180deg)
|
||||
|
||||
.timepoint-title
|
||||
font-size: 14px
|
||||
theme('color', '$textSecondaryColor')
|
||||
text-transform: uppercase
|
||||
|
||||
&
|
||||
.timepoint-expand-icon i,
|
||||
.timepoint-title,
|
||||
.timepoint-date,
|
||||
.timepoint-summary
|
||||
theme('color', '$textSecondaryColor')
|
||||
transition($sidebarTransition)
|
||||
|
||||
&:hover
|
||||
&,
|
||||
.timepoint-title,
|
||||
.timepoint-expand-icon i,
|
||||
.timepoint-date,
|
||||
.timepoint-summary
|
||||
theme('color', '$textPrimaryColor') !important
|
||||
|
||||
.timepoint-details
|
||||
.timepoint-summary
|
||||
line-height: 25px
|
||||
|
||||
.timepoint-summary
|
||||
height: 25px
|
||||
max-height: 25px
|
||||
opacity: 1
|
||||
overflow: hidden
|
||||
|
||||
.timepoint-expand-icon i
|
||||
theme('color', '$defaultColor')
|
||||
|
||||
.timepoint-title
|
||||
theme('color', '$textPrimaryColor')
|
||||
padding-top: 2px
|
||||
|
||||
.series-quick-switch .timepoint-browser-list
|
||||
padding: 0 10px
|
||||
|
||||
div.timepoint-title
|
||||
text-transform: none
|
||||
@@ -0,0 +1,3 @@
|
||||
<template name="timepointBrowserQuickSwitch">
|
||||
{{>timepointBrowserList (extend timepointBrowserData)}}
|
||||
</template>
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.timepointBrowserQuickSwitch.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { timepointApi } = OHIF.viewer;
|
||||
|
||||
instance.selectedTimepoint = new ReactiveVar();
|
||||
instance.timepoints = new ReactiveVar([]);
|
||||
|
||||
instance.updateSelectedTimepoint = studyInstanceUid => {
|
||||
const selectedTimepoint = timepointApi.study(studyInstanceUid)[0];
|
||||
instance.selectedTimepoint.set(selectedTimepoint);
|
||||
};
|
||||
|
||||
const currentTimepoint = timepointApi.current();
|
||||
const filter = { latestDate: { $lte: currentTimepoint.latestDate } };
|
||||
instance.keyTimepoints = timepointApi.key(filter);
|
||||
|
||||
const { viewportIndex } = instance.data;
|
||||
instance.autorun(() => {
|
||||
OHIF.viewerbase.layoutManager.observer.depend();
|
||||
const viewportData = OHIF.viewerbase.layoutManager.viewportData[viewportIndex];
|
||||
let { studyInstanceUid } = viewportData;
|
||||
if (!studyInstanceUid) {
|
||||
Tracker.nonreactive(() => {
|
||||
const currentStudy = instance.data.currentStudy.get();
|
||||
if (currentStudy) {
|
||||
studyInstanceUid = currentStudy.studyInstanceUid;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
instance.updateSelectedTimepoint(studyInstanceUid);
|
||||
});
|
||||
|
||||
instance.autorun(() => {
|
||||
const selectedTimepoint = instance.selectedTimepoint.get();
|
||||
const timepoints = [selectedTimepoint];
|
||||
instance.timepoints.set(timepoints);
|
||||
});
|
||||
});
|
||||
|
||||
Template.timepointBrowserQuickSwitch.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.updateActiveStudy = () => {
|
||||
const currentStudy = instance.data.currentStudy.get();
|
||||
const studyInstanceUid = (currentStudy && currentStudy.studyInstanceUid) || '';
|
||||
Tracker.afterFlush(() => {
|
||||
const $studyBrowserItems = instance.$('.study-browser-item');
|
||||
$studyBrowserItems.removeClass('active');
|
||||
$studyBrowserItems.filter(`[data-uid="${studyInstanceUid}"]`).addClass('active');
|
||||
});
|
||||
};
|
||||
|
||||
instance.autorun(() => {
|
||||
const selectedTimepoint = instance.selectedTimepoint.get();
|
||||
const selectedTimepointId = (selectedTimepoint && selectedTimepoint.timepointId) || '';
|
||||
const $allBrowserItems = instance.$('.timepoint-browser-item');
|
||||
const $browserItem = $allBrowserItems.filter(`[data-id=${selectedTimepointId}]`);
|
||||
if (!$browserItem.hasClass('active')) {
|
||||
$browserItem.find('.timepoint-item').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
instance.autorun(instance.updateActiveStudy);
|
||||
});
|
||||
|
||||
Template.timepointBrowserQuickSwitch.events({
|
||||
'ohif.measurements.timepoint.click'(event, instance) {
|
||||
const $element = $(event.currentTarget);
|
||||
$element.toggleClass('active');
|
||||
instance.updateActiveStudy();
|
||||
},
|
||||
|
||||
'ohif.studies.study.click'(event, instance, studyInformation) {
|
||||
const { studyInstanceUid } = studyInformation;
|
||||
const study = OHIF.viewer.Studies.findBy({ studyInstanceUid });
|
||||
instance.data.currentStudy.set(study);
|
||||
const $studySwitch = $(event.currentTarget).closest('.study-switch');
|
||||
$studySwitch.siblings('.series-switch').trigger('rescale');
|
||||
instance.updateSelectedTimepoint(studyInformation.studyInstanceUid);
|
||||
|
||||
// Create a hover bridge to prevent quick switch from closing due to scroll height
|
||||
Meteor.defer(() => {
|
||||
const $scrollable = $studySwitch.find('.study-browser>.scrollable');
|
||||
const offsetY = $scrollable.offset().top + $scrollable.outerHeight();
|
||||
if (event.clientY > offsetY) {
|
||||
const hoverHandler = _.throttle(event => {
|
||||
if (event.clientY <= offsetY) {
|
||||
$scrollable.css('padding-bottom', '');
|
||||
$scrollable.off('mousemove', hoverHandler);
|
||||
$scrollable.off('mouseleave', hoverHandler);
|
||||
}
|
||||
}, 100);
|
||||
$scrollable.css('padding-bottom', event.clientY - offsetY + 20);
|
||||
$scrollable.on('mousemove', hoverHandler);
|
||||
$scrollable.one('mouseleave', () => $scrollable.off('mousemove', hoverHandler));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.timepointBrowserQuickSwitch.helpers({
|
||||
timepointBrowserData() {
|
||||
const instance = Template.instance();
|
||||
const { timepointApi } = OHIF.viewer;
|
||||
return {
|
||||
timepointApi,
|
||||
timepoints: instance.timepoints.get(),
|
||||
timepointChildTemplate: 'timepointBrowserStudies'
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<template name="timepointBrowserSidebar">
|
||||
<div class="full-height flex-v noselect">
|
||||
<div class="p-x-1">
|
||||
<div class="timepoint-switch p-y-2">
|
||||
{{>roundedButtonGroup viewTypeButtonGroupData}}
|
||||
</div>
|
||||
</div>
|
||||
{{#scrollArea class='flex-grow fit' scrollableClass='p-a-1' scrollStep=191}}
|
||||
{{>timepointBrowserList (extend timepointBrowserData)}}
|
||||
{{/scrollArea}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
|
||||
Template.timepointBrowserSidebar.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Reactive variable to control the view type for all or key timepoints
|
||||
instance.timepointViewType = new ReactiveVar(instance.data.timepointViewType);
|
||||
});
|
||||
|
||||
Template.timepointBrowserSidebar.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
instance.lastType = '';
|
||||
|
||||
// Collapse all timepoints but first when timepoint view type changes
|
||||
instance.$browserList = instance.$('.timepoint-browser-list').first();
|
||||
instance.autorun(() => {
|
||||
// Runs this computation every time the timepointViewType is changed
|
||||
const type = instance.timepointViewType.get();
|
||||
if (type !== instance.lastType) {
|
||||
const eventKey = 'ohif.measurements.timepoint.changeViewType';
|
||||
instance.$browserList.trigger(eventKey, type);
|
||||
}
|
||||
|
||||
instance.lastType = type;
|
||||
});
|
||||
});
|
||||
|
||||
Template.timepointBrowserSidebar.events({
|
||||
'ohif.studies.study.click'(event, instance) {
|
||||
const $element = $(event.currentTarget);
|
||||
|
||||
// Defer the active class toggling to wait for child template rendering
|
||||
Meteor.defer(() => {
|
||||
// Remove max height restriction from studies browser
|
||||
const $studiesBrowser = $element.closest('.timepoint-browser-studies');
|
||||
$studiesBrowser.css('max-height', '');
|
||||
|
||||
// Remove active class from sibling studies
|
||||
$element.siblings().removeClass('active');
|
||||
|
||||
// Toggle the active class on clicked study
|
||||
$element.toggleClass('active');
|
||||
|
||||
// Adjust the max height for studiesBrowser when series transition is finished
|
||||
const $seriesBrowser = $element.find('.study-browser-series');
|
||||
$seriesBrowser.one('transitionend', () => $studiesBrowser.adjustMax('height'));
|
||||
});
|
||||
},
|
||||
|
||||
'ohif.measurements.timepoint.click'(event, instance) {
|
||||
const $element = $(event.currentTarget);
|
||||
|
||||
// Defer the active class toggling to wait for child template rendering
|
||||
Meteor.defer(() => $element.toggleClass('active'));
|
||||
}
|
||||
});
|
||||
|
||||
Template.timepointBrowserSidebar.helpers({
|
||||
viewTypeButtonGroupData() {
|
||||
return {
|
||||
value: Template.instance().timepointViewType,
|
||||
options: [{
|
||||
value: 'key',
|
||||
text: 'Key Timepoints'
|
||||
}, {
|
||||
value: 'all',
|
||||
text: 'All Timepoints'
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
timepointBrowserData() {
|
||||
const instance = Template.instance();
|
||||
const { timepointApi } = instance.data;
|
||||
|
||||
const currentTimepoint = timepointApi.current();
|
||||
const { patientId } = currentTimepoint;
|
||||
let timepoints = [];
|
||||
if (instance.timepointViewType.get() === 'key') {
|
||||
const filter = { latestDate: { $lte: currentTimepoint.latestDate } };
|
||||
timepoints = timepointApi.key(filter);
|
||||
} else {
|
||||
timepoints = timepointApi.all({ patientId });
|
||||
}
|
||||
|
||||
return {
|
||||
timepointApi,
|
||||
timepoints,
|
||||
timepointChildTemplate: 'timepointBrowserStudies',
|
||||
studyChildTemplate: 'studyBrowserSeries'
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<template name="timepointBrowserStudies">
|
||||
<div class="timepoint-browser-studies">
|
||||
<div class="timepoint-studies-container">
|
||||
{{>studyBrowserList this}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
Template.timepointBrowserStudies.onRendered(() => {
|
||||
Template.instance().$('.timepoint-browser-studies').adjustMax('height');
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<template name="irRCDescription">
|
||||
<h4>irRC</h4>
|
||||
<h5>Baseline Checks</h5>
|
||||
<ul>
|
||||
<li>Target lesions must be >= 10 X 10 mm AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Up to a max of 5 target lesions per organ</li>
|
||||
<li>Up to a max of 10 target lesions total</li>
|
||||
<li>Non-targets can only be assessed as 'present'</li>
|
||||
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
|
||||
</ul>
|
||||
<h5>New Lesion Checks for Follow-ups</h5>
|
||||
<ul>
|
||||
<li>New target lesions must be >= 5 X 5 mm AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Up to a max of 5 target lesions per organ</li>
|
||||
<li>Up to a max of 10 target lesions total</li>
|
||||
</ul>
|
||||
<!--Time Point Measurement Total = SPD target lesions + SPD new lesions (SPD = sum of product of long axis and short axis diameters)-->
|
||||
</template>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<template name="recistDescription">
|
||||
<h4>RECIST 1.1</h4>
|
||||
<h5>Baseline Checks</h5>
|
||||
<ul>
|
||||
<li>Extranodal lesions must be >= 10 mm long axis AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Extranodal lesions must be >= 20 mm on chest x-ray (although x-rays rarely used for clinical trial assessment)</li>
|
||||
<li>Nodal lesions must be >= 15 mm short axis AND >= double the acquisition slice thickness by CT and MR</li>
|
||||
<li>Up to a max of 2 target lesions per organ</li>
|
||||
<li>Up to a max of 5 target lesions total</li>
|
||||
<li>Non-targets can only be assessed as 'present'</li>
|
||||
<li>Target lesions must have measurements (cannot be assessed as CR, UN/NE, EX)</li>
|
||||
</ul>
|
||||
<!-- Time Point Measurement Total = Sum of long axis measurements for extranodal target lesion + short axis measurements for nodal lesions</li>-->
|
||||
</template>
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<template name="trialOptionsModal">
|
||||
{{#dialogForm (extend this
|
||||
schema=instance.schema dialogClass='modal-lg'
|
||||
confirmLabel='Save' title='Clinical Trial Options'
|
||||
)}}
|
||||
<h3 class="m-t-0">Select one or more Trial Criteria types:</h3>
|
||||
{{>groupRadio class='js-trial' labelClass='form-group' key='trialCriteria'}}
|
||||
|
||||
<h3>Selected Trial Criteria:</h3>
|
||||
<div class="m-t-2">
|
||||
{{#let descriptionTemplate=(getDescriptionTemplate instance.selectedTrial.get)}}
|
||||
{{>Template.dynamic template=descriptionTemplate}}
|
||||
{{/let}}
|
||||
</div>
|
||||
{{/dialogForm}}
|
||||
</template>
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
|
||||
Meteor.startup(() => {
|
||||
const TrialCriteriaTypes = new Meteor.Collection(null);
|
||||
TrialCriteriaTypes._debugName = 'TrialCriteriaTypes';
|
||||
|
||||
TrialCriteriaTypes.insert({
|
||||
id: 'RECIST',
|
||||
name: 'RECIST 1.1',
|
||||
descriptionTemplate: 'recistDescription',
|
||||
selected: true
|
||||
});
|
||||
|
||||
TrialCriteriaTypes.insert({
|
||||
id: 'irRC',
|
||||
name: 'irRC',
|
||||
descriptionTemplate: 'irRCDescription',
|
||||
selected: false
|
||||
});
|
||||
|
||||
OHIF.lesiontracker.TrialCriteriaTypes = TrialCriteriaTypes;
|
||||
});
|
||||
|
||||
Template.trialOptionsModal.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { TrialCriteriaTypes } = OHIF.lesiontracker;
|
||||
const types = TrialCriteriaTypes.find().fetch();
|
||||
const defaultValue = _.findWhere(types, { selected: true })._id;
|
||||
instance.selectedTrial = new ReactiveVar(defaultValue);
|
||||
|
||||
instance.schema = new SimpleSchema({
|
||||
trialCriteria: {
|
||||
type: String,
|
||||
allowedValues: _.pluck(types, '_id'),
|
||||
valuesLabels: _.pluck(types, 'name'),
|
||||
defaultValue
|
||||
}
|
||||
});
|
||||
|
||||
instance.data.promise.then(formData => {
|
||||
// Set "selected" to false for the entire collection
|
||||
TrialCriteriaTypes.update({}, {
|
||||
$set: { selected: false }
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// TODO: Use filter with "_id: $in" when allowing multiple criteria
|
||||
// Set "selected" to true for the current criteria
|
||||
TrialCriteriaTypes.update(formData.trialCriteria, {
|
||||
$set: { selected: true }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.trialOptionsModal.helpers({
|
||||
getDescriptionTemplate(_id) {
|
||||
return OHIF.lesiontracker.TrialCriteriaTypes.findOne(_id).descriptionTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
Template.trialOptionsModal.events({
|
||||
'change .js-trial'(event, instance) {
|
||||
const form = instance.$('form').first().data('component');
|
||||
if (!form) return;
|
||||
instance.selectedTrial.set(form.value().trialCriteria);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
// Client-side collections
|
||||
import './collections';
|
||||
|
||||
// Additional Custom Cornerstone Tools for Lesion Tracker
|
||||
import './compatibility';
|
||||
|
||||
import './tools.js';
|
||||
|
||||
// UI Components
|
||||
import './components';
|
||||
|
||||
import './lib';
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
export default function(handles, image={}) {
|
||||
// Calculate the long axis length
|
||||
const dx = (handles.start.x - handles.end.x) * (image.columnPixelSpacing || 1);
|
||||
const dy = (handles.start.y - handles.end.y) * (image.rowPixelSpacing || 1);
|
||||
const length = Math.sqrt(dx * dx + dy * dy) || 0;
|
||||
|
||||
// Calculate the short axis length
|
||||
const wx = (handles.perpendicularStart.x - handles.perpendicularEnd.x) * (image.columnPixelSpacing || 1);
|
||||
const wy = (handles.perpendicularStart.y - handles.perpendicularEnd.y) * (image.rowPixelSpacing || 1);
|
||||
const width = Math.sqrt(wx * wx + wy * wy) || 0;
|
||||
|
||||
return {
|
||||
longestDiameter: length.toFixed(1),
|
||||
shortestDiameter: width.toFixed(1)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Get the key for the handle which is selected
|
||||
export default function(handles) {
|
||||
let selectedHandleKey;
|
||||
Object.keys(handles).every(handleKey => {
|
||||
const handle = handles[handleKey];
|
||||
if (handle.selected) {
|
||||
selectedHandleKey = handleKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return selectedHandleKey;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import getSelectedHandleKey from './getSelectedHandleKey';
|
||||
import repositionBidirectionalArmHandle from './repositionBidirectionalArmHandle';
|
||||
import getLongestAndShortestDiameters from './getLongestAndShortestDiameters';
|
||||
|
||||
OHIF.lesiontracker.bidirectional = {
|
||||
toolType: 'bidirectional',
|
||||
inverseKeyMap: {
|
||||
start: 'end',
|
||||
end: 'start',
|
||||
perpendicularStart: 'perpendicularEnd',
|
||||
perpendicularEnd: 'perpendicularStart'
|
||||
},
|
||||
perpendicularKeyMap: {
|
||||
start: 'perpendicularStart',
|
||||
end: 'perpendicularEnd',
|
||||
perpendicularStart: 'start',
|
||||
perpendicularEnd: 'end'
|
||||
},
|
||||
getSelectedHandleKey,
|
||||
repositionBidirectionalArmHandle,
|
||||
getLongestAndShortestDiameters
|
||||
};
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
// Return the newPosition for the handle based on the mmStep and handles
|
||||
export default function(image, handles, handleKey, mmStep, mmLimit=1) {
|
||||
if (handleKey === 'textBox') return;
|
||||
|
||||
const lib = OHIF.lesiontracker.bidirectional;
|
||||
|
||||
// Defines how much the arm will increase/decrease
|
||||
const columnPixelSpacing = (image && image.columnPixelSpacing) || 1;
|
||||
const rowPixelSpacing = (image && image.rowPixelSpacing) || 1;
|
||||
const stepX = mmStep * (1 / columnPixelSpacing);
|
||||
const stepY = mmStep * (1 / rowPixelSpacing);
|
||||
|
||||
// Get the line angle and its handles
|
||||
const keyA = handleKey;
|
||||
const keyB = lib.inverseKeyMap[handleKey];
|
||||
const handleA = handles[keyA];
|
||||
const handleB = handles[keyB];
|
||||
const angle = Math.atan2(handleA.y - handleB.y, handleA.x - handleB.x);
|
||||
|
||||
// Calculate the new position of the handle
|
||||
const newPosition = {
|
||||
x: handleA.x + Math.cos(angle) * stepX,
|
||||
y: handleA.y + Math.sin(angle) * stepY
|
||||
};
|
||||
|
||||
if (mmStep < 0) {
|
||||
// Get the perpendicular handles
|
||||
const keyC = lib.perpendicularKeyMap[keyA];
|
||||
const keyD = lib.perpendicularKeyMap[keyB];
|
||||
const handleC = handles[keyC];
|
||||
const handleD = handles[keyD];
|
||||
|
||||
// Create the line segment for the arm being resized
|
||||
const lineAB = {
|
||||
start: _.pick(handleA, ['x', 'y']),
|
||||
end: _.pick(handleB, ['x', 'y'])
|
||||
};
|
||||
|
||||
// Create the line segment for the perpendicular arm
|
||||
const lineCD = {
|
||||
start: _.pick(handleC, ['x', 'y']),
|
||||
end: _.pick(handleD, ['x', 'y'])
|
||||
};
|
||||
|
||||
// Get the intersection point between the arms
|
||||
const intersection = cornerstoneMath.lineSegment.intersectLine(lineAB, lineCD);
|
||||
|
||||
// Keep the minimum distance of 0.1 mm to the intersection point
|
||||
const dx = (intersection.x - newPosition.x) * columnPixelSpacing;
|
||||
const dy = (intersection.y - newPosition.y) * rowPixelSpacing;
|
||||
const distance = Math.sqrt((dx * dx) + (dy * dy));
|
||||
const newAngle = Math.atan2(newPosition.y - intersection.y, newPosition.x - intersection.x);
|
||||
if (angle.toFixed(8) !== newAngle.toFixed(8) || distance < Math.abs(mmLimit)) {
|
||||
Object.assign(newPosition, {
|
||||
x: intersection.x - Math.cos(angle) * mmLimit * Math.sign(stepX),
|
||||
y: intersection.y - Math.sin(angle) * mmLimit * Math.sign(stepY)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newPosition;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
const removeToolDataWithMeasurementId = (imageId, toolType, measurementId) => {
|
||||
OHIF.log.info('removeToolDataWithMeasurementId');
|
||||
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
|
||||
|
||||
// Find any related toolData
|
||||
if (!toolState[imageId] || !toolState[imageId][toolType]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolData = toolState[imageId][toolType].data;
|
||||
if (!toolData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search toolData for entries linked to the specified Measurement
|
||||
const toRemove = [];
|
||||
toolData.forEach(function(measurement, index) {
|
||||
if (measurement.id === measurementId ||
|
||||
measurement._id === measurementId) {
|
||||
toRemove.push(index);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
OHIF.log.info('Removing Indices: ');
|
||||
OHIF.log.info(toRemove);
|
||||
|
||||
// If any toolData entries need to be removed, splice them from
|
||||
// the toolData array
|
||||
toRemove.forEach(function(index) {
|
||||
toolData.splice(index, 1);
|
||||
});
|
||||
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(toolState);
|
||||
};
|
||||
|
||||
OHIF.lesiontracker.clearMeasurementTimepointData = (measurementId, timepointId) => {
|
||||
const data = Measurements.findOne(measurementId);
|
||||
|
||||
// Clear the Measurement data for this timepoint
|
||||
const imageId = data.timepoints[timepointId].imageId;
|
||||
const toolType = data.toolType;
|
||||
removeToolDataWithMeasurementId(imageId, toolType, measurementId);
|
||||
|
||||
// Update any viewports that are currently displaying this imageId
|
||||
const enabledElements = cornerstone.getEnabledElementsByImageId(imageId);
|
||||
enabledElements.forEach(enabledElement => cornerstone.updateImage(enabledElement.element));
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
OHIF.lesiontracker.configureTargetToolsHandles = () => {
|
||||
const toggleLabel = (measurementData, eventData, doneCallback) => {
|
||||
delete measurementData.isCreating;
|
||||
|
||||
if (OHIF.lesiontracker.removeMeasurementIfInvalid(measurementData, eventData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getHandlePosition = key => _.pick(measurementData.handles[key], ['x', 'y']);
|
||||
const start = getHandlePosition('start');
|
||||
const end = getHandlePosition('end');
|
||||
const getDirection = axis => start[axis] < end[axis] ? 1 : -1;
|
||||
const position = OHIF.cornerstone.pixelToPage(eventData.element, end);
|
||||
|
||||
OHIF.measurements.toggleLabelButton({
|
||||
measurement: measurementData,
|
||||
element: eventData.element,
|
||||
measurementApi: OHIF.viewer.measurementApi,
|
||||
position: position,
|
||||
direction: {
|
||||
x: getDirection('x'),
|
||||
y: getDirection('y')
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const callbackConfig = {
|
||||
// TODO: Check the position for these, the Add Label button position seems very awkward
|
||||
getMeasurementLocationCallback: toggleLabel,
|
||||
changeMeasurementLocationCallback: toggleLabel,
|
||||
};
|
||||
|
||||
// TODO: Reconcile this with the configuration in toolManager it would be better to have this
|
||||
// all in one place.
|
||||
const appendConfig = toolType => {
|
||||
const tool = cornerstoneTools[toolType];
|
||||
const toolConfig = tool.getConfiguration();
|
||||
const config = Object.assign({}, toolConfig, callbackConfig);
|
||||
|
||||
tool.setConfiguration(config);
|
||||
};
|
||||
|
||||
// Append the callback configuration to bidirectional tool
|
||||
appendConfig('bidirectional');
|
||||
|
||||
// Append the callback configuration to CR and UN tools
|
||||
appendConfig('targetCR');
|
||||
appendConfig('targetUN');
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
// StudyList-related functions
|
||||
import './studylist/studylistModification.js';
|
||||
|
||||
// Bidirectional tool utility functions
|
||||
import './bidirectional';
|
||||
|
||||
// Library functions
|
||||
import './pixelSpacingAutorunCheck.js';
|
||||
import './removeMeasurementIfInvalid.js';
|
||||
import './toggleLesionTrackerTools.js';
|
||||
import './clearMeasurementTimepointData.js';
|
||||
import './configureTargetToolsHandles.js';
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Session } from 'meteor/session';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
OHIF.lesiontracker.pixelSpacingAutorunCheck = () => {
|
||||
OHIF.log.info('lesionTool button change autorun');
|
||||
|
||||
// Get oncology tools
|
||||
const $oncologyTools = $('button#lesion, button#nonTarget');
|
||||
|
||||
// TODO: Set activeViewport for empty viewport element
|
||||
const activeViewportIndex = window.store.getState().viewports.activeViewport;
|
||||
if (activeViewportIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = $('.imageViewerViewport').get(activeViewportIndex);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
let enabledElement;
|
||||
try {
|
||||
enabledElement = cornerstone.getEnabledElement(element);
|
||||
} catch(error) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check value of rowPixelSpacing & columnPixelSpacing to define as unavailable
|
||||
if (!enabledElement ||
|
||||
!enabledElement.image ||
|
||||
!enabledElement.image.rowPixelSpacing ||
|
||||
!enabledElement.image.columnPixelSpacing) {
|
||||
// Disable Lesion Buttons
|
||||
$oncologyTools.prop('disabled', true);
|
||||
} else {
|
||||
// Enable Lesion Buttons
|
||||
$oncologyTools.prop('disabled', false);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { _ } from 'meteor/underscore';
|
||||
|
||||
OHIF.lesiontracker.removeMeasurementIfInvalid = (measurementData, eventData) => {
|
||||
const handles = measurementData.handles;
|
||||
const start = _.pick(handles.start, ['x', 'y']);
|
||||
const end = _.pick(handles.end, ['x', 'y']);
|
||||
const element = eventData.element;
|
||||
const toolType = measurementData.toolType;
|
||||
if (_.isEqual(start, end)) {
|
||||
cornerstoneTools.removeToolState(element, toolType, measurementData);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Router } from 'meteor/clinical:router';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Meteor.startup(function() {
|
||||
if (!OHIF.studylist) return;
|
||||
|
||||
OHIF.studylist.callbacks.dblClickOnStudy = dblClickOnStudy;
|
||||
OHIF.studylist.callbacks.middleClickOnStudy = dblClickOnStudy;
|
||||
|
||||
OHIF.studylist.timepointApi = new OHIF.measurements.TimepointApi();
|
||||
});
|
||||
|
||||
/**
|
||||
* Lesion Tracker method including Timepoints / other studies
|
||||
*/
|
||||
const dblClickOnStudy = data => {
|
||||
// Find the relevant timepoint given the clicked-on study
|
||||
const timepointApi = OHIF.studylist.timepointApi;
|
||||
if (!timepointApi) {
|
||||
OHIF.log.warn('No timepoint api on dbl-clicked study?');
|
||||
return;
|
||||
}
|
||||
|
||||
const timepoint = timepointApi.study(data.studyInstanceUid)[0];
|
||||
if (timepoint) {
|
||||
Router.go('viewerTimepoint', { timepointId: timepoint.timepointId });
|
||||
} else {
|
||||
Router.go('viewerStudies', { studyInstanceUids: data.studyInstanceUid });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { Viewerbase } from 'meteor/ohif:viewerbase';
|
||||
|
||||
/**
|
||||
* Show / hide lesion tracker tools
|
||||
*/
|
||||
|
||||
let previousStates;
|
||||
let previousActiveTool;
|
||||
|
||||
let toolsShown = true;
|
||||
|
||||
OHIF.lesiontracker.toggleLesionTrackerTools = () => {
|
||||
const toolManager = Viewerbase.toolManager;
|
||||
|
||||
if (toolsShown === true) {
|
||||
// Save the current settings for later
|
||||
previousStates = toolManager.getToolDefaultStates();
|
||||
previousActiveTool = toolManager.getActiveTool();
|
||||
|
||||
// Hide the tools (set them all to disabled)
|
||||
const toolDefaultStates = {
|
||||
activate: ['deleteLesionKeyboardTool'],
|
||||
deactivate: [],
|
||||
enable: [],
|
||||
disable: ['bidirectional', 'nonTarget', 'length', 'targetCR', 'targetUN']
|
||||
};
|
||||
|
||||
toolManager.setToolDefaultStates(toolDefaultStates);
|
||||
|
||||
// Using setActiveTool with no arguments activates the
|
||||
// default tool on all available viewports
|
||||
toolManager.setActiveTool();
|
||||
|
||||
toolsShown = false;
|
||||
} else {
|
||||
// Show the tools (reload previous states)
|
||||
toolManager.setToolDefaultStates(previousStates);
|
||||
|
||||
// Using setActiveTool with no elements specified activates
|
||||
// the specified tool on all available viewports
|
||||
toolManager.setActiveTool(previousActiveTool);
|
||||
|
||||
toolsShown = true;
|
||||
}
|
||||
};
|
||||
|
||||
OHIF.lesiontracker.toggleLesionTrackerToolsButtons = (isEnabled) => {
|
||||
const toolManager = Viewerbase.toolManager;
|
||||
const toolStates = previousStates || toolManager.getToolDefaultStates();
|
||||
|
||||
if (isEnabled) {
|
||||
toolStates.disabledToolButtons = [];
|
||||
OHIF.lesiontracker.toggleLesionTrackerToolsHotKeys(true);
|
||||
} else {
|
||||
toolStates.disabledToolButtons = ['bidirectional', 'nonTarget', 'targetCR', 'targetUN',
|
||||
'toggleHUD', 'toggleTrial', 'toolbarSectionEntry', 'toggleMeasurements'];
|
||||
OHIF.lesiontracker.toggleLesionTrackerToolsHotKeys(false);
|
||||
}
|
||||
|
||||
// Reload the updated previous or default states
|
||||
toolManager.setToolDefaultStates(toolStates);
|
||||
|
||||
// Reset the active tool if disabled
|
||||
if (!isEnabled) {
|
||||
toolManager.setActiveTool();
|
||||
}
|
||||
};
|
||||
|
||||
OHIF.lesiontracker.toggleLesionTrackerToolsHotKeys = (isEnabled) => {
|
||||
// The hotkey can also be an array (e.g. ["NUMPAD0", "0"])
|
||||
OHIF.viewer.defaultHotkeys = OHIF.viewer.defaultHotkeys || {};
|
||||
|
||||
if (isEnabled) {
|
||||
OHIF.viewer.defaultHotkeys.toggleLesionTrackerTools = 'O';
|
||||
OHIF.viewer.defaultHotkeys.bidirectional = 'T'; // Target
|
||||
OHIF.viewer.defaultHotkeys.nonTarget = 'N'; // Non-target
|
||||
} else {
|
||||
OHIF.viewer.defaultHotkeys.toggleLesionTrackerTools = null;
|
||||
OHIF.viewer.defaultHotkeys.bidirectional = null; // Target
|
||||
OHIF.viewer.defaultHotkeys.nonTarget = null; // Non-target
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Viewerbase } from 'meteor/ohif:viewerbase';
|
||||
|
||||
Meteor.startup(function() {
|
||||
const toolManager = Viewerbase.toolManager;
|
||||
|
||||
toolManager.addTool('bidirectional', {
|
||||
mouse: cornerstoneTools.bidirectional,
|
||||
touch: cornerstoneTools.bidirectionalTouch
|
||||
});
|
||||
|
||||
toolManager.addTool('nonTarget', {
|
||||
mouse: cornerstoneTools.nonTarget,
|
||||
touch: cornerstoneTools.nonTargetTouch
|
||||
});
|
||||
|
||||
toolManager.addTool('deleteLesionKeyboardTool', {
|
||||
mouse: cornerstoneTools.deleteLesionKeyboardTool,
|
||||
touch: cornerstoneTools.deleteLesionKeyboardTool
|
||||
});
|
||||
|
||||
toolManager.addTool('targetCR', {
|
||||
mouse: cornerstoneTools.targetCR,
|
||||
touch: cornerstoneTools.targetCRTouch
|
||||
});
|
||||
|
||||
toolManager.addTool('targetUN', {
|
||||
mouse: cornerstoneTools.targetUN,
|
||||
touch: cornerstoneTools.targetUNTouch
|
||||
});
|
||||
|
||||
// Update default state for tools making sure each tool is only inserted once
|
||||
let currentDefaultStates = toolManager.getToolDefaultStates();
|
||||
let newDefaultStates = {
|
||||
enable: [],
|
||||
deactivate: ['bidirectional', 'nonTarget', 'length', 'targetCR', 'targetUN'],
|
||||
activate: ['deleteLesionKeyboardTool']
|
||||
};
|
||||
|
||||
Object.keys(newDefaultStates).forEach(state => {
|
||||
newDefaultStates[state].forEach(tool => {
|
||||
let tools = currentDefaultStates[state];
|
||||
// make sure each tool is only inserted once
|
||||
if (tools && tools.indexOf(tool) < 0) {
|
||||
tools.push(tool);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
toolManager.setToolDefaultStates(currentDefaultStates);
|
||||
});
|
||||
Reference in new issue
Block a user