@cornerstonejs/adapters 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +14 -5
  2. package/src/adapters/Cornerstone/Angle.js +92 -0
  3. package/src/adapters/Cornerstone/ArrowAnnotate.js +106 -0
  4. package/src/adapters/Cornerstone/Bidirectional.js +187 -0
  5. package/src/adapters/Cornerstone/CircleRoi.js +109 -0
  6. package/src/adapters/Cornerstone/CobbAngle.js +96 -0
  7. package/src/adapters/Cornerstone/EllipticalRoi.js +149 -0
  8. package/src/adapters/Cornerstone/FreehandRoi.js +82 -0
  9. package/src/adapters/Cornerstone/Length.js +80 -0
  10. package/src/adapters/Cornerstone/MeasurementReport.js +352 -0
  11. package/src/adapters/Cornerstone/RectangleRoi.js +92 -0
  12. package/src/adapters/Cornerstone/Segmentation.js +118 -0
  13. package/src/adapters/Cornerstone/Segmentation_3X.js +632 -0
  14. package/src/adapters/Cornerstone/Segmentation_4X.js +1543 -0
  15. package/src/adapters/Cornerstone/cornerstone4Tag.js +1 -0
  16. package/src/adapters/Cornerstone/index.js +27 -0
  17. package/src/adapters/Cornerstone3D/ArrowAnnotate.js +155 -0
  18. package/src/adapters/Cornerstone3D/Bidirectional.js +196 -0
  19. package/src/adapters/Cornerstone3D/CodingScheme.js +16 -0
  20. package/src/adapters/Cornerstone3D/EllipticalROI.js +204 -0
  21. package/src/adapters/Cornerstone3D/Length.js +113 -0
  22. package/src/adapters/Cornerstone3D/MeasurementReport.js +445 -0
  23. package/src/adapters/Cornerstone3D/PlanarFreehandROI.js +137 -0
  24. package/src/adapters/Cornerstone3D/Probe.js +106 -0
  25. package/src/adapters/Cornerstone3D/cornerstone3DTag.js +1 -0
  26. package/src/adapters/Cornerstone3D/index.js +24 -0
  27. package/src/adapters/VTKjs/Segmentation.js +223 -0
  28. package/src/adapters/VTKjs/index.js +7 -0
  29. package/src/adapters/helpers.js +19 -0
  30. package/src/adapters/index.js +11 -0
  31. package/src/index.js +4 -0
  32. package/.babelrc +0 -9
  33. package/.eslintrc.json +0 -18
  34. package/.prettierrc +0 -5
  35. package/CHANGELOG.md +0 -106
  36. package/generate-dictionary.js +0 -145
  37. package/netlify.toml +0 -20
  38. package/rollup.config.js +0 -37
  39. package/test/adapters.test.js +0 -1
