diff --git a/src/__mocks__/externalModules.js b/src/__mocks__/externalModules.js index 0c2b17888..d057ef1d9 100644 --- a/src/__mocks__/externalModules.js +++ b/src/__mocks__/externalModules.js @@ -1,3 +1,5 @@ +import cornerstoneMath from 'cornerstone-math'; + export default { cornerstone: { getEnabledElement: jest.fn().mockImplementation(() => ({ @@ -19,6 +21,7 @@ export default { updateImage: jest.fn(), }, cornerstoneMath: { + ...cornerstoneMath, point: { distance: jest.fn(), }, diff --git a/src/tools/annotation/CobbAngleTool.js b/src/tools/annotation/CobbAngleTool.js index 7c16bffee..e23c01bdc 100644 --- a/src/tools/annotation/CobbAngleTool.js +++ b/src/tools/annotation/CobbAngleTool.js @@ -124,12 +124,14 @@ export default class CobbAngleTool extends BaseAnnotationTool { return false; } - return ( + const seg1Near = lineSegDistance(element, data.handles.start, data.handles.end, coords) < - 25 || + 25; + const seg2Near = lineSegDistance(element, data.handles.start2, data.handles.end2, coords) < - 25 - ); + 25; + + return seg1Near || seg2Near; } updateCachedStats(image, element, data) { @@ -182,6 +184,9 @@ export default class CobbAngleTool extends BaseAnnotationTool { const lineWidth = toolStyle.getToolWidth(); const lineDash = getModule('globalConfiguration').configuration.lineDash; const font = textStyle.getFont(); + const { element } = evt.detail; + const image = external.cornerstone.getEnabledElement(element).image; + const { rowPixelSpacing, colPixelSpacing } = getPixelSpacing(image); for (let i = 0; i < toolData.data.length; i++) { const data = toolData.data[i]; @@ -190,6 +195,10 @@ export default class CobbAngleTool extends BaseAnnotationTool { continue; } + if (!data.value) { + data.value = this.textBoxText(data, rowPixelSpacing, colPixelSpacing); + } + draw(context, context => { setShadow(context, this.configuration); @@ -291,6 +300,15 @@ export default class CobbAngleTool extends BaseAnnotationTool { return; } + const eventType = EVENTS.MEASUREMENT_COMPLETED; + const eventData = { + toolName: this.name, + toolType: this.name, // Deprecation notice: toolType will be replaced by toolName + element, + measurementData, + }; + + triggerEvent(element, eventType, eventData); }; // Search for incomplete measurements @@ -373,22 +391,20 @@ export default class CobbAngleTool extends BaseAnnotationTool { } } - const { rAngle } = data; - - data.value = ''; + data.value = this.textBoxText(data, rowPixelSpacing, colPixelSpacing); + } - if (!Number.isNaN(rAngle)) { - data.value = textBoxText(rAngle, rowPixelSpacing, colPixelSpacing); + textBoxText({ rAngle }, rowPixelSpacing, colPixelSpacing) { + if (rAngle === undefined) { + return ''; + } + if (Number.isNaN(rAngle)) { + return ''; } - function textBoxText(rAngle, rowPixelSpacing, colPixelSpacing) { - const suffix = !rowPixelSpacing || !colPixelSpacing ? ' (isotropic)' : ''; - const str = '00B0'; // Degrees symbol + const suffix = !rowPixelSpacing || !colPixelSpacing ? ' (isotropic)' : ''; - return ( - rAngle.toString() + String.fromCharCode(parseInt(str, 16)) + suffix - ); - } + return `${rAngle}\u00B0${suffix}`; } activeCallback(element) { diff --git a/src/tools/annotation/CobbAngleTool.test.js b/src/tools/annotation/CobbAngleTool.test.js new file mode 100644 index 000000000..7daa711f8 --- /dev/null +++ b/src/tools/annotation/CobbAngleTool.test.js @@ -0,0 +1,237 @@ +import Tool from './CobbAngleTool.js'; +import { getToolState } from '../../stateManagement/toolState.js'; + +jest.mock('./../../stateManagement/toolState.js', () => ({ + getToolState: jest.fn(), +})); + +jest.mock('./../../externalModules.js'); +import external from '../../externalModules.js'; + +const goodMouseEventData = { + currentPoints: { + image: { + x: 0, + y: 0, + }, + }, +}; + +const image = { + rowPixelSpacing: 0.8984375, + columnPixelSpacing: 0.8984375, +}; + +describe('CobbAngleTool.js', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('default values', () => { + it('has a default name of "CobbAngle"', () => { + const defaultName = 'CobbAngle'; + const instantiatedTool = new Tool(); + + expect(instantiatedTool.name).toEqual(defaultName); + }); + + it('can be created with a custom tool name', () => { + const customToolName = { name: 'customToolName' }; + const instantiatedTool = new Tool(customToolName); + + expect(instantiatedTool.name).toEqual(customToolName.name); + }); + }); + + describe('createNewMeasurement', () => { + it('returns aa new measurement object', () => { + const instantiatedTool = new Tool('CobbAngle'); + + const toolMeasurement = instantiatedTool.createNewMeasurement( + goodMouseEventData + ); + + expect(typeof toolMeasurement).toBe(typeof {}); + }); + + it("returns a measurement with a start, end, start2 and end2 handles at the eventData's x and y", () => { + const instantiatedTool = new Tool('toolName'); + + const toolMeasurement = instantiatedTool.createNewMeasurement( + goodMouseEventData + ); + const startHandle = { + x: toolMeasurement.handles.start.x, + y: toolMeasurement.handles.start.y, + }; + const endHandle = { + x: toolMeasurement.handles.end.x, + y: toolMeasurement.handles.end.y, + }; + const start2Handle = { + x: toolMeasurement.handles.start2.x, + y: toolMeasurement.handles.start2.y, + }; + const end2Handle = { + x: toolMeasurement.handles.end2.x, + y: toolMeasurement.handles.end2.y, + }; + + expect(startHandle.x).toBe(goodMouseEventData.currentPoints.image.x); + expect(startHandle.y).toBe(goodMouseEventData.currentPoints.image.y); + expect(start2Handle.x).toBe(goodMouseEventData.currentPoints.image.x); + expect(start2Handle.y).toBe(goodMouseEventData.currentPoints.image.y); + expect(endHandle.x).toBe(goodMouseEventData.currentPoints.image.x); + expect(endHandle.y).toBe(goodMouseEventData.currentPoints.image.y); + expect(end2Handle.x).toBe(goodMouseEventData.currentPoints.image.x + 1); + expect(end2Handle.y).toBe(goodMouseEventData.currentPoints.image.y); + }); + + it('returns a measurement with a textBox handle', () => { + const instantiatedTool = new Tool('toolName'); + + const toolMeasurement = instantiatedTool.createNewMeasurement( + goodMouseEventData + ); + + expect(typeof toolMeasurement.handles.textBox).toBe(typeof {}); + }); + }); + + describe('pointNearTool', () => { + let element, coords; + + beforeEach(() => { + element = jest.fn(); + coords = jest.fn(); + }); + + it('returns false when measurement data is not visible', () => { + const instantiatedTool = new Tool('AngleTool'); + const notVisibleMeasurementData = { + visible: false, + }; + + const isPointNearTool = instantiatedTool.pointNearTool( + element, + notVisibleMeasurementData, + coords + ); + + expect(isPointNearTool).toBe(false); + }); + + it('returns false when measurement data is incomplete', () => { + const instantiatedTool = new Tool('AngleTool'); + instantiatedTool.hasIncomplete = true; + const measurementData = { + visible: true, + handles: { + start: { x: 10, y: 10 }, + end: { x: 20, y: 10 }, + }, + }; + const isPointNearTool = instantiatedTool.pointNearTool( + element, + measurementData, + measurementData.handles.start + ); + + expect(isPointNearTool).toBe(false); + }); + + it('returns true when measurement data is near the end points', () => { + const instantiatedTool = new Tool('AngleTool'); + const measurementData = { + visible: true, + handles: { + start: { x: 10, y: 10 }, + end: { x: 20, y: 10 }, + start2: { x: 40, y: 10 }, + end2: { x: 40, y: 40 }, + }, + }; + + external.cornerstone.pixelToCanvas.mockImplementation((el, pt) => { + return pt; + }); + + expect( + instantiatedTool.pointNearTool(element, measurementData, { + x: 70, + y: 10, + }) + ).toBe(false); + + expect( + instantiatedTool.pointNearTool( + element, + measurementData, + measurementData.handles.start + ) + ).toBe(true); + + expect( + instantiatedTool.pointNearTool( + element, + measurementData, + measurementData.handles.end2 + ) + ).toBe(true); + }); + }); + + describe('updateCachedStats', () => { + let element; + + beforeEach(() => { + element = jest.fn(); + }); + + it('should calculate and update annotation value', () => { + const instantiatedTool = new Tool('AngleTool'); + + const data = { + handles: { + start: { + x: 166, + y: 90, + }, + end: { + x: 120, + y: 113, + }, + start2: { + x: 120, + y: 113, + }, + end2: { + x: 145, + y: 143, + }, + }, + }; + + instantiatedTool.updateCachedStats(image, element, data); + expect(data.rAngle).toBe(76.76); + expect(data.invalidated).toBe(false); + }); + }); + + describe('renderToolData', () => { + it('returns undefined when no toolData exists for the tool', () => { + const instantiatedTool = new Tool('AngleTool'); + const mockEvent = { + detail: { + enabledElement: undefined, + }, + }; + + getToolState.mockReturnValueOnce(undefined); + + const renderResult = instantiatedTool.renderToolData(mockEvent); + + expect(renderResult).toBe(undefined); + }); + }); +}); diff --git a/src/tools/annotation/ProbeTool.js b/src/tools/annotation/ProbeTool.js index 95c327845..1cf365936 100644 --- a/src/tools/annotation/ProbeTool.js +++ b/src/tools/annotation/ProbeTool.js @@ -158,7 +158,7 @@ export default class ProbeTool extends BaseAnnotationTool { if (this.configuration.drawHandles) { // Draw the handles - let handleOptions = { handleRadius, color }; + const handleOptions = { handleRadius, color }; if (renderDashed) { handleOptions.lineDash = lineDash; diff --git a/src/tools/annotation/RectangleRoiTool.js b/src/tools/annotation/RectangleRoiTool.js index 822290433..173692f38 100644 --- a/src/tools/annotation/RectangleRoiTool.js +++ b/src/tools/annotation/RectangleRoiTool.js @@ -339,8 +339,13 @@ function _calculateStats(image, element, handles, modality, pixelSpacing) { (pixelSpacing.colPixelSpacing || 1) * (roiCoordinates.height * (pixelSpacing.rowPixelSpacing || 1)); + const perimeter = + roiCoordinates.width * 2 * (pixelSpacing.colPixelSpacing || 1) + + roiCoordinates.height * 2 * (pixelSpacing.rowPixelSpacing || 1); + return { area: area || 0, + perimeter, count: roiMeanStdDev.count || 0, mean: roiMeanStdDev.mean || 0, variance: roiMeanStdDev.variance || 0, diff --git a/src/tools/annotation/RectangleRoiTool.test.js b/src/tools/annotation/RectangleRoiTool.test.js index 185b96193..03c43e3a4 100644 --- a/src/tools/annotation/RectangleRoiTool.test.js +++ b/src/tools/annotation/RectangleRoiTool.test.js @@ -201,6 +201,7 @@ describe('RectangleRoiTool.js', () => { instantiatedTool.updateCachedStats(image, element, data); expect(data.cachedStats.area.toFixed(2)).toEqual('7.26'); + expect(data.cachedStats.perimeter.toFixed(2)).toEqual('10.78'); expect(data.cachedStats.mean.toFixed(2)).toEqual('57.56'); expect(data.cachedStats.stdDev.toFixed(2)).toEqual('47.46'); diff --git a/src/util/convertToVector3.test.js b/src/util/convertToVector3.test.js index 37bbfe7b0..cefc2b215 100644 --- a/src/util/convertToVector3.test.js +++ b/src/util/convertToVector3.test.js @@ -3,6 +3,7 @@ import external from '../externalModules.js'; jest.mock('../externalModules.js', () => { const cornerstoneMath = require('cornerstone-math'); + return { cornerstoneMath: { Vector3: cornerstoneMath.Vector3, diff --git a/src/util/findAndMoveHelpers.js b/src/util/findAndMoveHelpers.js index 3ffadff33..c3a4fda43 100644 --- a/src/util/findAndMoveHelpers.js +++ b/src/util/findAndMoveHelpers.js @@ -1,6 +1,8 @@ import { state } from '../store/index.js'; import getHandleNearImagePoint from '../manipulators/getHandleNearImagePoint.js'; import { moveHandle, moveAllHandles } from './../manipulators/index.js'; +import EVENTS from '../events'; +import triggerEvent from './triggerEvent'; // TODO this should just be in manipulators? They are just manipulator wrappers anyway. @@ -26,6 +28,18 @@ const moveHandleNearImagePoint = function( ) { toolData.active = true; state.isToolLocked = true; + const doneHandler = success => { + const { element } = evt.detail; + const toolName = toolData.toolType || toolData.toolName; + const modifiedEventData = { + toolName, + toolType: toolName, // Deprecation notice: toolType will be replaced by toolName + element, + measurementData: { ...toolData, active: false }, + }; + + triggerEvent(element, EVENTS.MEASUREMENT_COMPLETED, modifiedEventData); + }; moveHandle( evt.detail, @@ -33,7 +47,8 @@ const moveHandleNearImagePoint = function( toolData, handle, tool.options, - interactionType + interactionType, + doneHandler ); evt.stopImmediatePropagation();