Skip to content

Commit 8f13fa9

Browse files
committed
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.
1 parent fb6019f commit 8f13fa9

File tree

7 files changed

+239
-18
lines changed

7 files changed

+239
-18
lines changed

src/tools/annotation/CobbAngleTool.js

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,14 @@ export default class CobbAngleTool extends BaseAnnotationTool {
124124
return false;
125125
}
126126

127-
return (
127+
const seg1Near =
128128
lineSegDistance(element, data.handles.start, data.handles.end, coords) <
129-
25 ||
129+
25;
130+
const seg2Near =
130131
lineSegDistance(element, data.handles.start2, data.handles.end2, coords) <
131-
25
132-
);
132+
25;
133+
134+
return seg1Near || seg2Near;
133135
}
134136

135137
updateCachedStats(image, element, data) {
@@ -182,6 +184,9 @@ export default class CobbAngleTool extends BaseAnnotationTool {
182184
const lineWidth = toolStyle.getToolWidth();
183185
const lineDash = getModule('globalConfiguration').configuration.lineDash;
184186
const font = textStyle.getFont();
187+
const { element } = evt.detail;
188+
const image = external.cornerstone.getEnabledElement(element).image;
189+
const { rowPixelSpacing, colPixelSpacing } = getPixelSpacing(image);
185190

186191
for (let i = 0; i < toolData.data.length; i++) {
187192
const data = toolData.data[i];
@@ -190,6 +195,10 @@ export default class CobbAngleTool extends BaseAnnotationTool {
190195
continue;
191196
}
192197

198+
if (!data.value) {
199+
data.value = this.textBoxText(data, rowPixelSpacing, colPixelSpacing);
200+
}
201+
193202
draw(context, context => {
194203
setShadow(context, this.configuration);
195204

@@ -291,6 +300,15 @@ export default class CobbAngleTool extends BaseAnnotationTool {
291300

292301
return;
293302
}
303+
const eventType = EVENTS.MEASUREMENT_COMPLETED;
304+
const eventData = {
305+
toolName: this.name,
306+
toolType: this.name, // Deprecation notice: toolType will be replaced by toolName
307+
element,
308+
measurementData,
309+
};
310+
311+
triggerEvent(element, eventType, eventData);
294312
};
295313

296314
// Search for incomplete measurements
@@ -373,22 +391,20 @@ export default class CobbAngleTool extends BaseAnnotationTool {
373391
}
374392
}
375393

376-
const { rAngle } = data;
377-
378-
data.value = '';
394+
data.value = this.textBoxText(data, rowPixelSpacing, colPixelSpacing);
395+
}
379396

380-
if (!Number.isNaN(rAngle)) {
381-
data.value = textBoxText(rAngle, rowPixelSpacing, colPixelSpacing);
397+
textBoxText({ rAngle }, rowPixelSpacing, colPixelSpacing) {
398+
if (rAngle === undefined) {
399+
return '';
400+
}
401+
if (Number.isNaN(rAngle)) {
402+
return '';
382403
}
383404

384-
function textBoxText(rAngle, rowPixelSpacing, colPixelSpacing) {
385-
const suffix = !rowPixelSpacing || !colPixelSpacing ? ' (isotropic)' : '';
386-
const str = '00B0'; // Degrees symbol
405+
const suffix = !rowPixelSpacing || !colPixelSpacing ? ' (isotropic)' : '';
387406

388-
return (
389-
rAngle.toString() + String.fromCharCode(parseInt(str, 16)) + suffix
390-
);
391-
}
407+
return `${rAngle}\u00B0${suffix}`;
392408
}
393409

394410
activeCallback(element) {
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import Tool from './CobbAngleTool.js';
2+
import { getToolState } from '../../stateManagement/toolState.js';
3+
4+
jest.mock('./../../stateManagement/toolState.js', () => ({
5+
getToolState: jest.fn(),
6+
}));
7+
8+
jest.mock('./../../importInternal.js', () => ({
9+
default: jest.fn(),
10+
}));
11+
12+
jest.mock('./../../externalModules.js', () => ({
13+
cornerstone: {
14+
metaData: {
15+
get: jest.fn(),
16+
},
17+
},
18+
}));
19+
20+
const goodMouseEventData = {
21+
currentPoints: {
22+
image: {
23+
x: 0,
24+
y: 0,
25+
},
26+
},
27+
};
28+
29+
const image = {
30+
rowPixelSpacing: 0.8984375,
31+
columnPixelSpacing: 0.8984375,
32+
};
33+
34+
describe('CobbAngleTool.js', () => {
35+
describe('default values', () => {
36+
it('has a default name of "CobbAngle"', () => {
37+
const defaultName = 'CobbAngle';
38+
const instantiatedTool = new Tool();
39+
40+
expect(instantiatedTool.name).toEqual(defaultName);
41+
});
42+
43+
it('can be created with a custom tool name', () => {
44+
const customToolName = { name: 'customToolName' };
45+
const instantiatedTool = new Tool(customToolName);
46+
47+
expect(instantiatedTool.name).toEqual(customToolName.name);
48+
});
49+
});
50+
51+
describe('createNewMeasurement', () => {
52+
it('returns aa new measurement object', () => {
53+
const instantiatedTool = new Tool('CobbAngle');
54+
55+
const toolMeasurement = instantiatedTool.createNewMeasurement(
56+
goodMouseEventData
57+
);
58+
59+
expect(typeof toolMeasurement).toBe(typeof {});
60+
});
61+
62+
it("returns a measurement with a start, end, start2 and end2 handles at the eventData's x and y", () => {
63+
const instantiatedTool = new Tool('toolName');
64+
65+
const toolMeasurement = instantiatedTool.createNewMeasurement(
66+
goodMouseEventData
67+
);
68+
const startHandle = {
69+
x: toolMeasurement.handles.start.x,
70+
y: toolMeasurement.handles.start.y,
71+
};
72+
const endHandle = {
73+
x: toolMeasurement.handles.end.x,
74+
y: toolMeasurement.handles.end.y,
75+
};
76+
const start2Handle = {
77+
x: toolMeasurement.handles.start2.x,
78+
y: toolMeasurement.handles.start2.y,
79+
};
80+
const end2Handle = {
81+
x: toolMeasurement.handles.end2.x,
82+
y: toolMeasurement.handles.end2.y,
83+
};
84+
85+
expect(startHandle.x).toBe(goodMouseEventData.currentPoints.image.x);
86+
expect(startHandle.y).toBe(goodMouseEventData.currentPoints.image.y);
87+
expect(start2Handle.x).toBe(goodMouseEventData.currentPoints.image.x);
88+
expect(start2Handle.y).toBe(goodMouseEventData.currentPoints.image.y);
89+
expect(endHandle.x).toBe(goodMouseEventData.currentPoints.image.x);
90+
expect(endHandle.y).toBe(goodMouseEventData.currentPoints.image.y);
91+
expect(end2Handle.x).toBe(goodMouseEventData.currentPoints.image.x + 1);
92+
expect(end2Handle.y).toBe(goodMouseEventData.currentPoints.image.y);
93+
});
94+
95+
it('returns a measurement with a textBox handle', () => {
96+
const instantiatedTool = new Tool('toolName');
97+
98+
const toolMeasurement = instantiatedTool.createNewMeasurement(
99+
goodMouseEventData
100+
);
101+
102+
expect(typeof toolMeasurement.handles.textBox).toBe(typeof {});
103+
});
104+
});
105+
106+
describe('pointNearTool', () => {
107+
let element, coords;
108+
109+
beforeEach(() => {
110+
element = jest.fn();
111+
coords = jest.fn();
112+
});
113+
114+
it('returns false when measurement data is not visible', () => {
115+
const instantiatedTool = new Tool('AngleTool');
116+
const notVisibleMeasurementData = {
117+
visible: false,
118+
};
119+
120+
const isPointNearTool = instantiatedTool.pointNearTool(
121+
element,
122+
notVisibleMeasurementData,
123+
coords
124+
);
125+
126+
expect(isPointNearTool).toBe(false);
127+
});
128+
});
129+
130+
describe('updateCachedStats', () => {
131+
let element;
132+
133+
beforeEach(() => {
134+
element = jest.fn();
135+
});
136+
137+
it('should calculate and update annotation value', () => {
138+
const instantiatedTool = new Tool('AngleTool');
139+
140+
const data = {
141+
handles: {
142+
start: {
143+
x: 166,
144+
y: 90,
145+
},
146+
end: {
147+
x: 120,
148+
y: 113,
149+
},
150+
start2: {
151+
x: 120,
152+
y: 113,
153+
},
154+
end2: {
155+
x: 145,
156+
y: 143,
157+
},
158+
},
159+
};
160+
161+
instantiatedTool.updateCachedStats(image, element, data);
162+
expect(data.rAngle).toBe(76.76);
163+
expect(data.invalidated).toBe(false);
164+
});
165+
});
166+
167+
describe('renderToolData', () => {
168+
it('returns undefined when no toolData exists for the tool', () => {
169+
const instantiatedTool = new Tool('AngleTool');
170+
const mockEvent = {
171+
detail: {
172+
enabledElement: undefined,
173+
},
174+
};
175+
176+
getToolState.mockReturnValueOnce(undefined);
177+
178+
const renderResult = instantiatedTool.renderToolData(mockEvent);
179+
180+
expect(renderResult).toBe(undefined);
181+
});
182+
});
183+
});

src/tools/annotation/ProbeTool.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export default class ProbeTool extends BaseAnnotationTool {
158158

159159
if (this.configuration.drawHandles) {
160160
// Draw the handles
161-
let handleOptions = { handleRadius, color };
161+
const handleOptions = { handleRadius, color };
162162

163163
if (renderDashed) {
164164
handleOptions.lineDash = lineDash;

src/tools/annotation/RectangleRoiTool.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,13 @@ function _calculateStats(image, element, handles, modality, pixelSpacing) {
339339
(pixelSpacing.colPixelSpacing || 1) *
340340
(roiCoordinates.height * (pixelSpacing.rowPixelSpacing || 1));
341341

342+
const perimeter =
343+
roiCoordinates.width * 2 * (pixelSpacing.colPixelSpacing || 1) +
344+
roiCoordinates.height * 2 * (pixelSpacing.rowPixelSpacing || 1);
345+
342346
return {
343347
area: area || 0,
348+
perimeter,
344349
count: roiMeanStdDev.count || 0,
345350
mean: roiMeanStdDev.mean || 0,
346351
variance: roiMeanStdDev.variance || 0,

src/tools/annotation/RectangleRoiTool.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ describe('RectangleRoiTool.js', () => {
201201

202202
instantiatedTool.updateCachedStats(image, element, data);
203203
expect(data.cachedStats.area.toFixed(2)).toEqual('7.26');
204+
expect(data.cachedStats.perimeter.toFixed(2)).toEqual('10.78');
204205
expect(data.cachedStats.mean.toFixed(2)).toEqual('57.56');
205206
expect(data.cachedStats.stdDev.toFixed(2)).toEqual('47.46');
206207

src/util/convertToVector3.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import external from '../externalModules.js';
33

44
jest.mock('../externalModules.js', () => {
55
const cornerstoneMath = require('cornerstone-math');
6+
67
return {
78
cornerstoneMath: {
89
Vector3: cornerstoneMath.Vector3,

src/util/findAndMoveHelpers.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { state } from '../store/index.js';
22
import getHandleNearImagePoint from '../manipulators/getHandleNearImagePoint.js';
33
import { moveHandle, moveAllHandles } from './../manipulators/index.js';
4+
import EVENTS from '../events';
5+
import triggerEvent from './triggerEvent';
46

57
// TODO this should just be in manipulators? They are just manipulator wrappers anyway.
68

@@ -26,14 +28,27 @@ const moveHandleNearImagePoint = function(
2628
) {
2729
toolData.active = true;
2830
state.isToolLocked = true;
31+
const doneHandler = success => {
32+
const { element } = evt.detail;
33+
const toolName = toolData.toolType || toolData.toolName;
34+
const modifiedEventData = {
35+
toolName,
36+
toolType: toolName, // Deprecation notice: toolType will be replaced by toolName
37+
element,
38+
measurementData: { ...toolData, active: false },
39+
};
40+
41+
triggerEvent(element, EVENTS.MEASUREMENT_COMPLETED, modifiedEventData);
42+
};
2943

3044
moveHandle(
3145
evt.detail,
3246
tool.name,
3347
toolData,
3448
handle,
3549
tool.options,
36-
interactionType
50+
interactionType,
51+
doneHandler
3752
);
3853

3954
evt.stopImmediatePropagation();

0 commit comments

Comments
 (0)