From 8f13fa9a4b2a4d03b343d37d2d6de99a13f7cc7d Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Fri, 22 Oct 2021 16:09:03 -0400 Subject: [PATCH 1/2] fix(CobbAngleTool, RectangleRoi): Fix the interaction of the tools with the TID.300 saving changes. Correct notification on tool completion, with values restored correctly when loaded from TID.300 is required for store/save functionality. This is implemented in dcmjs, but the changes are required here. The cobb angle needed to be udpated after restoring, the Rectangle ROI needed the perimeter, and changes needed to publish a completed measurement event. --- src/tools/annotation/CobbAngleTool.js | 48 +++-- src/tools/annotation/CobbAngleTool.test.js | 183 ++++++++++++++++++ src/tools/annotation/ProbeTool.js | 2 +- src/tools/annotation/RectangleRoiTool.js | 5 + src/tools/annotation/RectangleRoiTool.test.js | 1 + src/util/convertToVector3.test.js | 1 + src/util/findAndMoveHelpers.js | 17 +- 7 files changed, 239 insertions(+), 18 deletions(-) create mode 100644 src/tools/annotation/CobbAngleTool.test.js 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..904d93eca --- /dev/null +++ b/src/tools/annotation/CobbAngleTool.test.js @@ -0,0 +1,183 @@ +import Tool from './CobbAngleTool.js'; +import { getToolState } from '../../stateManagement/toolState.js'; + +jest.mock('./../../stateManagement/toolState.js', () => ({ + getToolState: jest.fn(), +})); + +jest.mock('./../../importInternal.js', () => ({ + default: jest.fn(), +})); + +jest.mock('./../../externalModules.js', () => ({ + cornerstone: { + metaData: { + get: jest.fn(), + }, + }, +})); + +const goodMouseEventData = { + currentPoints: { + image: { + x: 0, + y: 0, + }, + }, +}; + +const image = { + rowPixelSpacing: 0.8984375, + columnPixelSpacing: 0.8984375, +}; + +describe('CobbAngleTool.js', () => { + 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); + }); + }); + + 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(); From 79890230c2662a7f17faa47f2cdc80fde408da5b Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Mon, 25 Oct 2021 09:31:59 -0400 Subject: [PATCH 2/2] test(CobbAngleTool.test): Add missing tests on pointNearTool The existing test only checked negative tests, adding tests for point is near tool. --- src/__mocks__/externalModules.js | 3 + src/tools/annotation/CobbAngleTool.test.js | 76 ++++++++++++++++++---- 2 files changed, 68 insertions(+), 11 deletions(-) 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.test.js b/src/tools/annotation/CobbAngleTool.test.js index 904d93eca..7daa711f8 100644 --- a/src/tools/annotation/CobbAngleTool.test.js +++ b/src/tools/annotation/CobbAngleTool.test.js @@ -5,17 +5,8 @@ jest.mock('./../../stateManagement/toolState.js', () => ({ getToolState: jest.fn(), })); -jest.mock('./../../importInternal.js', () => ({ - default: jest.fn(), -})); - -jest.mock('./../../externalModules.js', () => ({ - cornerstone: { - metaData: { - get: jest.fn(), - }, - }, -})); +jest.mock('./../../externalModules.js'); +import external from '../../externalModules.js'; const goodMouseEventData = { currentPoints: { @@ -32,6 +23,10 @@ const image = { }; describe('CobbAngleTool.js', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + describe('default values', () => { it('has a default name of "CobbAngle"', () => { const defaultName = 'CobbAngle'; @@ -125,6 +120,65 @@ describe('CobbAngleTool.js', () => { 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', () => {