@@ -0,0 +1,113 @@
1
+ import { utilities } from "dcmjs";
2
+ import CORNERSTONE_3D_TAG from "./cornerstone3DTag";
3
+ import MeasurementReport from "./MeasurementReport";
4
+
5
+ const { Length: TID300Length } = utilities.TID300;
6
+
7
+ const LENGTH = "Length";
8
+ const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${LENGTH}`;
9
+
10
+ class Length {
11
+ // TODO: this function is required for all Cornerstone Tool Adapters, since it is called by MeasurementReport.
12
+ static getMeasurementData(
13
+ MeasurementGroup,
14
+ sopInstanceUIDToImageIdMap,
15
+ imageToWorldCoords,
16
+ metadata
17
+ ) {
18
+ const { defaultState, NUMGroup, SCOORDGroup, ReferencedFrameNumber } =
19
+ MeasurementReport.getSetupMeasurementData(
20
+ MeasurementGroup,
21
+ sopInstanceUIDToImageIdMap,
22
+ metadata,
23
+ Length.toolType
24
+ );
25
+
26
+ const referencedImageId =
27
+ defaultState.annotation.metadata.referencedImageId;
28
+
29
+ const { GraphicData } = SCOORDGroup;
30
+ const worldCoords = [];
31
+ for (let i = 0; i < GraphicData.length; i += 2) {
32
+ const point = imageToWorldCoords(referencedImageId, [
33
+ GraphicData[i],
34
+ GraphicData[i + 1]
35
+ ]);
36
+ worldCoords.push(point);
37
+ }
38
+
39
+ const state = defaultState;
40
+
41
+ state.annotation.data = {
42
+ handles: {
43
+ points: [worldCoords[0], worldCoords[1]],
44
+ activeHandleIndex: 0,
45
+ textBox: {
46
+ hasMoved: false
47
+ }
48
+ },
49
+ cachedStats: {
50
+ [`imageId:${referencedImageId}`]: {
51
+ length: NUMGroup
52
+ ? NUMGroup.MeasuredValueSequence.NumericValue
53
+ : 0
54
+ }
55
+ },
56
+ frameNumber: ReferencedFrameNumber
57
+ };
58
+
59
+ return state;
60
+ }
61
+
62
+ static getTID300RepresentationArguments(tool, worldToImageCoords) {
63
+ const { data, finding, findingSites, metadata } = tool;
64
+ const { cachedStats = {}, handles } = data;
65
+
66
+ const { referencedImageId } = metadata;
67
+
68
+ if (!referencedImageId) {
69
+ throw new Error(
70
+ "Length.getTID300RepresentationArguments: referencedImageId is not defined"
71
+ );
72
+ }
73
+
74
+ const start = worldToImageCoords(referencedImageId, handles.points[0]);
75
+ const end = worldToImageCoords(referencedImageId, handles.points[1]);
76
+
77
+ const point1 = { x: start[0], y: start[1] };
78
+ const point2 = { x: end[0], y: end[1] };
79
+
80
+ const { length: distance } =
81
+ cachedStats[`imageId:${referencedImageId}`] || {};
82
+
83
+ return {
84
+ point1,
85
+ point2,
86
+ distance,
87
+ trackingIdentifierTextValue,
88
+ finding,
89
+ findingSites: findingSites || []
90
+ };
91
+ }
92
+ }
93
+
94
+ Length.toolType = LENGTH;
95
+ Length.utilityToolType = LENGTH;
96
+ Length.TID300Representation = TID300Length;
97
+ Length.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => {
98
+ if (!TrackingIdentifier.includes(":")) {
99
+ return false;
100
+ }
101
+
102
+ const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":");
103
+
104
+ if (cornerstone3DTag !== CORNERSTONE_3D_TAG) {
105
+ return false;
106
+ }
107
+
108
+ return toolType === LENGTH;
109
+ };
110
+
111
+ MeasurementReport.registerTool(Length);
112
+
113
+ export default Length;
@@ -0,0 +1,445 @@
1
+ import { normalizers, data, utilities, derivations } from "dcmjs";
2
+
3
+ import { toArray, codeMeaningEquals } from "../helpers";
4
+ import Cornerstone3DCodingScheme from "./CodingScheme";
5
+
6
+ const { TID1500, addAccessors } = utilities;
7
+
8
+ const { StructuredReport } = derivations;
9
+
10
+ const { Normalizer } = normalizers;
11
+
12
+ const { TID1500MeasurementReport, TID1501MeasurementGroup } = TID1500;
13
+
14
+ const { DicomMetaDictionary } = data;
15
+
16
+ const FINDING = { CodingSchemeDesignator: "DCM", CodeValue: "121071" };
17
+ const FINDING_SITE = { CodingSchemeDesignator: "SCT", CodeValue: "363698007" };
18
+ const FINDING_SITE_OLD = { CodingSchemeDesignator: "SRT", CodeValue: "G-C0E3" };
19
+
20
+ const codeValueMatch = (group, code, oldCode) => {
21
+ const { ConceptNameCodeSequence } = group;
22
+ if (!ConceptNameCodeSequence) return;
23
+ const { CodingSchemeDesignator, CodeValue } = ConceptNameCodeSequence;
24
+ return (
25
+ (CodingSchemeDesignator == code.CodingSchemeDesignator &&
26
+ CodeValue == code.CodeValue) ||
27
+ (oldCode &&
28
+ CodingSchemeDesignator == oldCode.CodingSchemeDesignator &&
29
+ CodeValue == oldCode.CodeValue)
30
+ );
31
+ };
32
+
33
+ function getTID300ContentItem(
34
+ tool,
35
+ toolType,
36
+ ReferencedSOPSequence,
37
+ toolClass,
38
+ worldToImageCoords
39
+ ) {
40
+ const args = toolClass.getTID300RepresentationArguments(
41
+ tool,
42
+ worldToImageCoords
43
+ );
44
+ args.ReferencedSOPSequence = ReferencedSOPSequence;
45
+
46
+ const TID300Measurement = new toolClass.TID300Representation(args);
47
+
48
+ return TID300Measurement;
49
+ }
50
+
51
+ function getMeasurementGroup(
52
+ toolType,
53
+ toolData,
54
+ ReferencedSOPSequence,
55
+ worldToImageCoords
56
+ ) {
57
+ const toolTypeData = toolData[toolType];
58
+ const toolClass =
59
+ MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_TOOL_TYPE[toolType];
60
+ if (
61
+ !toolTypeData ||
62
+ !toolTypeData.data ||
63
+ !toolTypeData.data.length ||
64
+ !toolClass
65
+ ) {
66
+ return;
67
+ }
68
+
69
+ // Loop through the array of tool instances
70
+ // for this tool
71
+ const Measurements = toolTypeData.data.map(tool => {
72
+ return getTID300ContentItem(
73
+ tool,
74
+ toolType,
75
+ ReferencedSOPSequence,
76
+ toolClass,
77
+ worldToImageCoords
78
+ );
79
+ });
80
+
81
+ return new TID1501MeasurementGroup(Measurements);
82
+ }
83
+
84
+ export default class MeasurementReport {
85
+ static getCornerstoneLabelFromDefaultState(defaultState) {
86
+ const { findingSites = [], finding } = defaultState;
87
+
88
+ const cornersoneFreeTextCodingValue =
89
+ Cornerstone3DCodingScheme.codeValues.CORNERSTONEFREETEXT;
90
+
91
+ let freeTextLabel = findingSites.find(
92
+ fs => fs.CodeValue === cornersoneFreeTextCodingValue
93
+ );
94
+
95
+ if (freeTextLabel) {
96
+ return freeTextLabel.CodeMeaning;
97
+ }
98
+
99
+ if (finding && finding.CodeValue === cornersoneFreeTextCodingValue) {
100
+ return finding.CodeMeaning;
101
+ }
102
+ }
103
+
104
+ static generateDatasetMeta() {
105
+ // TODO: what is the correct metaheader
106
+ // http://dicom.nema.org/medical/Dicom/current/output/chtml/part10/chapter_7.html
107
+ // TODO: move meta creation to happen in derivations.js
108
+ const fileMetaInformationVersionArray = new Uint8Array(2);
109
+ fileMetaInformationVersionArray[1] = 1;
110
+
111
+ const _meta = {
112
+ FileMetaInformationVersion: {
113
+ Value: [fileMetaInformationVersionArray.buffer],
114
+ vr: "OB"
115
+ },
116
+ //MediaStorageSOPClassUID
117
+ //MediaStorageSOPInstanceUID: sopCommonModule.sopInstanceUID,
118
+ TransferSyntaxUID: {
119
+ Value: ["1.2.840.10008.1.2.1"],
120
+ vr: "UI"
121
+ },
122
+ ImplementationClassUID: {
123
+ Value: [DicomMetaDictionary.uid()], // TODO: could be git hash or other valid id
124
+ vr: "UI"
125
+ },
126
+ ImplementationVersionName: {
127
+ Value: ["dcmjs"],
128
+ vr: "SH"
129
+ }
130
+ };
131
+
132
+ return _meta;
133
+ }
134
+
135
+ static generateDerivationSourceDataset(
136
+ StudyInstanceUID,
137
+ SeriesInstanceUID
138
+ ) {
139
+ const _vrMap = {
140
+ PixelData: "OW"
141
+ };
142
+
143
+ const _meta = MeasurementReport.generateDatasetMeta();
144
+
145
+ const derivationSourceDataset = {
146
+ StudyInstanceUID,
147
+ SeriesInstanceUID,
148
+ _meta: _meta,
149
+ _vrMap: _vrMap
150
+ };
151
+
152
+ return derivationSourceDataset;
153
+ }
154
+
155
+ static getSetupMeasurementData(
156
+ MeasurementGroup,
157
+ sopInstanceUIDToImageIdMap,
158
+ metadata,
159
+ toolType
160
+ ) {
161
+ const { ContentSequence } = MeasurementGroup;
162
+
163
+ const contentSequenceArr = toArray(ContentSequence);
164
+ const findingGroup = contentSequenceArr.find(group =>
165
+ codeValueMatch(group, FINDING)
166
+ );
167
+ const findingSiteGroups =
168
+ contentSequenceArr.filter(group =>
169
+ codeValueMatch(group, FINDING_SITE, FINDING_SITE_OLD)
170
+ ) || [];
171
+ const NUMGroup = contentSequenceArr.find(
172
+ group => group.ValueType === "NUM"
173
+ );
174
+ const SCOORDGroup = toArray(NUMGroup.ContentSequence).find(
175
+ group => group.ValueType === "SCOORD"
176
+ );
177
+ const { ReferencedSOPSequence } = SCOORDGroup.ContentSequence;
178
+ const { ReferencedSOPInstanceUID, ReferencedFrameNumber } =
179
+ ReferencedSOPSequence;
180
+
181
+ const referencedImageId =
182
+ sopInstanceUIDToImageIdMap[ReferencedSOPInstanceUID];
183
+ const imagePlaneModule = metadata.get(
184
+ "imagePlaneModule",
185
+ referencedImageId
186
+ );
187
+
188
+ const finding = findingGroup
189
+ ? addAccessors(findingGroup.ConceptCodeSequence)
190
+ : undefined;
191
+ const findingSites = findingSiteGroups.map(fsg => {
192
+ return addAccessors(fsg.ConceptCodeSequence);
193
+ });
194
+
195
+ const defaultState = {
196
+ sopInstanceUid: ReferencedSOPInstanceUID,
197
+ annotation: {
198
+ annotationUID: DicomMetaDictionary.uid(),
199
+ metadata: {
200
+ toolName: toolType,
201
+ referencedImageId,
202
+ FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
203
+ label: ""
204
+ }
205
+ },
206
+ finding,
207
+ findingSites
208
+ };
209
+ if (defaultState.finding) {
210
+ defaultState.description = defaultState.finding.CodeMeaning;
211
+ }
212
+
213
+ defaultState.annotation.metadata.label =
214
+ MeasurementReport.getCornerstoneLabelFromDefaultState(defaultState);
215
+
216
+ return {
217
+ defaultState,
218
+ NUMGroup,
219
+ SCOORDGroup,
220
+ ReferencedSOPSequence,
221
+ ReferencedSOPInstanceUID,
222
+ ReferencedFrameNumber
223
+ };
224
+ }
225
+
226
+ static generateReport(
227
+ toolState,
228
+ metadataProvider,
229
+ worldToImageCoords,
230
+ options
231
+ ) {
232
+ // ToolState for array of imageIDs to a Report
233
+ // Assume Cornerstone metadata provider has access to Study / Series / Sop Instance UID
234
+ let allMeasurementGroups = [];
235
+
236
+ /* Patient ID
237
+ Warning - Missing attribute or value that would be needed to build DICOMDIR - Patient ID
238
+ Warning - Missing attribute or value that would be needed to build DICOMDIR - Study Date
239
+ Warning - Missing attribute or value that would be needed to build DICOMDIR - Study Time
240
+ Warning - Missing attribute or value that would be needed to build DICOMDIR - Study ID
241
+ */
242
+
243
+ const sopInstanceUIDsToSeriesInstanceUIDMap = {};
244
+ const derivationSourceDatasets = [];
245
+
246
+ const _meta = MeasurementReport.generateDatasetMeta();
247
+
248
+ // Loop through each image in the toolData
249
+ Object.keys(toolState).forEach(imageId => {
250
+ const sopCommonModule = metadataProvider.get(
251
+ "sopCommonModule",
252
+ imageId
253
+ );
254
+ const generalSeriesModule = metadataProvider.get(
255
+ "generalSeriesModule",
256
+ imageId
257
+ );
258
+
259
+ const { sopInstanceUID, sopClassUID } = sopCommonModule;
260
+ const { studyInstanceUID, seriesInstanceUID } = generalSeriesModule;
261
+
262
+ sopInstanceUIDsToSeriesInstanceUIDMap[sopInstanceUID] =
263
+ seriesInstanceUID;
264
+
265
+ if (
266
+ !derivationSourceDatasets.find(
267
+ dsd => dsd.SeriesInstanceUID === seriesInstanceUID
268
+ )
269
+ ) {
270
+ // Entry not present for series, create one.
271
+ const derivationSourceDataset =
272
+ MeasurementReport.generateDerivationSourceDataset(
273
+ studyInstanceUID,
274
+ seriesInstanceUID
275
+ );
276
+
277
+ derivationSourceDatasets.push(derivationSourceDataset);
278
+ }
279
+
280
+ const frameNumber = metadataProvider.get("frameNumber", imageId);
281
+ const toolData = toolState[imageId];
282
+ const toolTypes = Object.keys(toolData);
283
+
284
+ const ReferencedSOPSequence = {
285
+ ReferencedSOPClassUID: sopClassUID,
286
+ ReferencedSOPInstanceUID: sopInstanceUID
287
+ };
288
+
289
+ const instance = metadataProvider.get("instance", imageId);
290
+ if (
291
+ (instance &&
292
+ instance.NumberOfFrames &&
293
+ instance.NumberOfFrames > 1) ||
294
+ Normalizer.isMultiframeSOPClassUID(sopClassUID)
295
+ ) {
296
+ ReferencedSOPSequence.ReferencedFrameNumber = frameNumber;
297
+ }
298
+
299
+ // Loop through each tool type for the image
300
+ const measurementGroups = [];
301
+
302
+ toolTypes.forEach(toolType => {
303
+ const group = getMeasurementGroup(
304
+ toolType,
305
+ toolData,
306
+ ReferencedSOPSequence,
307
+ worldToImageCoords
308
+ );
309
+ if (group) {
310
+ measurementGroups.push(group);
311
+ }
312
+ });
313
+
314
+ allMeasurementGroups =
315
+ allMeasurementGroups.concat(measurementGroups);
316
+ });
317
+
318
+ const tid1500MeasurementReport = new TID1500MeasurementReport(
319
+ { TID1501MeasurementGroups: allMeasurementGroups },
320
+ options
321
+ );
322
+
323
+ const report = new StructuredReport(derivationSourceDatasets);
324
+
325
+ const contentItem = tid1500MeasurementReport.contentItem(
326
+ derivationSourceDatasets,
327
+ { sopInstanceUIDsToSeriesInstanceUIDMap }
328
+ );
329
+
330
+ // Merge the derived dataset with the content from the Measurement Report
331
+ report.dataset = Object.assign(report.dataset, contentItem);
332
+ report.dataset._meta = _meta;
333
+
334
+ return report;
335
+ }
336
+
337
+ /**
338
+ * Generate Cornerstone tool state from dataset
339
+ * @param {object} dataset dataset
340
+ * @param {object} hooks
341
+ * @param {function} hooks.getToolClass Function to map dataset to a tool class
342
+ * @returns
343
+ */
344
+ static generateToolState(
345
+ dataset,
346
+ sopInstanceUIDToImageIdMap,
347
+ imageToWorldCoords,
348
+ metadata,
349
+ hooks = {}
350
+ ) {
351
+ // For now, bail out if the dataset is not a TID1500 SR with length measurements
352
+ if (dataset.ContentTemplateSequence.TemplateIdentifier !== "1500") {
353
+ throw new Error(
354
+ "This package can currently only interpret DICOM SR TID 1500"
355
+ );
356
+ }
357
+
358
+ const REPORT = "Imaging Measurements";
359
+ const GROUP = "Measurement Group";
360
+ const TRACKING_IDENTIFIER = "Tracking Identifier";
361
+
362
+ // Identify the Imaging Measurements
363
+ const imagingMeasurementContent = toArray(dataset.ContentSequence).find(
364
+ codeMeaningEquals(REPORT)
365
+ );
366
+
367
+ // Retrieve the Measurements themselves
368
+ const measurementGroups = toArray(
369
+ imagingMeasurementContent.ContentSequence
370
+ ).filter(codeMeaningEquals(GROUP));
371
+
372
+ // For each of the supported measurement types, compute the measurement data
373
+ const measurementData = {};
374
+
375
+ const cornerstoneToolClasses =
376
+ MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
377
+
378
+ const registeredToolClasses = [];
379
+
380
+ Object.keys(cornerstoneToolClasses).forEach(key => {
381
+ registeredToolClasses.push(cornerstoneToolClasses[key]);
382
+ measurementData[key] = [];
383
+ });
384
+
385
+ measurementGroups.forEach(measurementGroup => {
386
+ const measurementGroupContentSequence = toArray(
387
+ measurementGroup.ContentSequence
388
+ );
389
+
390
+ const TrackingIdentifierGroup =
391
+ measurementGroupContentSequence.find(
392
+ contentItem =>
393
+ contentItem.ConceptNameCodeSequence.CodeMeaning ===
394
+ TRACKING_IDENTIFIER
395
+ );
396
+
397
+ const TrackingIdentifierValue = TrackingIdentifierGroup.TextValue;
398
+
399
+ const toolClass = hooks.getToolClass
400
+ ? hooks.getToolClass(
401
+ measurementGroup,
402
+ dataset,
403
+ registeredToolClasses
404
+ )
405
+ : registeredToolClasses.find(tc =>
406
+ tc.isValidCornerstoneTrackingIdentifier(
407
+ TrackingIdentifierValue
408
+ )
409
+ );
410
+
411
+ if (toolClass) {
412
+ const measurement = toolClass.getMeasurementData(
413
+ measurementGroup,
414
+ sopInstanceUIDToImageIdMap,
415
+ imageToWorldCoords,
416
+ metadata
417
+ );
418
+
419
+ console.log(`=== ${toolClass.toolType} ===`);
420
+ console.log(measurement);
421
+
422
+ measurementData[toolClass.toolType].push(measurement);
423
+ }
424
+ });
425
+
426
+ // NOTE: There is no way of knowing the cornerstone imageIds as that could be anything.
427
+ // That is up to the consumer to derive from the SOPInstanceUIDs.
428
+ return measurementData;
429
+ }
430
+
431
+ static registerTool(toolClass) {
432
+ MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE[
433
+ toolClass.utilityToolType
434
+ ] = toolClass;
435
+ MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_TOOL_TYPE[
436
+ toolClass.toolType
437
+ ] = toolClass;
438
+ MeasurementReport.MEASUREMENT_BY_TOOLTYPE[toolClass.toolType] =
439
+ toolClass.utilityToolType;
440
+ }
441
+ }
442
+
443
+ MeasurementReport.MEASUREMENT_BY_TOOLTYPE = {};
444
+ MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE = {};
445
+ MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_TOOL_TYPE = {};
@@ -0,0 +1,137 @@
1
+ import MeasurementReport from "./MeasurementReport";
2
+ import { utilities } from "dcmjs";
3
+ import CORNERSTONE_3D_TAG from "./cornerstone3DTag";
4
+ import { vec3 } from "gl-matrix";
5
+
6
+ const { Polyline: TID300Polyline } = utilities.TID300;
7
+
8
+ const PLANARFREEHANDROI = "PlanarFreehandROI";
9
+ const trackingIdentifierTextValue = `${CORNERSTONE_3D_TAG}:${PLANARFREEHANDROI}`;
10
+ const closedContourThreshold = 1e-5;
11
+
12
+ class PlanarFreehandROI {
13
+ static getMeasurementData(
14
+ MeasurementGroup,
15
+ sopInstanceUIDToImageIdMap,
16
+ imageToWorldCoords,
17
+ metadata
18
+ ) {
19
+ const { defaultState, SCOORDGroup, ReferencedFrameNumber } =
20
+ MeasurementReport.getSetupMeasurementData(
21
+ MeasurementGroup,
22
+ sopInstanceUIDToImageIdMap,
23
+ metadata,
24
+ PlanarFreehandROI.toolType
25
+ );
26
+
27
+ const referencedImageId =
28
+ defaultState.annotation.metadata.referencedImageId;
29
+ const { GraphicData } = SCOORDGroup;
30
+
31
+ const worldCoords = [];
32
+
33
+ for (let i = 0; i < GraphicData.length; i += 2) {
34
+ const point = imageToWorldCoords(referencedImageId, [
35
+ GraphicData[i],
36
+ GraphicData[i + 1]
37
+ ]);
38
+
39
+ worldCoords.push(point);
40
+ }
41
+
42
+ const distanceBetweenFirstAndLastPoint = vec3.distance(
43
+ worldCoords[worldCoords.length - 1],
44
+ worldCoords[0]
45
+ );
46
+
47
+ let isOpenContour = true;
48
+
49
+ // If the contour is closed, this should have been encoded as exactly the same point, so check for a very small difference.
50
+ if (distanceBetweenFirstAndLastPoint < closedContourThreshold) {
51
+ worldCoords.pop(); // Remove the last element which is duplicated.
52
+
53
+ isOpenContour = false;
54
+ }
55
+
56
+ let points = [];
57
+
58
+ if (isOpenContour) {
59
+ points.push(worldCoords[0], worldCoords[worldCoords.length - 1]);
60
+ }
61
+
62
+ const state = defaultState;
63
+
64
+ state.annotation.data = {
65
+ polyline: worldCoords,
66
+ isOpenContour,
67
+ handles: {
68
+ points,
69
+ activeHandleIndex: null,
70
+ textBox: {
71
+ hasMoved: false
72
+ }
73
+ },
74
+ frameNumber: ReferencedFrameNumber
75
+ };
76
+
77
+ return state;
78
+ }
79
+
80
+ static getTID300RepresentationArguments(tool, worldToImageCoords) {
81
+ const { data, finding, findingSites, metadata } = tool;
82
+ const { isOpenContour, polyline } = data;
83
+
84
+ const { referencedImageId } = metadata;
85
+
86
+ if (!referencedImageId) {
87
+ throw new Error(
88
+ "PlanarFreehandROI.getTID300RepresentationArguments: referencedImageId is not defined"
89
+ );
90
+ }
91
+
92
+ const points = polyline.map(worldPos =>
93
+ worldToImageCoords(referencedImageId, worldPos)
94
+ );
95
+
96
+ if (!isOpenContour) {
97
+ // Need to repeat the first point at the end of to have an explicitly closed contour.
98
+ const firstPoint = points[0];
99
+
100
+ // Explicitly expand to avoid ciruclar references.
101
+ points.push([firstPoint[0], firstPoint[1]]);
102
+ }
103
+
104
+ const area = 0; // TODO -> The tool doesn't have these stats yet.
105
+ const perimeter = 0;
106
+
107
+ return {
108
+ points,
109
+ area,
110
+ perimeter,
111
+ trackingIdentifierTextValue,
112
+ finding,
113
+ findingSites: findingSites || []
114
+ };
115
+ }
116
+ }
117
+
118
+ PlanarFreehandROI.toolType = PLANARFREEHANDROI;
119
+ PlanarFreehandROI.utilityToolType = PLANARFREEHANDROI;
120
+ PlanarFreehandROI.TID300Representation = TID300Polyline;
121
+ PlanarFreehandROI.isValidCornerstoneTrackingIdentifier = TrackingIdentifier => {
122
+ if (!TrackingIdentifier.includes(":")) {
123
+ return false;
124
+ }
125
+
126
+ const [cornerstone3DTag, toolType] = TrackingIdentifier.split(":");
127
+
128
+ if (cornerstone3DTag !== CORNERSTONE_3D_TAG) {
129
+ return false;
130
+ }
131
+
132
+ return toolType === PLANARFREEHANDROI;
133
+ };
134
+
135
+ MeasurementReport.registerTool(PlanarFreehandROI);
136
+
137
+ export default PlanarFreehandROI;