@ohif/app 3.8.0-beta.8 → 3.8.0-beta.81

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 (99) hide show
  1. package/dist/{220.bundle.f7e1c96c94245e70f2be.js → 109.bundle.b4fee2a22b622839baf5.js} +4466 -3715
  2. package/dist/{471.bundle.49c8d281adbae4a2c4df.js → 121.bundle.47f05840a5b3cdf75543.js} +94 -113
  3. package/dist/141.bundle.556b4c1e4cab770417ac.js +8620 -0
  4. package/dist/{687.bundle.9065db35c01823286f08.js → 164.bundle.fadc7c5d634402c73b5f.js} +22 -38
  5. package/dist/17dd54813d5acc10bf8f.wasm +0 -0
  6. package/dist/183.bundle.a3e238998be71c4b2af8.js +30410 -0
  7. package/dist/{506.bundle.5731bb4349e266491225.js → 188.bundle.51dc4b37920f45594393.js} +23 -28
  8. package/dist/{342.bundle.e7c3d500f86fdfcc62b5.js → 206.bundle.fcaa081a0d1f68095c31.js} +1991 -1145
  9. package/dist/20fc4c659b85ccd2a9c0.wasm +0 -0
  10. package/dist/217.bundle.d44bbaa50b6fa563fe15.js +115126 -0
  11. package/dist/{451.bundle.57c21db5d003c75e9d61.js → 295.bundle.5ace95771ced62bdcab8.js} +111 -128
  12. package/dist/{125.bundle.253395f320b72180da63.js → 297.bundle.194d8985ab974839b5b6.js} +7 -8
  13. package/dist/{19.bundle.f77c5787b6d8ac0b638b.js → 325.bundle.fd8e0c18db4708d03a91.js} +477 -373
  14. package/dist/335.bundle.8400aa5a88697a6b9d53.js +2590 -0
  15. package/dist/{202.bundle.d3490836f71e001dd30f.js → 342.bundle.e6d0bba29351b5650a8c.js} +566 -868
  16. package/dist/{776.bundle.a2dedb405a12ffd7699b.js → 41.bundle.0905b258a90a7c6437bb.js} +7453 -3624
  17. package/dist/422.bundle.c6fd037b075dd54f1ba7.js +865 -0
  18. package/dist/{957.bundle.9ea4506963ef8b2d84ba.js → 433.bundle.e0018820758f5a86fa7f.js} +14797 -27561
  19. package/dist/445.bundle.38c6d2af64e41cd7c614.js +7835 -0
  20. package/dist/{126.bundle.6e7111d58bcc937ffd80.js → 448.bundle.5e6da31477887bf53016.js} +356 -430
  21. package/dist/487.bundle.89d973049defb3ba6cb7.js +1876 -0
  22. package/dist/{886.bundle.c8dd3ecc42a4253de278.js → 530.bundle.207b38c15c4c01e4db0e.js} +104 -121
  23. package/dist/{250.bundle.aea3335667054bdefe36.js → 544.bundle.1c1f57118560046649c1.js} +37 -62
  24. package/dist/574.bundle.d648fea691d6709bf2b4.js +2652 -0
  25. package/dist/{181.css → 574.css} +1 -1
  26. package/dist/{410.bundle.15c855b0ff4a1a674fb8.js → 594.bundle.84076375b127b9c7f673.js} +183 -221
  27. package/dist/{221.bundle.aef554202c58483cb34e.js → 633.bundle.acab89baaa06a299d679.js} +365 -553
  28. package/dist/{774.bundle.4b2dc46a35012b898e1a.js → 644.bundle.1e77691d2eeb96a423b0.js} +1852 -8945
  29. package/dist/{663.bundle.d7be28450db14266cdd0.js → 669.bundle.b17e8a621e38d92c653f.js} +310 -265
  30. package/dist/699.bundle.9367d7ef9f7615b2e733.js +772 -0
  31. package/dist/702.bundle.963481fbf871984b646f.js +8426 -0
  32. package/dist/722.bundle.afab1fe6bfcd569130ac.js +1083 -0
  33. package/dist/{359.bundle.45ecb3d28e8c22142606.js → 724.bundle.55f9f49816de931af91a.js} +165 -260
  34. package/dist/{757.bundle.ec8301d8e70d2b990f65.js → 726.bundle.0b3d9277d22fe7e15b89.js} +512 -879
  35. package/dist/{530.bundle.a03b6f942ace3e1baa1e.js → 835.bundle.15aff0b7433bb0dd6d6d.js} +37 -30
  36. package/dist/{822.bundle.82cdc418f8f56da6060b.js → 862.bundle.d32ab08e64806b2e964d.js} +81 -97
  37. package/dist/{236.bundle.4e9924934a747afac132.js → 889.bundle.8ef8b723d0163d5d135c.js} +207 -199
  38. package/dist/{281.bundle.deb7492d143e7768d8bf.js → 905.bundle.8a96e1a75b7cfe5ec093.js} +157 -124
  39. package/dist/{814.bundle.c8c951d20039b63b865a.js → 907.bundle.5c88ed911bed18582da4.js} +16 -30
  40. package/dist/{417.bundle.af0a207c29b109f84159.js → 931.bundle.d270a1fda9a2836c3cc5.js} +26 -26
  41. package/dist/{686.bundle.dccef1f36e4bc79bcc48.js → 939.bundle.9d93b2e47c52338747a2.js} +7 -8
  42. package/dist/94.bundle.f5f2479c214180d05d42.js +778 -0
  43. package/dist/{12.bundle.b5ca13e5363f170ecb3b.js → 961.bundle.f4e52bc76d3044d05372.js} +20 -33
  44. package/dist/app-config.js +1 -0
  45. package/dist/app.bundle.css +16 -13
  46. package/dist/{app.bundle.a978edc59b9d82f2eb22.js → app.bundle.ed937512f7d19d61c411.js} +183396 -87682
  47. package/dist/assets/images/CT-AAA.png +0 -0
  48. package/dist/assets/images/CT-AAA2.png +0 -0
  49. package/dist/assets/images/CT-Air.png +0 -0
  50. package/dist/assets/images/CT-Bone.png +0 -0
  51. package/dist/assets/images/CT-Bones.png +0 -0
  52. package/dist/assets/images/CT-Cardiac.png +0 -0
  53. package/dist/assets/images/CT-Cardiac2.png +0 -0
  54. package/dist/assets/images/CT-Cardiac3.png +0 -0
  55. package/dist/assets/images/CT-Chest-Contrast-Enhanced.png +0 -0
  56. package/dist/assets/images/CT-Chest-Vessels.png +0 -0
  57. package/dist/assets/images/CT-Coronary-Arteries-2.png +0 -0
  58. package/dist/assets/images/CT-Coronary-Arteries-3.png +0 -0
  59. package/dist/assets/images/CT-Coronary-Arteries.png +0 -0
  60. package/dist/assets/images/CT-Cropped-Volume-Bone.png +0 -0
  61. package/dist/assets/images/CT-Fat.png +0 -0
  62. package/dist/assets/images/CT-Liver-Vasculature.png +0 -0
  63. package/dist/assets/images/CT-Lung.png +0 -0
  64. package/dist/assets/images/CT-MIP.png +0 -0
  65. package/dist/assets/images/CT-Muscle.png +0 -0
  66. package/dist/assets/images/CT-Pulmonary-Arteries.png +0 -0
  67. package/dist/assets/images/CT-Soft-Tissue.png +0 -0
  68. package/dist/assets/images/DTI-FA-Brain.png +0 -0
  69. package/dist/assets/images/MR-Angio.png +0 -0
  70. package/dist/assets/images/MR-Default.png +0 -0
  71. package/dist/assets/images/MR-MIP.png +0 -0
  72. package/dist/assets/images/MR-T2-Brain.png +0 -0
  73. package/dist/assets/images/VolumeRendering.png +0 -0
  74. package/dist/cornerstoneDICOMImageLoader.min.js +1 -1
  75. package/dist/cornerstoneDICOMImageLoader.min.js.map +1 -1
  76. package/dist/{dicom-microscopy-viewer.bundle.2c146384eb9466d02ff8.js → dicom-microscopy-viewer.bundle.d3a56dc9f62df5e11019.js} +3 -3
  77. package/dist/histogram-worker.bundle.829e14ec12c2b41a4323.js +359 -0
  78. package/dist/index.html +1 -1
  79. package/dist/{index.worker.e62ecca63f1a2e124230.worker.js → index.worker.64c896c4316fcd506666.worker.js} +2 -2
  80. package/dist/index.worker.64c896c4316fcd506666.worker.js.map +1 -0
  81. package/dist/polySeg.bundle.f1a6ece1396dc1385155.js +249 -0
  82. package/dist/serve.json +12 -0
  83. package/dist/sw.js +1 -1
  84. package/package.json +26 -22
  85. package/dist/181.bundle.a62b9f0ec692299acb35.js +0 -1527
  86. package/dist/23.bundle.e008ad788170f2ed5569.js +0 -900
  87. package/dist/604.bundle.a51f83e64004bca5f497.js +0 -1848
  88. package/dist/613.bundle.9e7072e5b575354fe51e.js +0 -532
  89. package/dist/743.bundle.489f7df3a089d4d374e1.js +0 -78007
  90. package/dist/75788f12450d4c5ed494.wasm +0 -0
  91. package/dist/775.bundle.2285e7e0e67878948c0d.js +0 -1009
  92. package/dist/788.bundle.207ac23c0dfa70cbe3fb.js +0 -2682
  93. package/dist/82.bundle.d6fdcca0f67540bb226a.js +0 -1049
  94. package/dist/index.worker.e62ecca63f1a2e124230.worker.js.map +0 -1
  95. /package/dist/{19.css → 325.css} +0 -0
  96. /package/dist/{776.css → 41.css} +0 -0
  97. /package/dist/{579.css → 481.css} +0 -0
  98. /package/dist/{250.css → 544.css} +0 -0
  99. /package/dist/{221.css → 633.css} +0 -0
@@ -0,0 +1,2590 @@
1
+ "use strict";
2
+ (globalThis["webpackChunk"] = globalThis["webpackChunk"] || []).push([[335],{
3
+
4
+ /***/ 27335:
5
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
6
+
7
+ // ESM COMPAT FLAG
8
+ __webpack_require__.r(__webpack_exports__);
9
+
10
+ // EXPORTS
11
+ __webpack_require__.d(__webpack_exports__, {
12
+ "default": () => (/* binding */ tmtv_src)
13
+ });
14
+
15
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/package.json
16
+ const package_namespaceObject = /*#__PURE__*/JSON.parse('{"UU":"@ohif/extension-tmtv"}');
17
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/id.js
18
+
19
+ const id = package_namespaceObject.UU;
20
+
21
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/hpViewports.ts
22
+ const ctAXIAL = {
23
+ viewportOptions: {
24
+ viewportId: 'ctAXIAL',
25
+ viewportType: 'volume',
26
+ orientation: 'axial',
27
+ toolGroupId: 'ctToolGroup',
28
+ initialImageOptions: {
29
+ // index: 5,
30
+ preset: 'first' // 'first', 'last', 'middle'
31
+ },
32
+ syncGroups: [{
33
+ type: 'cameraPosition',
34
+ id: 'axialSync',
35
+ source: true,
36
+ target: true
37
+ }, {
38
+ type: 'voi',
39
+ id: 'ctWLSync',
40
+ source: true,
41
+ target: true,
42
+ options: {
43
+ syncColormap: true
44
+ }
45
+ }]
46
+ },
47
+ displaySets: [{
48
+ id: 'ctDisplaySet'
49
+ }]
50
+ };
51
+ const ctSAGITTAL = {
52
+ viewportOptions: {
53
+ viewportId: 'ctSAGITTAL',
54
+ viewportType: 'volume',
55
+ orientation: 'sagittal',
56
+ toolGroupId: 'ctToolGroup',
57
+ syncGroups: [{
58
+ type: 'cameraPosition',
59
+ id: 'sagittalSync',
60
+ source: true,
61
+ target: true
62
+ }, {
63
+ type: 'voi',
64
+ id: 'ctWLSync',
65
+ source: true,
66
+ target: true,
67
+ options: {
68
+ syncColormap: true
69
+ }
70
+ }]
71
+ },
72
+ displaySets: [{
73
+ id: 'ctDisplaySet'
74
+ }]
75
+ };
76
+ const ctCORONAL = {
77
+ viewportOptions: {
78
+ viewportId: 'ctCORONAL',
79
+ viewportType: 'volume',
80
+ orientation: 'coronal',
81
+ toolGroupId: 'ctToolGroup',
82
+ syncGroups: [{
83
+ type: 'cameraPosition',
84
+ id: 'coronalSync',
85
+ source: true,
86
+ target: true
87
+ }, {
88
+ type: 'voi',
89
+ id: 'ctWLSync',
90
+ source: true,
91
+ target: true,
92
+ options: {
93
+ syncColormap: true
94
+ }
95
+ }]
96
+ },
97
+ displaySets: [{
98
+ id: 'ctDisplaySet'
99
+ }]
100
+ };
101
+ const ptAXIAL = {
102
+ viewportOptions: {
103
+ viewportId: 'ptAXIAL',
104
+ viewportType: 'volume',
105
+ background: [1, 1, 1],
106
+ orientation: 'axial',
107
+ toolGroupId: 'ptToolGroup',
108
+ initialImageOptions: {
109
+ // index: 5,
110
+ preset: 'first' // 'first', 'last', 'middle'
111
+ },
112
+ syncGroups: [{
113
+ type: 'cameraPosition',
114
+ id: 'axialSync',
115
+ source: true,
116
+ target: true
117
+ }, {
118
+ type: 'voi',
119
+ id: 'ptWLSync',
120
+ source: true,
121
+ target: true,
122
+ options: {
123
+ syncColormap: true
124
+ }
125
+ }, {
126
+ type: 'voi',
127
+ id: 'ptFusionWLSync',
128
+ source: true,
129
+ target: false,
130
+ options: {
131
+ syncInvertState: false
132
+ }
133
+ }]
134
+ },
135
+ displaySets: [{
136
+ options: {
137
+ voi: {
138
+ custom: 'getPTVOIRange'
139
+ },
140
+ voiInverted: true
141
+ },
142
+ id: 'ptDisplaySet'
143
+ }]
144
+ };
145
+ const ptSAGITTAL = {
146
+ viewportOptions: {
147
+ viewportId: 'ptSAGITTAL',
148
+ viewportType: 'volume',
149
+ orientation: 'sagittal',
150
+ background: [1, 1, 1],
151
+ toolGroupId: 'ptToolGroup',
152
+ syncGroups: [{
153
+ type: 'cameraPosition',
154
+ id: 'sagittalSync',
155
+ source: true,
156
+ target: true
157
+ }, {
158
+ type: 'voi',
159
+ id: 'ptWLSync',
160
+ source: true,
161
+ target: true,
162
+ options: {
163
+ syncColormap: true
164
+ }
165
+ }, {
166
+ type: 'voi',
167
+ id: 'ptFusionWLSync',
168
+ source: true,
169
+ target: false,
170
+ options: {
171
+ syncInvertState: false
172
+ }
173
+ }]
174
+ },
175
+ displaySets: [{
176
+ options: {
177
+ voi: {
178
+ custom: 'getPTVOIRange'
179
+ },
180
+ voiInverted: true
181
+ },
182
+ id: 'ptDisplaySet'
183
+ }]
184
+ };
185
+ const ptCORONAL = {
186
+ viewportOptions: {
187
+ viewportId: 'ptCORONAL',
188
+ viewportType: 'volume',
189
+ orientation: 'coronal',
190
+ background: [1, 1, 1],
191
+ toolGroupId: 'ptToolGroup',
192
+ syncGroups: [{
193
+ type: 'cameraPosition',
194
+ id: 'coronalSync',
195
+ source: true,
196
+ target: true
197
+ }, {
198
+ type: 'voi',
199
+ id: 'ptWLSync',
200
+ source: true,
201
+ target: true,
202
+ options: {
203
+ syncColormap: true
204
+ }
205
+ }, {
206
+ type: 'voi',
207
+ id: 'ptFusionWLSync',
208
+ source: true,
209
+ target: false,
210
+ options: {
211
+ syncInvertState: false
212
+ }
213
+ }]
214
+ },
215
+ displaySets: [{
216
+ options: {
217
+ voi: {
218
+ custom: 'getPTVOIRange'
219
+ },
220
+ voiInverted: true
221
+ },
222
+ id: 'ptDisplaySet'
223
+ }]
224
+ };
225
+ const fusionAXIAL = {
226
+ viewportOptions: {
227
+ viewportId: 'fusionAXIAL',
228
+ viewportType: 'volume',
229
+ orientation: 'axial',
230
+ toolGroupId: 'fusionToolGroup',
231
+ initialImageOptions: {
232
+ // index: 5,
233
+ preset: 'first' // 'first', 'last', 'middle'
234
+ },
235
+ syncGroups: [{
236
+ type: 'cameraPosition',
237
+ id: 'axialSync',
238
+ source: true,
239
+ target: true
240
+ }, {
241
+ type: 'voi',
242
+ id: 'ctWLSync',
243
+ source: false,
244
+ target: true
245
+ }, {
246
+ type: 'voi',
247
+ id: 'fusionWLSync',
248
+ source: true,
249
+ target: true,
250
+ options: {
251
+ syncColormap: true
252
+ }
253
+ }, {
254
+ type: 'voi',
255
+ id: 'ptFusionWLSync',
256
+ source: false,
257
+ target: true,
258
+ options: {
259
+ syncInvertState: false
260
+ }
261
+ }]
262
+ },
263
+ displaySets: [{
264
+ id: 'ctDisplaySet'
265
+ }, {
266
+ id: 'ptDisplaySet',
267
+ options: {
268
+ colormap: {
269
+ name: 'hsv',
270
+ opacity: [{
271
+ value: 0,
272
+ opacity: 0
273
+ }, {
274
+ value: 0.1,
275
+ opacity: 0.9
276
+ }, {
277
+ value: 1,
278
+ opacity: 0.95
279
+ }]
280
+ },
281
+ voi: {
282
+ custom: 'getPTVOIRange'
283
+ }
284
+ }
285
+ }]
286
+ };
287
+ const fusionSAGITTAL = {
288
+ viewportOptions: {
289
+ viewportId: 'fusionSAGITTAL',
290
+ viewportType: 'volume',
291
+ orientation: 'sagittal',
292
+ toolGroupId: 'fusionToolGroup',
293
+ // initialImageOptions: {
294
+ // index: 180,
295
+ // preset: 'middle', // 'first', 'last', 'middle'
296
+ // },
297
+ syncGroups: [{
298
+ type: 'cameraPosition',
299
+ id: 'sagittalSync',
300
+ source: true,
301
+ target: true
302
+ }, {
303
+ type: 'voi',
304
+ id: 'ctWLSync',
305
+ source: false,
306
+ target: true
307
+ }, {
308
+ type: 'voi',
309
+ id: 'fusionWLSync',
310
+ source: true,
311
+ target: true,
312
+ options: {
313
+ syncColormap: true
314
+ }
315
+ }, {
316
+ type: 'voi',
317
+ id: 'ptFusionWLSync',
318
+ source: false,
319
+ target: true,
320
+ options: {
321
+ syncInvertState: false
322
+ }
323
+ }]
324
+ },
325
+ displaySets: [{
326
+ id: 'ctDisplaySet'
327
+ }, {
328
+ id: 'ptDisplaySet',
329
+ options: {
330
+ colormap: {
331
+ name: 'hsv',
332
+ opacity: [{
333
+ value: 0,
334
+ opacity: 0
335
+ }, {
336
+ value: 0.1,
337
+ opacity: 0.9
338
+ }, {
339
+ value: 1,
340
+ opacity: 0.95
341
+ }]
342
+ },
343
+ voi: {
344
+ custom: 'getPTVOIRange'
345
+ }
346
+ }
347
+ }]
348
+ };
349
+ const fusionCORONAL = {
350
+ viewportOptions: {
351
+ viewportId: 'fusionCoronal',
352
+ viewportType: 'volume',
353
+ orientation: 'coronal',
354
+ toolGroupId: 'fusionToolGroup',
355
+ // initialImageOptions: {
356
+ // index: 180,
357
+ // preset: 'middle', // 'first', 'last', 'middle'
358
+ // },
359
+ syncGroups: [{
360
+ type: 'cameraPosition',
361
+ id: 'coronalSync',
362
+ source: true,
363
+ target: true
364
+ }, {
365
+ type: 'voi',
366
+ id: 'ctWLSync',
367
+ source: false,
368
+ target: true
369
+ }, {
370
+ type: 'voi',
371
+ id: 'fusionWLSync',
372
+ source: true,
373
+ target: true,
374
+ options: {
375
+ syncColormap: true
376
+ }
377
+ }, {
378
+ type: 'voi',
379
+ id: 'ptFusionWLSync',
380
+ source: false,
381
+ target: true,
382
+ options: {
383
+ syncInvertState: false
384
+ }
385
+ }]
386
+ },
387
+ displaySets: [{
388
+ id: 'ctDisplaySet'
389
+ }, {
390
+ id: 'ptDisplaySet',
391
+ options: {
392
+ colormap: {
393
+ name: 'hsv',
394
+ opacity: [{
395
+ value: 0,
396
+ opacity: 0
397
+ }, {
398
+ value: 0.1,
399
+ opacity: 0.9
400
+ }, {
401
+ value: 1,
402
+ opacity: 0.95
403
+ }]
404
+ },
405
+ voi: {
406
+ custom: 'getPTVOIRange'
407
+ }
408
+ }
409
+ }]
410
+ };
411
+ const mipSAGITTAL = {
412
+ viewportOptions: {
413
+ viewportId: 'mipSagittal',
414
+ viewportType: 'volume',
415
+ orientation: 'sagittal',
416
+ background: [1, 1, 1],
417
+ toolGroupId: 'mipToolGroup',
418
+ syncGroups: [{
419
+ type: 'voi',
420
+ id: 'ptWLSync',
421
+ source: true,
422
+ target: true,
423
+ options: {
424
+ syncColormap: true
425
+ }
426
+ }, {
427
+ type: 'voi',
428
+ id: 'ptFusionWLSync',
429
+ source: true,
430
+ target: false,
431
+ options: {
432
+ syncInvertState: false
433
+ }
434
+ }],
435
+ // Custom props can be used to set custom properties which extensions
436
+ // can react on.
437
+ customViewportProps: {
438
+ // We use viewportDisplay to filter the viewports which are displayed
439
+ // in mip and we set the scrollbar according to their rotation index
440
+ // in the cornerstone extension.
441
+ hideOverlays: true
442
+ }
443
+ },
444
+ displaySets: [{
445
+ options: {
446
+ blendMode: 'MIP',
447
+ slabThickness: 'fullVolume',
448
+ voi: {
449
+ custom: 'getPTVOIRange'
450
+ },
451
+ voiInverted: true
452
+ },
453
+ id: 'ptDisplaySet'
454
+ }]
455
+ };
456
+
457
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/getHangingProtocolModule.js
458
+
459
+
460
+ /**
461
+ * represents a 3x4 viewport layout configuration. The layout displays CT axial, sagittal, and coronal
462
+ * images in the first row, PT axial, sagittal, and coronal images in the second row, and fusion axial,
463
+ * sagittal, and coronal images in the third row. The fourth column is fully spanned by a MIP sagittal
464
+ * image, covering all three rows. It has synchronizers for windowLevel for all CT and PT images, and
465
+ * also camera synchronizer for each orientation
466
+ */
467
+ const stage1 = {
468
+ name: 'default',
469
+ viewportStructure: {
470
+ layoutType: 'grid',
471
+ properties: {
472
+ rows: 3,
473
+ columns: 4,
474
+ layoutOptions: [{
475
+ x: 0,
476
+ y: 0,
477
+ width: 1 / 4,
478
+ height: 1 / 3
479
+ }, {
480
+ x: 1 / 4,
481
+ y: 0,
482
+ width: 1 / 4,
483
+ height: 1 / 3
484
+ }, {
485
+ x: 2 / 4,
486
+ y: 0,
487
+ width: 1 / 4,
488
+ height: 1 / 3
489
+ }, {
490
+ x: 0,
491
+ y: 1 / 3,
492
+ width: 1 / 4,
493
+ height: 1 / 3
494
+ }, {
495
+ x: 1 / 4,
496
+ y: 1 / 3,
497
+ width: 1 / 4,
498
+ height: 1 / 3
499
+ }, {
500
+ x: 2 / 4,
501
+ y: 1 / 3,
502
+ width: 1 / 4,
503
+ height: 1 / 3
504
+ }, {
505
+ x: 0,
506
+ y: 2 / 3,
507
+ width: 1 / 4,
508
+ height: 1 / 3
509
+ }, {
510
+ x: 1 / 4,
511
+ y: 2 / 3,
512
+ width: 1 / 4,
513
+ height: 1 / 3
514
+ }, {
515
+ x: 2 / 4,
516
+ y: 2 / 3,
517
+ width: 1 / 4,
518
+ height: 1 / 3
519
+ }, {
520
+ x: 3 / 4,
521
+ y: 0,
522
+ width: 1 / 4,
523
+ height: 1
524
+ }]
525
+ }
526
+ },
527
+ viewports: [ctAXIAL, ctSAGITTAL, ctCORONAL, ptAXIAL, ptSAGITTAL, ptCORONAL, fusionAXIAL, fusionSAGITTAL, fusionCORONAL, mipSAGITTAL],
528
+ createdDate: '2021-02-23T18:32:42.850Z'
529
+ };
530
+
531
+ /**
532
+ * The layout displays CT axial image in the top-left viewport, fusion axial image
533
+ * in the top-right viewport, PT axial image in the bottom-left viewport, and MIP
534
+ * sagittal image in the bottom-right viewport. The layout follows a simple grid
535
+ * pattern with 2 rows and 2 columns. It includes synchronizers as well.
536
+ */
537
+ const stage2 = {
538
+ name: 'Fusion 2x2',
539
+ viewportStructure: {
540
+ layoutType: 'grid',
541
+ properties: {
542
+ rows: 2,
543
+ columns: 2
544
+ }
545
+ },
546
+ viewports: [ctAXIAL, fusionAXIAL, ptAXIAL, mipSAGITTAL]
547
+ };
548
+
549
+ /**
550
+ * The top row displays CT images in axial, sagittal, and coronal orientations from
551
+ * left to right, respectively. The bottom row displays PT images in axial, sagittal,
552
+ * and coronal orientations from left to right, respectively.
553
+ * The layout follows a simple grid pattern with 2 rows and 3 columns.
554
+ * It includes synchronizers as well.
555
+ */
556
+ const stage3 = {
557
+ name: '2x3-layout',
558
+ viewportStructure: {
559
+ layoutType: 'grid',
560
+ properties: {
561
+ rows: 2,
562
+ columns: 3
563
+ }
564
+ },
565
+ viewports: [ctAXIAL, ctSAGITTAL, ctCORONAL, ptAXIAL, ptSAGITTAL, ptCORONAL]
566
+ };
567
+
568
+ /**
569
+ * In this layout, the top row displays PT images in coronal, sagittal, and axial
570
+ * orientations from left to right, respectively, followed by a MIP sagittal image
571
+ * that spans both rows on the rightmost side. The bottom row displays fusion images
572
+ * in coronal, sagittal, and axial orientations from left to right, respectively.
573
+ * There is no viewport in the bottom row's rightmost position, as the MIP sagittal viewport
574
+ * from the top row spans the full height of both rows.
575
+ * It includes synchronizers as well.
576
+ */
577
+ const stage4 = {
578
+ name: '2x4-layout',
579
+ viewportStructure: {
580
+ layoutType: 'grid',
581
+ properties: {
582
+ rows: 2,
583
+ columns: 4,
584
+ layoutOptions: [{
585
+ x: 0,
586
+ y: 0,
587
+ width: 1 / 4,
588
+ height: 1 / 2
589
+ }, {
590
+ x: 1 / 4,
591
+ y: 0,
592
+ width: 1 / 4,
593
+ height: 1 / 2
594
+ }, {
595
+ x: 2 / 4,
596
+ y: 0,
597
+ width: 1 / 4,
598
+ height: 1 / 2
599
+ }, {
600
+ x: 3 / 4,
601
+ y: 0,
602
+ width: 1 / 4,
603
+ height: 1
604
+ }, {
605
+ x: 0,
606
+ y: 1 / 2,
607
+ width: 1 / 4,
608
+ height: 1 / 2
609
+ }, {
610
+ x: 1 / 4,
611
+ y: 1 / 2,
612
+ width: 1 / 4,
613
+ height: 1 / 2
614
+ }, {
615
+ x: 2 / 4,
616
+ y: 1 / 2,
617
+ width: 1 / 4,
618
+ height: 1 / 2
619
+ }]
620
+ }
621
+ },
622
+ viewports: [ptCORONAL, ptSAGITTAL, ptAXIAL, mipSAGITTAL, fusionCORONAL, fusionSAGITTAL, fusionAXIAL]
623
+ };
624
+ const ptCT = {
625
+ id: '@ohif/extension-tmtv.hangingProtocolModule.ptCT',
626
+ locked: true,
627
+ name: 'Default',
628
+ createdDate: '2021-02-23T19:22:08.894Z',
629
+ modifiedDate: '2022-10-04T19:22:08.894Z',
630
+ availableTo: {},
631
+ editableBy: {},
632
+ imageLoadStrategy: 'interleaveTopToBottom',
633
+ // "default" , "interleaveTopToBottom", "interleaveCenter"
634
+ protocolMatchingRules: [{
635
+ attribute: 'ModalitiesInStudy',
636
+ constraint: {
637
+ contains: ['CT', 'PT']
638
+ }
639
+ }, {
640
+ attribute: 'StudyDescription',
641
+ constraint: {
642
+ contains: 'PETCT'
643
+ }
644
+ }, {
645
+ attribute: 'StudyDescription',
646
+ constraint: {
647
+ contains: 'PET/CT'
648
+ }
649
+ }],
650
+ displaySetSelectors: {
651
+ ctDisplaySet: {
652
+ seriesMatchingRules: [{
653
+ attribute: 'Modality',
654
+ constraint: {
655
+ equals: {
656
+ value: 'CT'
657
+ }
658
+ },
659
+ required: true
660
+ }, {
661
+ attribute: 'isReconstructable',
662
+ constraint: {
663
+ equals: {
664
+ value: true
665
+ }
666
+ },
667
+ required: true
668
+ }, {
669
+ attribute: 'SeriesDescription',
670
+ constraint: {
671
+ contains: 'CT'
672
+ }
673
+ }, {
674
+ attribute: 'SeriesDescription',
675
+ constraint: {
676
+ contains: 'CT WB'
677
+ }
678
+ }]
679
+ },
680
+ ptDisplaySet: {
681
+ seriesMatchingRules: [{
682
+ attribute: 'Modality',
683
+ constraint: {
684
+ equals: 'PT'
685
+ },
686
+ required: true
687
+ }, {
688
+ attribute: 'isReconstructable',
689
+ constraint: {
690
+ equals: {
691
+ value: true
692
+ }
693
+ },
694
+ required: true
695
+ }, {
696
+ attribute: 'SeriesDescription',
697
+ constraint: {
698
+ contains: 'Corrected'
699
+ }
700
+ }, {
701
+ weight: 2,
702
+ attribute: 'SeriesDescription',
703
+ constraint: {
704
+ doesNotContain: {
705
+ value: 'Uncorrected'
706
+ }
707
+ }
708
+ }]
709
+ }
710
+ },
711
+ stages: [stage1, stage2, stage3, stage4],
712
+ numberOfPriorsReferenced: -1
713
+ };
714
+ function getHangingProtocolModule() {
715
+ return [{
716
+ name: ptCT.id,
717
+ protocol: ptCT
718
+ }];
719
+ }
720
+ /* harmony default export */ const src_getHangingProtocolModule = (getHangingProtocolModule);
721
+ // EXTERNAL MODULE: ../../../node_modules/react/index.js
722
+ var react = __webpack_require__(41766);
723
+ // EXTERNAL MODULE: ../../../node_modules/prop-types/index.js
724
+ var prop_types = __webpack_require__(11374);
725
+ var prop_types_default = /*#__PURE__*/__webpack_require__.n(prop_types);
726
+ // EXTERNAL MODULE: ../../ui/src/index.js + 785 modules
727
+ var src = __webpack_require__(5085);
728
+ // EXTERNAL MODULE: ../../core/src/index.ts + 70 modules
729
+ var core_src = __webpack_require__(55411);
730
+ // EXTERNAL MODULE: ../../../node_modules/react-i18next/dist/es/index.js + 15 modules
731
+ var es = __webpack_require__(80619);
732
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelPetSUV.tsx
733
+
734
+
735
+
736
+
737
+
738
+ const DEFAULT_MEATADATA = {
739
+ PatientWeight: null,
740
+ PatientSex: null,
741
+ SeriesTime: null,
742
+ RadiopharmaceuticalInformationSequence: {
743
+ RadionuclideTotalDose: null,
744
+ RadionuclideHalfLife: null,
745
+ RadiopharmaceuticalStartTime: null
746
+ }
747
+ };
748
+
749
+ /*
750
+ * PETSUV panel enables the user to modify the patient related information, such as
751
+ * patient sex, patientWeight. This is allowed since
752
+ * sometimes these metadata are missing or wrong. By changing them
753
+ * @param param0
754
+ * @returns
755
+ */
756
+ function PanelPetSUV({
757
+ servicesManager,
758
+ commandsManager
759
+ }) {
760
+ const {
761
+ t
762
+ } = (0,es/* useTranslation */.Bd)('PanelSUV');
763
+ const {
764
+ displaySetService,
765
+ toolGroupService,
766
+ toolbarService,
767
+ hangingProtocolService
768
+ } = servicesManager.services;
769
+ const [metadata, setMetadata] = (0,react.useState)(DEFAULT_MEATADATA);
770
+ const [ptDisplaySet, setPtDisplaySet] = (0,react.useState)(null);
771
+ const handleMetadataChange = metadata => {
772
+ setMetadata(prevState => {
773
+ const newState = {
774
+ ...prevState
775
+ };
776
+ Object.keys(metadata).forEach(key => {
777
+ if (typeof metadata[key] === 'object') {
778
+ newState[key] = {
779
+ ...prevState[key],
780
+ ...metadata[key]
781
+ };
782
+ } else {
783
+ newState[key] = metadata[key];
784
+ }
785
+ });
786
+ return newState;
787
+ });
788
+ };
789
+ const getMatchingPTDisplaySet = viewportMatchDetails => {
790
+ const ptDisplaySet = commandsManager.runCommand('getMatchingPTDisplaySet', {
791
+ viewportMatchDetails
792
+ });
793
+ if (!ptDisplaySet) {
794
+ return;
795
+ }
796
+ const metadata = commandsManager.runCommand('getPTMetadata', {
797
+ ptDisplaySet
798
+ });
799
+ return {
800
+ ptDisplaySet,
801
+ metadata
802
+ };
803
+ };
804
+ (0,react.useEffect)(() => {
805
+ const displaySets = displaySetService.getActiveDisplaySets();
806
+ const {
807
+ viewportMatchDetails
808
+ } = hangingProtocolService.getMatchDetails();
809
+ if (!displaySets.length) {
810
+ return;
811
+ }
812
+ const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails);
813
+ if (!displaySetInfo) {
814
+ return;
815
+ }
816
+ const {
817
+ ptDisplaySet,
818
+ metadata
819
+ } = displaySetInfo;
820
+ setPtDisplaySet(ptDisplaySet);
821
+ setMetadata(metadata);
822
+ }, []);
823
+
824
+ // get the patientMetadata from the StudyInstanceUIDs and update the state
825
+ (0,react.useEffect)(() => {
826
+ const {
827
+ unsubscribe
828
+ } = hangingProtocolService.subscribe(hangingProtocolService.EVENTS.PROTOCOL_CHANGED, ({
829
+ viewportMatchDetails
830
+ }) => {
831
+ const displaySetInfo = getMatchingPTDisplaySet(viewportMatchDetails);
832
+ if (!displaySetInfo) {
833
+ return;
834
+ }
835
+ const {
836
+ ptDisplaySet,
837
+ metadata
838
+ } = displaySetInfo;
839
+ setPtDisplaySet(ptDisplaySet);
840
+ setMetadata(metadata);
841
+ });
842
+ return () => {
843
+ unsubscribe();
844
+ };
845
+ }, []);
846
+ function updateMetadata() {
847
+ if (!ptDisplaySet) {
848
+ throw new Error('No ptDisplaySet found');
849
+ }
850
+
851
+ // metadata should be dcmjs naturalized
852
+ core_src.DicomMetadataStore.updateMetadataForSeries(ptDisplaySet.StudyInstanceUID, ptDisplaySet.SeriesInstanceUID, metadata);
853
+
854
+ // update the displaySets
855
+ displaySetService.setDisplaySetMetadataInvalidated(ptDisplaySet.displaySetInstanceUID);
856
+
857
+ // Crosshair position depends on the metadata values such as the positioning interaction
858
+ // between series, so when the metadata is updated, the crosshairs need to be reset.
859
+ setTimeout(() => {
860
+ commandsManager.runCommand('resetCrosshairs');
861
+ }, 0);
862
+ }
863
+ return /*#__PURE__*/react.createElement("div", {
864
+ className: "ohif-scrollbar flex min-h-0 flex-auto select-none flex-col justify-between overflow-auto"
865
+ }, /*#__PURE__*/react.createElement("div", {
866
+ className: "flex min-h-0 flex-1 flex-col bg-black text-[13px] font-[300]"
867
+ }, /*#__PURE__*/react.createElement(src/* PanelSection */.aU, {
868
+ title: t('Patient Information')
869
+ }, /*#__PURE__*/react.createElement("div", {
870
+ className: "flex flex-col"
871
+ }, /*#__PURE__*/react.createElement("div", {
872
+ className: "bg-primary-dark flex flex-col gap-4 p-2"
873
+ }, /*#__PURE__*/react.createElement(src/* Input */.pd, {
874
+ containerClassName: '!flex-row !justify-between items-center',
875
+ label: t('Patient Sex'),
876
+ labelClassName: "text-[13px] font-inter text-white",
877
+ className: "!m-0 !h-[26px] !w-[117px]",
878
+ value: metadata.PatientSex || '',
879
+ onChange: e => {
880
+ handleMetadataChange({
881
+ PatientSex: e.target.value
882
+ });
883
+ }
884
+ }), /*#__PURE__*/react.createElement(src/* Input */.pd, {
885
+ containerClassName: '!flex-row !justify-between items-center',
886
+ label: t('Weight'),
887
+ labelChildren: /*#__PURE__*/react.createElement("span", {
888
+ className: "text-aqua-pale"
889
+ }, " kg"),
890
+ labelClassName: "text-[13px] font-inter text-white",
891
+ className: "!m-0 !h-[26px] !w-[117px]",
892
+ value: metadata.PatientWeight || '',
893
+ onChange: e => {
894
+ handleMetadataChange({
895
+ PatientWeight: e.target.value
896
+ });
897
+ }
898
+ }), /*#__PURE__*/react.createElement(src/* Input */.pd, {
899
+ containerClassName: '!flex-row !justify-between items-center',
900
+ label: t('Total Dose'),
901
+ labelChildren: /*#__PURE__*/react.createElement("span", {
902
+ className: "text-aqua-pale"
903
+ }, " bq"),
904
+ labelClassName: "text-[13px] font-inter text-white",
905
+ className: "!m-0 !h-[26px] !w-[117px]",
906
+ value: metadata.RadiopharmaceuticalInformationSequence.RadionuclideTotalDose || '',
907
+ onChange: e => {
908
+ handleMetadataChange({
909
+ RadiopharmaceuticalInformationSequence: {
910
+ RadionuclideTotalDose: e.target.value
911
+ }
912
+ });
913
+ }
914
+ }), /*#__PURE__*/react.createElement(src/* Input */.pd, {
915
+ containerClassName: '!flex-row !justify-between items-center',
916
+ label: t('Half Life'),
917
+ labelChildren: /*#__PURE__*/react.createElement("span", {
918
+ className: "text-aqua-pale"
919
+ }, " s"),
920
+ labelClassName: "text-[13px] font-inter text-white",
921
+ className: "!m-0 !h-[26px] !w-[117px]",
922
+ value: metadata.RadiopharmaceuticalInformationSequence.RadionuclideHalfLife || '',
923
+ onChange: e => {
924
+ handleMetadataChange({
925
+ RadiopharmaceuticalInformationSequence: {
926
+ RadionuclideHalfLife: e.target.value
927
+ }
928
+ });
929
+ }
930
+ }), /*#__PURE__*/react.createElement(src/* Input */.pd, {
931
+ containerClassName: '!flex-row !justify-between items-center',
932
+ label: t('Injection Time'),
933
+ labelChildren: /*#__PURE__*/react.createElement("span", {
934
+ className: "text-aqua-pale"
935
+ }, " s"),
936
+ labelClassName: "text-[13px] font-inter text-white",
937
+ className: "!m-0 !h-[26px] !w-[117px]",
938
+ value: metadata.RadiopharmaceuticalInformationSequence.RadiopharmaceuticalStartTime || '',
939
+ onChange: e => {
940
+ handleMetadataChange({
941
+ RadiopharmaceuticalInformationSequence: {
942
+ RadiopharmaceuticalStartTime: e.target.value
943
+ }
944
+ });
945
+ }
946
+ }), /*#__PURE__*/react.createElement(src/* Input */.pd, {
947
+ containerClassName: '!flex-row !justify-between items-center',
948
+ label: t('Acquisition Time'),
949
+ labelChildren: /*#__PURE__*/react.createElement("span", {
950
+ className: "text-aqua-pale"
951
+ }, " s"),
952
+ labelClassName: "text-[13px] font-inter text-white",
953
+ className: "!m-0 !h-[26px] !w-[117px]",
954
+ value: metadata.SeriesTime || '',
955
+ onChange: () => {}
956
+ }), /*#__PURE__*/react.createElement(src/* Button */.$n, {
957
+ className: "!h-[26px] !w-[115px] self-end !p-0",
958
+ onClick: updateMetadata
959
+ }, "Reload Data"))))));
960
+ }
961
+ PanelPetSUV.propTypes = {
962
+ servicesManager: prop_types_default().shape({
963
+ services: prop_types_default().shape({
964
+ measurementService: prop_types_default().shape({
965
+ getMeasurements: (prop_types_default()).func.isRequired,
966
+ subscribe: (prop_types_default()).func.isRequired,
967
+ EVENTS: (prop_types_default()).object.isRequired,
968
+ VALUE_TYPES: (prop_types_default()).object.isRequired
969
+ }).isRequired
970
+ }).isRequired
971
+ }).isRequired
972
+ };
973
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdExport.tsx
974
+
975
+
976
+
977
+
978
+ function PanelRoiThresholdSegmentation({
979
+ servicesManager,
980
+ commandsManager
981
+ }) {
982
+ const {
983
+ segmentationService,
984
+ uiNotificationService
985
+ } = servicesManager.services;
986
+ const {
987
+ t
988
+ } = (0,es/* useTranslation */.Bd)('PanelSUVExport');
989
+ const [segmentations, setSegmentations] = (0,react.useState)(() => segmentationService.getSegmentations());
990
+
991
+ /**
992
+ * Update UI based on segmentation changes (added, removed, updated)
993
+ */
994
+ (0,react.useEffect)(() => {
995
+ // ~~ Subscription
996
+ const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
997
+ const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
998
+ const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
999
+ const subscriptions = [];
1000
+ [added, updated, removed].forEach(evt => {
1001
+ const {
1002
+ unsubscribe
1003
+ } = segmentationService.subscribe(evt, () => {
1004
+ const segmentations = segmentationService.getSegmentations();
1005
+ setSegmentations(segmentations);
1006
+ });
1007
+ subscriptions.push(unsubscribe);
1008
+ });
1009
+ return () => {
1010
+ subscriptions.forEach(unsub => {
1011
+ unsub();
1012
+ });
1013
+ };
1014
+ }, []);
1015
+ const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
1016
+ const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
1017
+ segmentations.forEach(segmentation => {
1018
+ const {
1019
+ cachedStats
1020
+ } = segmentation;
1021
+ if (!cachedStats) {
1022
+ return;
1023
+ }
1024
+
1025
+ // segment 1
1026
+ const suvPeak = cachedStats?.['1']?.suvPeak?.suvPeak;
1027
+ if (Number.isNaN(suvPeak)) {
1028
+ uiNotificationService.show({
1029
+ title: 'SUV Peak',
1030
+ message: 'Segmented volume does not allow SUV Peak calculation',
1031
+ type: 'warning'
1032
+ });
1033
+ }
1034
+ });
1035
+ const actions = [{
1036
+ label: 'Export CSV',
1037
+ onClick: () => {
1038
+ commandsManager.runCommand('exportTMTVReportCSV', {
1039
+ segmentations,
1040
+ tmtv: tmtvValue,
1041
+ config
1042
+ });
1043
+ },
1044
+ disabled: tmtvValue === null
1045
+ }, {
1046
+ label: 'Create RT Report',
1047
+ onClick: () => {
1048
+ commandsManager.runCommand('createTMTVRTReport');
1049
+ },
1050
+ disabled: tmtvValue === null
1051
+ }];
1052
+ return /*#__PURE__*/react.createElement(react.Fragment, null, /*#__PURE__*/react.createElement("div", {
1053
+ className: "mt-2 mb-10 flex flex-col"
1054
+ }, /*#__PURE__*/react.createElement("div", {
1055
+ className: "invisible-scrollbar overflow-y-auto overflow-x-hidden"
1056
+ }, tmtvValue !== null ? /*#__PURE__*/react.createElement("div", {
1057
+ className: "bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1"
1058
+ }, /*#__PURE__*/react.createElement("span", {
1059
+ className: "text-base font-bold uppercase tracking-widest text-white"
1060
+ }, 'TMTV:'), /*#__PURE__*/react.createElement("div", {
1061
+ className: "text-white"
1062
+ }, `${tmtvValue} mL`)) : null, /*#__PURE__*/react.createElement("div", {
1063
+ className: "mt-2 flex justify-center"
1064
+ }, /*#__PURE__*/react.createElement(src/* ActionButtons */.wr, {
1065
+ actions: actions,
1066
+ t: t
1067
+ })))), /*#__PURE__*/react.createElement("div", {
1068
+ className: "absolute bottom-1 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80",
1069
+ onClick: () => {
1070
+ // navigate to a url in a new tab
1071
+ window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
1072
+ }
1073
+ }, /*#__PURE__*/react.createElement(src/* Icon */.In, {
1074
+ width: "15px",
1075
+ height: "15px",
1076
+ name: 'info',
1077
+ className: 'text-primary-active ml-4 mr-3'
1078
+ }), /*#__PURE__*/react.createElement("span", null, 'User Guide')));
1079
+ }
1080
+ PanelRoiThresholdSegmentation.propTypes = {
1081
+ commandsManager: prop_types_default().shape({
1082
+ runCommand: (prop_types_default()).func.isRequired
1083
+ }),
1084
+ servicesManager: prop_types_default().shape({
1085
+ services: prop_types_default().shape({
1086
+ segmentationService: prop_types_default().shape({
1087
+ getSegmentation: (prop_types_default()).func.isRequired,
1088
+ getSegmentations: (prop_types_default()).func.isRequired,
1089
+ toggleSegmentationVisibility: (prop_types_default()).func.isRequired,
1090
+ subscribe: (prop_types_default()).func.isRequired,
1091
+ EVENTS: (prop_types_default()).object.isRequired
1092
+ }).isRequired
1093
+ }).isRequired
1094
+ }).isRequired
1095
+ };
1096
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/index.ts
1097
+
1098
+ /* harmony default export */ const PanelROIThresholdSegmentation = (PanelRoiThresholdSegmentation);
1099
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/index.tsx
1100
+
1101
+
1102
+
1103
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/getPanelModule.tsx
1104
+
1105
+
1106
+
1107
+
1108
+ // TODO:
1109
+ // - No loading UI exists yet
1110
+ // - cancel promises when component is destroyed
1111
+ // - show errors in UI for thumbnails if promise fails
1112
+
1113
+ function getPanelModule({
1114
+ commandsManager,
1115
+ extensionManager,
1116
+ servicesManager
1117
+ }) {
1118
+ const wrappedPanelPetSuv = () => {
1119
+ return /*#__PURE__*/react.createElement(PanelPetSUV, {
1120
+ commandsManager: commandsManager,
1121
+ servicesManager: servicesManager
1122
+ });
1123
+ };
1124
+ const wrappedROIThresholdToolbox = () => {
1125
+ return /*#__PURE__*/react.createElement(react.Fragment, null, /*#__PURE__*/react.createElement(src/* Toolbox */.OO, {
1126
+ commandsManager: commandsManager,
1127
+ servicesManager: servicesManager,
1128
+ extensionManager: extensionManager,
1129
+ buttonSectionId: "ROIThresholdToolbox",
1130
+ title: "Threshold Tools"
1131
+ }));
1132
+ };
1133
+ const wrappedROIThresholdExport = () => {
1134
+ return /*#__PURE__*/react.createElement(react.Fragment, null, /*#__PURE__*/react.createElement(PanelROIThresholdSegmentation, {
1135
+ commandsManager: commandsManager,
1136
+ servicesManager: servicesManager
1137
+ }));
1138
+ };
1139
+ return [{
1140
+ name: 'petSUV',
1141
+ iconName: 'tab-patient-info',
1142
+ iconLabel: 'Patient Info',
1143
+ label: 'Patient Info',
1144
+ component: wrappedPanelPetSuv
1145
+ }, {
1146
+ name: 'tmtvBox',
1147
+ iconName: 'tab-segmentation',
1148
+ iconLabel: 'Segmentation',
1149
+ label: 'Segmentation Toolbox',
1150
+ component: wrappedROIThresholdToolbox
1151
+ }, {
1152
+ name: 'tmtvExport',
1153
+ iconName: 'tab-segmentation',
1154
+ iconLabel: 'Segmentation',
1155
+ label: 'Segmentation Export',
1156
+ component: wrappedROIThresholdExport
1157
+ }];
1158
+ }
1159
+ /* harmony default export */ const src_getPanelModule = (getPanelModule);
1160
+ // EXTERNAL MODULE: ../../../node_modules/@cornerstonejs/tools/dist/esm/index.js + 18 modules
1161
+ var esm = __webpack_require__(24542);
1162
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/measurementServiceMappings/constants/supportedTools.js
1163
+ /* harmony default export */ const supportedTools = (['RectangleROIStartEndThreshold']);
1164
+ // EXTERNAL MODULE: ../../../node_modules/@cornerstonejs/core/dist/esm/index.js + 327 modules
1165
+ var dist_esm = __webpack_require__(44656);
1166
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js
1167
+
1168
+ function getSOPInstanceAttributes(imageId) {
1169
+ if (imageId) {
1170
+ return _getUIDFromImageID(imageId);
1171
+ }
1172
+ }
1173
+ function _getUIDFromImageID(imageId) {
1174
+ const instance = dist_esm.metaData.get('instance', imageId);
1175
+ return {
1176
+ SOPInstanceUID: instance.SOPInstanceUID,
1177
+ SeriesInstanceUID: instance.SeriesInstanceUID,
1178
+ StudyInstanceUID: instance.StudyInstanceUID,
1179
+ frameNumber: instance.frameNumber || 1
1180
+ };
1181
+ }
1182
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js
1183
+
1184
+
1185
+ const RectangleROIStartEndThreshold = {
1186
+ toAnnotation: (measurement, definition) => {},
1187
+ /**
1188
+ * Maps cornerstone annotation event data to measurement service format.
1189
+ *
1190
+ * @param {Object} cornerstone Cornerstone event data
1191
+ * @return {Measurement} Measurement instance
1192
+ */
1193
+ toMeasurement: (csToolsEventDetail, displaySetService, cornerstoneViewportService) => {
1194
+ const {
1195
+ annotation,
1196
+ viewportId
1197
+ } = csToolsEventDetail;
1198
+ const {
1199
+ metadata,
1200
+ data,
1201
+ annotationUID
1202
+ } = annotation;
1203
+ if (!metadata || !data) {
1204
+ console.warn('Length tool: Missing metadata or data');
1205
+ return null;
1206
+ }
1207
+ const {
1208
+ toolName,
1209
+ referencedImageId,
1210
+ FrameOfReferenceUID
1211
+ } = metadata;
1212
+ const validToolType = supportedTools.includes(toolName);
1213
+ if (!validToolType) {
1214
+ throw new Error('Tool not supported');
1215
+ }
1216
+ const {
1217
+ SOPInstanceUID,
1218
+ SeriesInstanceUID,
1219
+ StudyInstanceUID
1220
+ } = getSOPInstanceAttributes(referencedImageId, cornerstoneViewportService, viewportId);
1221
+ let displaySet;
1222
+ if (SOPInstanceUID) {
1223
+ displaySet = displaySetService.getDisplaySetForSOPInstanceUID(SOPInstanceUID, SeriesInstanceUID);
1224
+ } else {
1225
+ displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
1226
+ }
1227
+ return {
1228
+ uid: annotationUID,
1229
+ SOPInstanceUID,
1230
+ FrameOfReferenceUID,
1231
+ // points,
1232
+ metadata,
1233
+ referenceSeriesUID: SeriesInstanceUID,
1234
+ referenceStudyUID: StudyInstanceUID,
1235
+ toolName: metadata.toolName,
1236
+ displaySetInstanceUID: displaySet.displaySetInstanceUID,
1237
+ label: metadata.label,
1238
+ data: data.cachedStats,
1239
+ type: 'RectangleROIStartEndThreshold'
1240
+ };
1241
+ }
1242
+ };
1243
+ /* harmony default export */ const measurementServiceMappings_RectangleROIStartEndThreshold = (RectangleROIStartEndThreshold);
1244
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js
1245
+
1246
+ const measurementServiceMappingsFactory = (measurementService, displaySetService, cornerstoneViewportService) => {
1247
+ return {
1248
+ RectangleROIStartEndThreshold: {
1249
+ toAnnotation: measurementServiceMappings_RectangleROIStartEndThreshold.toAnnotation,
1250
+ toMeasurement: csToolsAnnotation => measurementServiceMappings_RectangleROIStartEndThreshold.toMeasurement(csToolsAnnotation, displaySetService, cornerstoneViewportService),
1251
+ matchingCriteria: [{
1252
+ valueType: measurementService.VALUE_TYPES.ROI_THRESHOLD_MANUAL
1253
+ }]
1254
+ }
1255
+ };
1256
+ };
1257
+ /* harmony default export */ const measurementServiceMappings_measurementServiceMappingsFactory = (measurementServiceMappingsFactory);
1258
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/init.js
1259
+
1260
+
1261
+ const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
1262
+ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
1263
+ /**
1264
+ *
1265
+ * @param {Object} servicesManager
1266
+ * @param {Object} configuration
1267
+ * @param {Object|Array} configuration.csToolsConfig
1268
+ */
1269
+ function init({
1270
+ servicesManager
1271
+ }) {
1272
+ const {
1273
+ measurementService,
1274
+ displaySetService,
1275
+ cornerstoneViewportService
1276
+ } = servicesManager.services;
1277
+ (0,esm.addTool)(esm.RectangleROIStartEndThresholdTool);
1278
+ const {
1279
+ RectangleROIStartEndThreshold
1280
+ } = measurementServiceMappings_measurementServiceMappingsFactory(measurementService, displaySetService, cornerstoneViewportService);
1281
+ const csTools3DVer1MeasurementSource = measurementService.getSource(CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION);
1282
+ measurementService.addMapping(csTools3DVer1MeasurementSource, 'RectangleROIStartEndThreshold', RectangleROIStartEndThreshold.matchingCriteria, RectangleROIStartEndThreshold.toAnnotation, RectangleROIStartEndThreshold.toMeasurement);
1283
+ }
1284
+ // EXTERNAL MODULE: ../../../node_modules/gl-matrix/esm/index.js + 1 modules
1285
+ var gl_matrix_esm = __webpack_require__(44753);
1286
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/getThresholdValue.ts
1287
+
1288
+ function getRoiStats(referencedVolume, annotations) {
1289
+ // roiStats
1290
+ const {
1291
+ imageData
1292
+ } = referencedVolume;
1293
+ const values = imageData.getPointData().getScalars().getData();
1294
+
1295
+ // Todo: add support for other strategies
1296
+ const {
1297
+ fn,
1298
+ baseValue
1299
+ } = _getStrategyFn('max');
1300
+ let value = baseValue;
1301
+ const boundsIJK = esm.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations(annotations, referencedVolume);
1302
+ const [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK;
1303
+ for (let i = iMin; i <= iMax; i++) {
1304
+ for (let j = jMin; j <= jMax; j++) {
1305
+ for (let k = kMin; k <= kMax; k++) {
1306
+ const offset = imageData.computeOffsetIndex([i, j, k]);
1307
+ value = fn(values[offset], value);
1308
+ }
1309
+ }
1310
+ }
1311
+ return value;
1312
+ }
1313
+ function getThresholdValues(annotationUIDs, referencedVolumes, config) {
1314
+ if (config.strategy === 'range') {
1315
+ return {
1316
+ ptLower: Number(config.ptLower),
1317
+ ptUpper: Number(config.ptUpper),
1318
+ ctLower: Number(config.ctLower),
1319
+ ctUpper: Number(config.ctUpper)
1320
+ };
1321
+ }
1322
+ const {
1323
+ weight
1324
+ } = config;
1325
+ const annotations = annotationUIDs.map(annotationUID => esm.annotation.state.getAnnotation(annotationUID));
1326
+ const ptValue = getRoiStats(referencedVolumes[0], annotations);
1327
+ return {
1328
+ ctLower: -Infinity,
1329
+ ctUpper: +Infinity,
1330
+ ptLower: weight * ptValue,
1331
+ ptUpper: +Infinity
1332
+ };
1333
+ }
1334
+ function _getStrategyFn(statistic) {
1335
+ const baseValue = -Infinity;
1336
+ const fn = (number, maxValue) => {
1337
+ if (number > maxValue) {
1338
+ maxValue = number;
1339
+ }
1340
+ return maxValue;
1341
+ };
1342
+ return {
1343
+ fn,
1344
+ baseValue
1345
+ };
1346
+ }
1347
+ /* harmony default export */ const getThresholdValue = (getThresholdValues);
1348
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/calculateSUVPeak.ts
1349
+
1350
+
1351
+ /**
1352
+ * This method calculates the SUV peak on a segmented ROI from a reference PET
1353
+ * volume. If a rectangle annotation is provided, the peak is calculated within that
1354
+ * rectangle. Otherwise, the calculation is performed on the entire volume which
1355
+ * will be slower but same result.
1356
+ * @param viewport Viewport to use for the calculation
1357
+ * @param labelmap Labelmap from which the mask is taken
1358
+ * @param referenceVolume PET volume to use for SUV calculation
1359
+ * @param toolData [Optional] list of toolData to use for SUV calculation
1360
+ * @param segmentIndex The index of the segment to use for masking
1361
+ * @returns
1362
+ */
1363
+ function calculateSuvPeak(labelmap, referenceVolume, annotations, segmentIndex = 1) {
1364
+ if (referenceVolume.metadata.Modality !== 'PT') {
1365
+ return;
1366
+ }
1367
+ const labelmapData = labelmap.getScalarData();
1368
+ const referenceVolumeData = referenceVolume.getScalarData();
1369
+ if (labelmapData.length !== referenceVolumeData.length) {
1370
+ throw new Error('labelmap and referenceVolume must have the same number of pixels');
1371
+ }
1372
+ const {
1373
+ dimensions,
1374
+ imageData: labelmapImageData
1375
+ } = labelmap;
1376
+ const {
1377
+ imageData: referenceVolumeImageData
1378
+ } = referenceVolume;
1379
+ let boundsIJK;
1380
+ // Todo: using the first annotation for now
1381
+ if (annotations?.length && annotations[0].data?.cachedStats) {
1382
+ const {
1383
+ projectionPoints
1384
+ } = annotations[0].data.cachedStats;
1385
+ const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now
1386
+
1387
+ const rectangleCornersIJK = pointsToUse.map(world => {
1388
+ const ijk = gl_matrix_esm/* vec3.fromValues */.eR.fromValues(0, 0, 0);
1389
+ referenceVolumeImageData.worldToIndex(world, ijk);
1390
+ return ijk;
1391
+ });
1392
+ boundsIJK = esm.utilities.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions);
1393
+ }
1394
+ let max = 0;
1395
+ let maxIJK = [0, 0, 0];
1396
+ let maxLPS = [0, 0, 0];
1397
+ const callback = ({
1398
+ pointIJK,
1399
+ pointLPS
1400
+ }) => {
1401
+ const offset = referenceVolumeImageData.computeOffsetIndex(pointIJK);
1402
+ const value = labelmapData[offset];
1403
+ if (value !== segmentIndex) {
1404
+ return;
1405
+ }
1406
+ const referenceValue = referenceVolumeData[offset];
1407
+ if (referenceValue > max) {
1408
+ max = referenceValue;
1409
+ maxIJK = pointIJK;
1410
+ maxLPS = pointLPS;
1411
+ }
1412
+ };
1413
+ esm.utilities.pointInShapeCallback(labelmapImageData, () => true, callback, boundsIJK);
1414
+ const direction = labelmapImageData.getDirection().slice(0, 3);
1415
+
1416
+ /**
1417
+ * 2. Find the bottom and top of the great circle for the second sphere (1cc sphere)
1418
+ * V = (4/3)πr3
1419
+ */
1420
+ const radius = Math.pow(1 / (4 / 3 * Math.PI), 1 / 3) * 10;
1421
+ const diameter = radius * 2;
1422
+ const secondaryCircleWorld = gl_matrix_esm/* vec3.create */.eR.create();
1423
+ const bottomWorld = gl_matrix_esm/* vec3.create */.eR.create();
1424
+ const topWorld = gl_matrix_esm/* vec3.create */.eR.create();
1425
+ referenceVolumeImageData.indexToWorld(maxIJK, secondaryCircleWorld);
1426
+ gl_matrix_esm/* vec3.scaleAndAdd */.eR.scaleAndAdd(bottomWorld, secondaryCircleWorld, direction, -diameter / 2);
1427
+ gl_matrix_esm/* vec3.scaleAndAdd */.eR.scaleAndAdd(topWorld, secondaryCircleWorld, direction, diameter / 2);
1428
+ const suvPeakCirclePoints = [bottomWorld, topWorld];
1429
+
1430
+ /**
1431
+ * 3. Find the Mean and Max of the 1cc sphere centered on the suv Max of the previous
1432
+ * sphere
1433
+ */
1434
+ let count = 0;
1435
+ let acc = 0;
1436
+ const suvPeakMeanCallback = ({
1437
+ value
1438
+ }) => {
1439
+ acc += value;
1440
+ count += 1;
1441
+ };
1442
+ esm.utilities.pointInSurroundingSphereCallback(referenceVolumeImageData, suvPeakCirclePoints, suvPeakMeanCallback);
1443
+ const mean = acc / count;
1444
+ return {
1445
+ max,
1446
+ maxIJK,
1447
+ maxLPS,
1448
+ mean
1449
+ };
1450
+ }
1451
+ /* harmony default export */ const calculateSUVPeak = (calculateSuvPeak);
1452
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/calculateTMTV.ts
1453
+
1454
+
1455
+ /**
1456
+ * Given a list of labelmaps (with the possibility of overlapping regions),
1457
+ * and a referenceVolume, it calculates the total metabolic tumor volume (TMTV)
1458
+ * by flattening and rasterizing each segment into a single labelmap and summing
1459
+ * the total number of volume voxels. It should be noted that for this calculation
1460
+ * we do not double count voxels that are part of multiple labelmaps.
1461
+ * @param {} labelmaps
1462
+ * @param {number} segmentIndex
1463
+ * @returns {number} TMTV in ml
1464
+ */
1465
+ function calculateTMTV(labelmaps, segmentIndex = 1) {
1466
+ const volumeId = 'mergedLabelmap';
1467
+ const mergedLabelmap = esm.utilities.segmentation.createMergedLabelmapForIndex(labelmaps, segmentIndex, volumeId);
1468
+ const {
1469
+ imageData,
1470
+ spacing
1471
+ } = mergedLabelmap;
1472
+ const values = imageData.getPointData().getScalars().getData();
1473
+
1474
+ // count non-zero values inside the outputData, this would
1475
+ // consider the overlapping regions to be only counted once
1476
+ const numVoxels = values.reduce((acc, curr) => {
1477
+ if (curr > 0) {
1478
+ return acc + 1;
1479
+ }
1480
+ return acc;
1481
+ }, 0);
1482
+ return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
1483
+ }
1484
+ /* harmony default export */ const utils_calculateTMTV = (calculateTMTV);
1485
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/createAndDownloadTMTVReport.js
1486
+ function createAndDownloadTMTVReport(segReport, additionalReportRows, options = {}) {
1487
+ const firstReport = segReport[Object.keys(segReport)[0]];
1488
+ const columns = Object.keys(firstReport);
1489
+ const csv = [columns.join(',')];
1490
+ Object.values(segReport).forEach(segmentation => {
1491
+ const row = [];
1492
+ columns.forEach(column => {
1493
+ // if it is array then we need to replace , with space to avoid csv parsing error
1494
+ row.push(Array.isArray(segmentation[column]) ? segmentation[column].join(' ') : segmentation[column]);
1495
+ });
1496
+ csv.push(row.join(','));
1497
+ });
1498
+ csv.push('');
1499
+ csv.push('');
1500
+ csv.push('');
1501
+ csv.push(`Patient ID,${firstReport.PatientID}`);
1502
+ csv.push(`Study Date,${firstReport.StudyDate}`);
1503
+ csv.push('');
1504
+ additionalReportRows.forEach(({
1505
+ key,
1506
+ value: values
1507
+ }) => {
1508
+ const temp = [];
1509
+ temp.push(`${key}`);
1510
+ Object.keys(values).forEach(k => {
1511
+ temp.push(`${k}`);
1512
+ temp.push(`${values[k]}`);
1513
+ });
1514
+ csv.push(temp.join(','));
1515
+ });
1516
+ const blob = new Blob([csv.join('\n')], {
1517
+ type: 'text/csv;charset=utf-8'
1518
+ });
1519
+ const url = URL.createObjectURL(blob);
1520
+ const a = document.createElement('a');
1521
+ a.href = url;
1522
+ a.download = options.filename ?? `${firstReport.PatientID}_tmtv.csv`;
1523
+ a.click();
1524
+ }
1525
+ // EXTERNAL MODULE: ../../../node_modules/dcmjs/build/dcmjs.es.js
1526
+ var dcmjs_es = __webpack_require__(31426);
1527
+ // EXTERNAL MODULE: ../../../node_modules/@cornerstonejs/adapters/dist/adapters.es.js
1528
+ var adapters_es = __webpack_require__(83342);
1529
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/dicomRTAnnotationExport.js
1530
+
1531
+
1532
+
1533
+ const {
1534
+ datasetToBlob
1535
+ } = dcmjs_es/* default.data */.Ay.data;
1536
+ const metadataProvider = core_src.classes.MetadataProvider;
1537
+ function dicomRTAnnotationExport(annotations) {
1538
+ const dataset = adapters_es/* adaptersRT */.f_.Cornerstone3D.RTSS.generateRTSSFromAnnotations(annotations, metadataProvider, core_src.DicomMetadataStore);
1539
+ const reportBlob = datasetToBlob(dataset);
1540
+
1541
+ //Create a URL for the binary.
1542
+ var objectUrl = URL.createObjectURL(reportBlob);
1543
+ window.location.assign(objectUrl);
1544
+ }
1545
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/index.js
1546
+
1547
+ /* harmony default export */ const RTStructureSet = (dicomRTAnnotationExport);
1548
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/commandsModule.js
1549
+
1550
+
1551
+
1552
+
1553
+
1554
+
1555
+
1556
+
1557
+
1558
+
1559
+ const commandsModule_metadataProvider = core_src.classes.MetadataProvider;
1560
+ const RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS = ['RectangleROIStartEndThreshold', 'RectangleROIThreshold'];
1561
+ const LABELMAP = esm.Enums.SegmentationRepresentations.Labelmap;
1562
+ const commandsModule = ({
1563
+ servicesManager,
1564
+ commandsManager,
1565
+ extensionManager
1566
+ }) => {
1567
+ const {
1568
+ viewportGridService,
1569
+ uiNotificationService,
1570
+ displaySetService,
1571
+ hangingProtocolService,
1572
+ toolGroupService,
1573
+ cornerstoneViewportService,
1574
+ segmentationService
1575
+ } = servicesManager.services;
1576
+ const utilityModule = extensionManager.getModuleEntry('@ohif/extension-cornerstone.utilityModule.common');
1577
+ const {
1578
+ getEnabledElement
1579
+ } = utilityModule.exports;
1580
+ function _getActiveViewportsEnabledElement() {
1581
+ const {
1582
+ activeViewportId
1583
+ } = viewportGridService.getState();
1584
+ const {
1585
+ element
1586
+ } = getEnabledElement(activeViewportId) || {};
1587
+ const enabledElement = dist_esm.getEnabledElement(element);
1588
+ return enabledElement;
1589
+ }
1590
+ function _getMatchedViewportsToolGroupIds() {
1591
+ const {
1592
+ viewportMatchDetails
1593
+ } = hangingProtocolService.getMatchDetails();
1594
+ const toolGroupIds = [];
1595
+ viewportMatchDetails.forEach(viewport => {
1596
+ const {
1597
+ viewportOptions
1598
+ } = viewport;
1599
+ const {
1600
+ toolGroupId
1601
+ } = viewportOptions;
1602
+ if (toolGroupIds.indexOf(toolGroupId) === -1) {
1603
+ toolGroupIds.push(toolGroupId);
1604
+ }
1605
+ });
1606
+ return toolGroupIds;
1607
+ }
1608
+ function _getAnnotationsSelectedByToolNames(toolNames) {
1609
+ return toolNames.reduce((allAnnotationUIDs, toolName) => {
1610
+ const annotationUIDs = esm.annotation.selection.getAnnotationsSelectedByToolName(toolName);
1611
+ return allAnnotationUIDs.concat(annotationUIDs);
1612
+ }, []);
1613
+ }
1614
+ const actions = {
1615
+ getMatchingPTDisplaySet: ({
1616
+ viewportMatchDetails
1617
+ }) => {
1618
+ // Todo: this is assuming that the hanging protocol has successfully matched
1619
+ // the correct PT. For future, we should have a way to filter out the PTs
1620
+ // that are in the viewer layout (but then we have the problem of the attenuation
1621
+ // corrected PT vs the non-attenuation correct PT)
1622
+
1623
+ let ptDisplaySet = null;
1624
+ for (const [viewportId, viewportDetails] of viewportMatchDetails) {
1625
+ const {
1626
+ displaySetsInfo
1627
+ } = viewportDetails;
1628
+ const displaySets = displaySetsInfo.map(({
1629
+ displaySetInstanceUID
1630
+ }) => displaySetService.getDisplaySetByUID(displaySetInstanceUID));
1631
+ if (!displaySets || displaySets.length === 0) {
1632
+ continue;
1633
+ }
1634
+ ptDisplaySet = displaySets.find(displaySet => displaySet.Modality === 'PT');
1635
+ if (ptDisplaySet) {
1636
+ break;
1637
+ }
1638
+ }
1639
+ return ptDisplaySet;
1640
+ },
1641
+ getPTMetadata: ({
1642
+ ptDisplaySet
1643
+ }) => {
1644
+ const dataSource = extensionManager.getDataSources()[0];
1645
+ const imageIds = dataSource.getImageIdsForDisplaySet(ptDisplaySet);
1646
+ const firstImageId = imageIds[0];
1647
+ const instance = commandsModule_metadataProvider.get('instance', firstImageId);
1648
+ if (instance.Modality !== 'PT') {
1649
+ return;
1650
+ }
1651
+ const metadata = {
1652
+ SeriesTime: instance.SeriesTime,
1653
+ Modality: instance.Modality,
1654
+ PatientSex: instance.PatientSex,
1655
+ PatientWeight: instance.PatientWeight,
1656
+ RadiopharmaceuticalInformationSequence: {
1657
+ RadionuclideTotalDose: instance.RadiopharmaceuticalInformationSequence[0].RadionuclideTotalDose,
1658
+ RadionuclideHalfLife: instance.RadiopharmaceuticalInformationSequence[0].RadionuclideHalfLife,
1659
+ RadiopharmaceuticalStartTime: instance.RadiopharmaceuticalInformationSequence[0].RadiopharmaceuticalStartTime,
1660
+ RadiopharmaceuticalStartDateTime: instance.RadiopharmaceuticalInformationSequence[0].RadiopharmaceuticalStartDateTime
1661
+ }
1662
+ };
1663
+ return metadata;
1664
+ },
1665
+ createNewLabelmapFromPT: async ({
1666
+ label
1667
+ }) => {
1668
+ // Create a segmentation of the same resolution as the source data
1669
+ // using volumeLoader.createAndCacheDerivedVolume.
1670
+ const {
1671
+ viewportMatchDetails
1672
+ } = hangingProtocolService.getMatchDetails();
1673
+ const ptDisplaySet = actions.getMatchingPTDisplaySet({
1674
+ viewportMatchDetails
1675
+ });
1676
+ if (!ptDisplaySet) {
1677
+ uiNotificationService.error('No matching PT display set found');
1678
+ return;
1679
+ }
1680
+ const currentSegmentations = segmentationService.getSegmentations();
1681
+ const segmentationId = await segmentationService.createSegmentationForDisplaySet(ptDisplaySet.displaySetInstanceUID, {
1682
+ label: `Segmentation ${currentSegmentations.length + 1}`
1683
+ });
1684
+
1685
+ // Add Segmentation to all toolGroupIds in the viewer
1686
+ const toolGroupIds = _getMatchedViewportsToolGroupIds();
1687
+ const representationType = LABELMAP;
1688
+ for (const toolGroupId of toolGroupIds) {
1689
+ const hydrateSegmentation = true;
1690
+ await segmentationService.addSegmentationRepresentationToToolGroup(toolGroupId, segmentationId, hydrateSegmentation, representationType);
1691
+ segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
1692
+ }
1693
+ segmentationService.addSegment(segmentationId, {
1694
+ segmentIndex: 1,
1695
+ properties: {
1696
+ label: 'Segment 1'
1697
+ }
1698
+ });
1699
+ return segmentationId;
1700
+ },
1701
+ setSegmentationActiveForToolGroups: ({
1702
+ segmentationId
1703
+ }) => {
1704
+ const toolGroupIds = _getMatchedViewportsToolGroupIds();
1705
+ toolGroupIds.forEach(toolGroupId => {
1706
+ segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
1707
+ });
1708
+ },
1709
+ thresholdSegmentationByRectangleROITool: ({
1710
+ segmentationId,
1711
+ config,
1712
+ segmentIndex
1713
+ }) => {
1714
+ const segmentation = esm.segmentation.state.getSegmentation(segmentationId);
1715
+ const {
1716
+ representationData
1717
+ } = segmentation;
1718
+ const {
1719
+ displaySetMatchDetails: matchDetails
1720
+ } = hangingProtocolService.getMatchDetails();
1721
+ const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
1722
+
1723
+ const ctDisplaySet = matchDetails.get('ctDisplaySet');
1724
+ const ctVolumeId = `${volumeLoaderScheme}:${ctDisplaySet.displaySetInstanceUID}`; // VolumeId with loader id + volume id
1725
+
1726
+ const {
1727
+ volumeId: segVolumeId
1728
+ } = representationData[LABELMAP];
1729
+ const {
1730
+ referencedVolumeId
1731
+ } = dist_esm.cache.getVolume(segVolumeId);
1732
+ const annotationUIDs = _getAnnotationsSelectedByToolNames(RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS);
1733
+ if (annotationUIDs.length === 0) {
1734
+ uiNotificationService.show({
1735
+ title: 'Commands Module',
1736
+ message: 'No ROIThreshold Tool is Selected',
1737
+ type: 'error'
1738
+ });
1739
+ return;
1740
+ }
1741
+ const labelmapVolume = dist_esm.cache.getVolume(segmentationId);
1742
+ let referencedVolume = dist_esm.cache.getVolume(referencedVolumeId);
1743
+ const ctReferencedVolume = dist_esm.cache.getVolume(ctVolumeId);
1744
+
1745
+ // check if viewport is
1746
+
1747
+ if (!referencedVolume) {
1748
+ throw new Error('No Reference volume found');
1749
+ }
1750
+ if (!labelmapVolume) {
1751
+ throw new Error('No Reference labelmap found');
1752
+ }
1753
+ const annotation = esm.annotation.state.getAnnotation(annotationUIDs[0]);
1754
+ const {
1755
+ metadata: {
1756
+ enabledElement: {
1757
+ viewport
1758
+ }
1759
+ }
1760
+ } = annotation;
1761
+ const showingReferenceVolume = viewport.hasVolumeId(referencedVolumeId);
1762
+ if (!showingReferenceVolume) {
1763
+ // if the reference volume is not being displayed, we can't
1764
+ // rely on it for thresholding, we have couple of options here
1765
+ // 1. We choose whatever volume is being displayed
1766
+ // 2. We check if it is a fusion viewport, we pick the volume
1767
+ // that matches the size and dimensions of the labelmap. This might
1768
+ // happen if the 4D PT is converted to a computed volume and displayed
1769
+ // and wants to threshold the labelmap
1770
+ // 3. We throw an error
1771
+ const displaySetInstanceUIDs = viewportGridService.getDisplaySetsUIDsForViewport(viewport.id);
1772
+ displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
1773
+ const volume = dist_esm.cache.getVolumes().find(volume => volume.volumeId.includes(displaySetInstanceUID));
1774
+ if (dist_esm.utilities.isEqual(volume.dimensions, labelmapVolume.dimensions) && dist_esm.utilities.isEqual(volume.spacing, labelmapVolume.spacing)) {
1775
+ referencedVolume = volume;
1776
+ }
1777
+ });
1778
+ }
1779
+ const {
1780
+ ptLower,
1781
+ ptUpper,
1782
+ ctLower,
1783
+ ctUpper
1784
+ } = getThresholdValue(annotationUIDs, [referencedVolume, ctReferencedVolume], config);
1785
+ return esm.utilities.segmentation.rectangleROIThresholdVolumeByRange(annotationUIDs, labelmapVolume, [{
1786
+ volume: referencedVolume,
1787
+ lower: ptLower,
1788
+ upper: ptUpper
1789
+ }, {
1790
+ volume: ctReferencedVolume,
1791
+ lower: ctLower,
1792
+ upper: ctUpper
1793
+ }], {
1794
+ overwrite: true,
1795
+ segmentIndex
1796
+ });
1797
+ },
1798
+ calculateSuvPeak: ({
1799
+ labelmap,
1800
+ segmentIndex
1801
+ }) => {
1802
+ const {
1803
+ referencedVolumeId
1804
+ } = labelmap;
1805
+ const referencedVolume = dist_esm.cache.getVolume(referencedVolumeId);
1806
+ const annotationUIDs = _getAnnotationsSelectedByToolNames(RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS);
1807
+ const annotations = annotationUIDs.map(annotationUID => esm.annotation.state.getAnnotation(annotationUID));
1808
+ const suvPeak = calculateSUVPeak(labelmap, referencedVolume, annotations, segmentIndex);
1809
+ return {
1810
+ suvPeak: suvPeak.mean,
1811
+ suvMax: suvPeak.max,
1812
+ suvMaxIJK: suvPeak.maxIJK,
1813
+ suvMaxLPS: suvPeak.maxLPS
1814
+ };
1815
+ },
1816
+ getLesionStats: ({
1817
+ labelmap,
1818
+ segmentIndex = 1
1819
+ }) => {
1820
+ const {
1821
+ scalarData,
1822
+ spacing
1823
+ } = labelmap;
1824
+ const referencedScalarData = dist_esm.cache.getVolume(labelmap.referencedVolumeId).getScalarData();
1825
+ let segmentationMax = -Infinity;
1826
+ let segmentationMin = Infinity;
1827
+ let segmentationValues = [];
1828
+ let voxelCount = 0;
1829
+ for (let i = 0; i < scalarData.length; i++) {
1830
+ if (scalarData[i] === segmentIndex) {
1831
+ const value = referencedScalarData[i];
1832
+ segmentationValues.push(value);
1833
+ if (value > segmentationMax) {
1834
+ segmentationMax = value;
1835
+ }
1836
+ if (value < segmentationMin) {
1837
+ segmentationMin = value;
1838
+ }
1839
+ voxelCount++;
1840
+ }
1841
+ }
1842
+ const stats = {
1843
+ minValue: segmentationMin,
1844
+ maxValue: segmentationMax,
1845
+ meanValue: segmentationValues.reduce((a, b) => a + b, 0) / voxelCount,
1846
+ stdValue: Math.sqrt(segmentationValues.reduce((a, b) => a + b * b, 0) / voxelCount - segmentationValues.reduce((a, b) => a + b, 0) / voxelCount ** 2),
1847
+ volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3
1848
+ };
1849
+ return stats;
1850
+ },
1851
+ calculateLesionGlycolysis: ({
1852
+ lesionStats
1853
+ }) => {
1854
+ const {
1855
+ meanValue,
1856
+ volume
1857
+ } = lesionStats;
1858
+ return {
1859
+ lesionGlyoclysisStats: volume * meanValue
1860
+ };
1861
+ },
1862
+ calculateTMTV: ({
1863
+ segmentations
1864
+ }) => {
1865
+ const labelmaps = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
1866
+ if (!labelmaps.length) {
1867
+ return;
1868
+ }
1869
+ return utils_calculateTMTV(labelmaps);
1870
+ },
1871
+ exportTMTVReportCSV: ({
1872
+ segmentations,
1873
+ tmtv,
1874
+ config,
1875
+ options
1876
+ }) => {
1877
+ const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
1878
+ segmentations
1879
+ });
1880
+ const tlg = actions.getTotalLesionGlycolysis({
1881
+ segmentations
1882
+ });
1883
+ const additionalReportRows = [{
1884
+ key: 'Total Lesion Glycolysis',
1885
+ value: {
1886
+ tlg: tlg.toFixed(4)
1887
+ }
1888
+ }, {
1889
+ key: 'Threshold Configuration',
1890
+ value: {
1891
+ ...config
1892
+ }
1893
+ }];
1894
+ if (tmtv !== undefined) {
1895
+ additionalReportRows.unshift({
1896
+ key: 'Total Metabolic Tumor Volume',
1897
+ value: {
1898
+ tmtv
1899
+ }
1900
+ });
1901
+ }
1902
+ createAndDownloadTMTVReport(segReport, additionalReportRows, options);
1903
+ },
1904
+ getTotalLesionGlycolysis: ({
1905
+ segmentations
1906
+ }) => {
1907
+ const labelmapVolumes = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
1908
+ let mergedLabelmap;
1909
+ // merge labelmap will through an error if labels maps are not the same size
1910
+ // or same direction or ....
1911
+ try {
1912
+ mergedLabelmap = esm.utilities.segmentation.createMergedLabelmapForIndex(labelmapVolumes);
1913
+ } catch (e) {
1914
+ console.error('commandsModule::getTotalLesionGlycolysis', e);
1915
+ return;
1916
+ }
1917
+
1918
+ // grabbing the first labelmap referenceVolume since it will be the same for all
1919
+ const {
1920
+ referencedVolumeId,
1921
+ spacing
1922
+ } = labelmapVolumes[0];
1923
+ if (!referencedVolumeId) {
1924
+ console.error('commandsModule::getTotalLesionGlycolysis:No referencedVolumeId found');
1925
+ }
1926
+ const ptVolume = dist_esm.cache.getVolume(referencedVolumeId);
1927
+ const mergedLabelData = mergedLabelmap.getScalarData();
1928
+ if (mergedLabelData.length !== ptVolume.getScalarData().length) {
1929
+ console.error('commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size');
1930
+ }
1931
+ let suv = 0;
1932
+ let totalLesionVoxelCount = 0;
1933
+ for (let i = 0; i < mergedLabelData.length; i++) {
1934
+ // if not background
1935
+ if (mergedLabelData[i] !== 0) {
1936
+ suv += ptVolume.getScalarData()[i];
1937
+ totalLesionVoxelCount += 1;
1938
+ }
1939
+ }
1940
+
1941
+ // Average SUV for the merged labelmap
1942
+ const averageSuv = suv / totalLesionVoxelCount;
1943
+
1944
+ // total Lesion Glycolysis [suv * ml]
1945
+ return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3;
1946
+ },
1947
+ setStartSliceForROIThresholdTool: () => {
1948
+ const {
1949
+ viewport
1950
+ } = _getActiveViewportsEnabledElement();
1951
+ const {
1952
+ focalPoint,
1953
+ viewPlaneNormal
1954
+ } = viewport.getCamera();
1955
+ const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames(RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS);
1956
+ const annotationUID = selectedAnnotationUIDs[0];
1957
+ const annotation = esm.annotation.state.getAnnotation(annotationUID);
1958
+ const {
1959
+ handles
1960
+ } = annotation.data;
1961
+ const {
1962
+ points
1963
+ } = handles;
1964
+
1965
+ // get the current slice Index
1966
+ const sliceIndex = viewport.getCurrentImageIdIndex();
1967
+ annotation.data.startSlice = sliceIndex;
1968
+
1969
+ // distance between camera focal point and each point on the rectangle
1970
+ const newPoints = points.map(point => {
1971
+ const distance = gl_matrix_esm/* vec3.create */.eR.create();
1972
+ gl_matrix_esm/* vec3.subtract */.eR.subtract(distance, focalPoint, point);
1973
+ // distance in the direction of the viewPlaneNormal
1974
+ const distanceInViewPlane = gl_matrix_esm/* vec3.dot */.eR.dot(distance, viewPlaneNormal);
1975
+ // new point is current point minus distanceInViewPlane
1976
+ const newPoint = gl_matrix_esm/* vec3.create */.eR.create();
1977
+ gl_matrix_esm/* vec3.scaleAndAdd */.eR.scaleAndAdd(newPoint, point, viewPlaneNormal, distanceInViewPlane);
1978
+ return newPoint;
1979
+ //
1980
+ });
1981
+ handles.points = newPoints;
1982
+ // IMPORTANT: invalidate the toolData for the cached stat to get updated
1983
+ // and re-calculate the projection points
1984
+ annotation.invalidated = true;
1985
+ viewport.render();
1986
+ },
1987
+ setEndSliceForROIThresholdTool: () => {
1988
+ const {
1989
+ viewport
1990
+ } = _getActiveViewportsEnabledElement();
1991
+ const selectedAnnotationUIDs = _getAnnotationsSelectedByToolNames(RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS);
1992
+ const annotationUID = selectedAnnotationUIDs[0];
1993
+ const annotation = esm.annotation.state.getAnnotation(annotationUID);
1994
+
1995
+ // get the current slice Index
1996
+ const sliceIndex = viewport.getCurrentImageIdIndex();
1997
+ annotation.data.endSlice = sliceIndex;
1998
+
1999
+ // IMPORTANT: invalidate the toolData for the cached stat to get updated
2000
+ // and re-calculate the projection points
2001
+ annotation.invalidated = true;
2002
+ viewport.render();
2003
+ },
2004
+ createTMTVRTReport: () => {
2005
+ // get all Rectangle ROI annotation
2006
+ const stateManager = esm.annotation.state.getAnnotationManager();
2007
+ const annotations = [];
2008
+ Object.keys(stateManager.annotations).forEach(frameOfReferenceUID => {
2009
+ const forAnnotations = stateManager.annotations[frameOfReferenceUID];
2010
+ const ROIAnnotations = RECTANGLE_ROI_THRESHOLD_MANUAL_TOOL_IDS.reduce((annotations, toolName) => [...annotations, ...(forAnnotations[toolName] ?? [])], []);
2011
+ annotations.push(...ROIAnnotations);
2012
+ });
2013
+ commandsManager.runCommand('exportRTReportForAnnotations', {
2014
+ annotations
2015
+ });
2016
+ },
2017
+ getSegmentationCSVReport: ({
2018
+ segmentations
2019
+ }) => {
2020
+ if (!segmentations || !segmentations.length) {
2021
+ segmentations = segmentationService.getSegmentations();
2022
+ }
2023
+ let report = {};
2024
+ for (const segmentation of segmentations) {
2025
+ const {
2026
+ id,
2027
+ label,
2028
+ cachedStats: data
2029
+ } = segmentation;
2030
+ const segReport = {
2031
+ id,
2032
+ label
2033
+ };
2034
+ if (!data) {
2035
+ report[id] = segReport;
2036
+ continue;
2037
+ }
2038
+ Object.keys(data).forEach(key => {
2039
+ if (typeof data[key] !== 'object') {
2040
+ segReport[key] = data[key];
2041
+ } else {
2042
+ Object.keys(data[key]).forEach(subKey => {
2043
+ const newKey = `${key}_${subKey}`;
2044
+ segReport[newKey] = data[key][subKey];
2045
+ });
2046
+ }
2047
+ });
2048
+ const labelmapVolume = segmentationService.getLabelmapVolume(id);
2049
+ if (!labelmapVolume) {
2050
+ report[id] = segReport;
2051
+ continue;
2052
+ }
2053
+ const referencedVolumeId = labelmapVolume.referencedVolumeId;
2054
+ segReport.referencedVolumeId = referencedVolumeId;
2055
+ const referencedVolume = segmentationService.getLabelmapVolume(referencedVolumeId);
2056
+ if (!referencedVolume) {
2057
+ report[id] = segReport;
2058
+ continue;
2059
+ }
2060
+ if (!referencedVolume.imageIds || !referencedVolume.imageIds.length) {
2061
+ report[id] = segReport;
2062
+ continue;
2063
+ }
2064
+ const firstImageId = referencedVolume.imageIds[0];
2065
+ const instance = core_src/* default.classes */.Ay.classes.MetadataProvider.get('instance', firstImageId);
2066
+ if (!instance) {
2067
+ report[id] = segReport;
2068
+ continue;
2069
+ }
2070
+ report[id] = {
2071
+ ...segReport,
2072
+ PatientID: instance.PatientID ?? '000000',
2073
+ PatientName: instance.PatientName.Alphabetic,
2074
+ StudyInstanceUID: instance.StudyInstanceUID,
2075
+ SeriesInstanceUID: instance.SeriesInstanceUID,
2076
+ StudyDate: instance.StudyDate
2077
+ };
2078
+ }
2079
+ return report;
2080
+ },
2081
+ exportRTReportForAnnotations: ({
2082
+ annotations
2083
+ }) => {
2084
+ RTStructureSet(annotations);
2085
+ },
2086
+ setFusionPTColormap: ({
2087
+ toolGroupId,
2088
+ colormap
2089
+ }) => {
2090
+ const toolGroup = toolGroupService.getToolGroup(toolGroupId);
2091
+ const {
2092
+ viewportMatchDetails
2093
+ } = hangingProtocolService.getMatchDetails();
2094
+ const ptDisplaySet = actions.getMatchingPTDisplaySet({
2095
+ viewportMatchDetails
2096
+ });
2097
+ if (!ptDisplaySet) {
2098
+ return;
2099
+ }
2100
+ const fusionViewportIds = toolGroup.getViewportIds();
2101
+ let viewports = [];
2102
+ fusionViewportIds.forEach(viewportId => {
2103
+ commandsManager.runCommand('setViewportColormap', {
2104
+ viewportId,
2105
+ displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID,
2106
+ colormap: {
2107
+ name: colormap
2108
+ }
2109
+ });
2110
+ viewports.push(cornerstoneViewportService.getCornerstoneViewport(viewportId));
2111
+ });
2112
+ viewports.forEach(viewport => {
2113
+ viewport.render();
2114
+ });
2115
+ }
2116
+ };
2117
+ const definitions = {
2118
+ setEndSliceForROIThresholdTool: {
2119
+ commandFn: actions.setEndSliceForROIThresholdTool
2120
+ },
2121
+ setStartSliceForROIThresholdTool: {
2122
+ commandFn: actions.setStartSliceForROIThresholdTool
2123
+ },
2124
+ getMatchingPTDisplaySet: {
2125
+ commandFn: actions.getMatchingPTDisplaySet
2126
+ },
2127
+ getPTMetadata: {
2128
+ commandFn: actions.getPTMetadata
2129
+ },
2130
+ createNewLabelmapFromPT: {
2131
+ commandFn: actions.createNewLabelmapFromPT
2132
+ },
2133
+ setSegmentationActiveForToolGroups: {
2134
+ commandFn: actions.setSegmentationActiveForToolGroups
2135
+ },
2136
+ thresholdSegmentationByRectangleROITool: {
2137
+ commandFn: actions.thresholdSegmentationByRectangleROITool
2138
+ },
2139
+ getTotalLesionGlycolysis: {
2140
+ commandFn: actions.getTotalLesionGlycolysis
2141
+ },
2142
+ calculateSuvPeak: {
2143
+ commandFn: actions.calculateSuvPeak
2144
+ },
2145
+ getLesionStats: {
2146
+ commandFn: actions.getLesionStats
2147
+ },
2148
+ calculateTMTV: {
2149
+ commandFn: actions.calculateTMTV
2150
+ },
2151
+ exportTMTVReportCSV: {
2152
+ commandFn: actions.exportTMTVReportCSV
2153
+ },
2154
+ createTMTVRTReport: {
2155
+ commandFn: actions.createTMTVRTReport
2156
+ },
2157
+ getSegmentationCSVReport: {
2158
+ commandFn: actions.getSegmentationCSVReport
2159
+ },
2160
+ exportRTReportForAnnotations: {
2161
+ commandFn: actions.exportRTReportForAnnotations
2162
+ },
2163
+ setFusionPTColormap: {
2164
+ commandFn: actions.setFusionPTColormap
2165
+ }
2166
+ };
2167
+ return {
2168
+ actions,
2169
+ definitions,
2170
+ defaultContext: 'TMTV:CORNERSTONE'
2171
+ };
2172
+ };
2173
+ /* harmony default export */ const src_commandsModule = (commandsModule);
2174
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx
2175
+
2176
+
2177
+
2178
+ const ROI_STAT = 'roi_stat';
2179
+ const RANGE = 'range';
2180
+ const options = [{
2181
+ value: ROI_STAT,
2182
+ label: 'Max',
2183
+ placeHolder: 'Max'
2184
+ }, {
2185
+ value: RANGE,
2186
+ label: 'Range',
2187
+ placeHolder: 'Range'
2188
+ }];
2189
+ function ROIThresholdConfiguration({
2190
+ config,
2191
+ dispatch,
2192
+ runCommand
2193
+ }) {
2194
+ const {
2195
+ t
2196
+ } = (0,es/* useTranslation */.Bd)('ROIThresholdConfiguration');
2197
+ return /*#__PURE__*/react.createElement("div", {
2198
+ className: "bg-primary-dark flex flex-col space-y-4"
2199
+ }, /*#__PURE__*/react.createElement("div", {
2200
+ className: "flex items-end space-x-2"
2201
+ }, /*#__PURE__*/react.createElement("div", {
2202
+ className: "flex w-1/2 flex-col"
2203
+ }, /*#__PURE__*/react.createElement(src/* Select */.l6, {
2204
+ label: t('Strategy'),
2205
+ closeMenuOnSelect: true,
2206
+ className: "border-primary-main mr-2 bg-black text-white ",
2207
+ options: options,
2208
+ placeholder: options.find(option => option.value === config.strategy).placeHolder,
2209
+ value: config.strategy,
2210
+ onChange: ({
2211
+ value
2212
+ }) => {
2213
+ dispatch({
2214
+ type: 'setStrategy',
2215
+ payload: {
2216
+ strategy: value
2217
+ }
2218
+ });
2219
+ }
2220
+ })), /*#__PURE__*/react.createElement("div", {
2221
+ className: "w-1/2"
2222
+ }, /*#__PURE__*/react.createElement(src/* LegacyButtonGroup */.xA, null, /*#__PURE__*/react.createElement(src/* LegacyButton */._H, {
2223
+ size: "initial",
2224
+ className: "px-2 py-2 text-base text-white",
2225
+ color: "primaryLight",
2226
+ variant: "outlined",
2227
+ onClick: () => runCommand('setStartSliceForROIThresholdTool')
2228
+ }, t('Start')), /*#__PURE__*/react.createElement(src/* LegacyButton */._H, {
2229
+ size: "initial",
2230
+ color: "primaryLight",
2231
+ variant: "outlined",
2232
+ className: "px-2 py-2 text-base text-white",
2233
+ onClick: () => runCommand('setEndSliceForROIThresholdTool')
2234
+ }, t('End'))))), config.strategy === ROI_STAT && /*#__PURE__*/react.createElement(src/* Input */.pd, {
2235
+ label: t('Percentage of Max SUV'),
2236
+ labelClassName: "text-[13px] font-inter text-white",
2237
+ className: "border-primary-main bg-black",
2238
+ type: "text",
2239
+ containerClassName: "mr-2",
2240
+ value: config.weight,
2241
+ onChange: e => {
2242
+ dispatch({
2243
+ type: 'setWeight',
2244
+ payload: {
2245
+ weight: e.target.value
2246
+ }
2247
+ });
2248
+ }
2249
+ }), config.strategy !== ROI_STAT && /*#__PURE__*/react.createElement("div", {
2250
+ className: "mr-2 text-sm"
2251
+ }, /*#__PURE__*/react.createElement("table", null, /*#__PURE__*/react.createElement("tbody", null, /*#__PURE__*/react.createElement("tr", {
2252
+ className: "mt-2"
2253
+ }, /*#__PURE__*/react.createElement("td", {
2254
+ className: "pr-4",
2255
+ colSpan: "3"
2256
+ }, /*#__PURE__*/react.createElement(src/* Label */.JU, {
2257
+ className: "font-inter text-[13px] text-white",
2258
+ text: "Lower & Upper Ranges"
2259
+ }))), /*#__PURE__*/react.createElement("tr", {
2260
+ className: "mt-2"
2261
+ }, /*#__PURE__*/react.createElement("td", {
2262
+ className: "pr-4 pt-2 text-center"
2263
+ }, /*#__PURE__*/react.createElement(src/* Label */.JU, {
2264
+ className: "text-white",
2265
+ text: "CT"
2266
+ })), /*#__PURE__*/react.createElement("td", null, /*#__PURE__*/react.createElement("div", {
2267
+ className: "flex justify-between"
2268
+ }, /*#__PURE__*/react.createElement(src/* Input */.pd, {
2269
+ label: t(''),
2270
+ labelClassName: "text-white",
2271
+ className: "border-primary-main mt-2 bg-black",
2272
+ type: "text",
2273
+ containerClassName: "mr-2",
2274
+ value: config.ctLower,
2275
+ onChange: e => {
2276
+ dispatch({
2277
+ type: 'setThreshold',
2278
+ payload: {
2279
+ ctLower: e.target.value
2280
+ }
2281
+ });
2282
+ }
2283
+ }), /*#__PURE__*/react.createElement(src/* Input */.pd, {
2284
+ label: t(''),
2285
+ labelClassName: "text-white",
2286
+ className: "border-primary-main mt-2 bg-black",
2287
+ type: "text",
2288
+ containerClassName: "mr-2",
2289
+ value: config.ctUpper,
2290
+ onChange: e => {
2291
+ dispatch({
2292
+ type: 'setThreshold',
2293
+ payload: {
2294
+ ctUpper: e.target.value
2295
+ }
2296
+ });
2297
+ }
2298
+ })))), /*#__PURE__*/react.createElement("tr", null, /*#__PURE__*/react.createElement("td", {
2299
+ className: "pr-4 pt-2 text-center"
2300
+ }, /*#__PURE__*/react.createElement(src/* Label */.JU, {
2301
+ className: "text-white",
2302
+ text: "PT"
2303
+ })), /*#__PURE__*/react.createElement("td", null, /*#__PURE__*/react.createElement("div", {
2304
+ className: "flex justify-between"
2305
+ }, /*#__PURE__*/react.createElement(src/* Input */.pd, {
2306
+ label: t(''),
2307
+ labelClassName: "text-white",
2308
+ className: "border-primary-main mt-2 bg-black",
2309
+ type: "text",
2310
+ containerClassName: "mr-2",
2311
+ value: config.ptLower,
2312
+ onChange: e => {
2313
+ dispatch({
2314
+ type: 'setThreshold',
2315
+ payload: {
2316
+ ptLower: e.target.value
2317
+ }
2318
+ });
2319
+ }
2320
+ }), /*#__PURE__*/react.createElement(src/* Input */.pd, {
2321
+ label: t(''),
2322
+ labelClassName: "text-white",
2323
+ className: "border-primary-main mt-2 bg-black",
2324
+ type: "text",
2325
+ containerClassName: "mr-2",
2326
+ value: config.ptUpper,
2327
+ onChange: e => {
2328
+ dispatch({
2329
+ type: 'setThreshold',
2330
+ payload: {
2331
+ ptUpper: e.target.value
2332
+ }
2333
+ });
2334
+ }
2335
+ }))))))));
2336
+ }
2337
+ /* harmony default export */ const PanelROIThresholdSegmentation_ROIThresholdConfiguration = (ROIThresholdConfiguration);
2338
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/RectangleROIOptions.tsx
2339
+
2340
+
2341
+
2342
+
2343
+ const LOWER_CT_THRESHOLD_DEFAULT = -1024;
2344
+ const UPPER_CT_THRESHOLD_DEFAULT = 1024;
2345
+ const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
2346
+ const UPPER_PT_THRESHOLD_DEFAULT = 100;
2347
+ const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
2348
+ const DEFAULT_STRATEGY = ROI_STAT;
2349
+ function reducer(state, action) {
2350
+ const {
2351
+ payload
2352
+ } = action;
2353
+ const {
2354
+ strategy,
2355
+ ctLower,
2356
+ ctUpper,
2357
+ ptLower,
2358
+ ptUpper,
2359
+ weight
2360
+ } = payload;
2361
+ switch (action.type) {
2362
+ case 'setStrategy':
2363
+ return {
2364
+ ...state,
2365
+ strategy
2366
+ };
2367
+ case 'setThreshold':
2368
+ return {
2369
+ ...state,
2370
+ ctLower: ctLower ? ctLower : state.ctLower,
2371
+ ctUpper: ctUpper ? ctUpper : state.ctUpper,
2372
+ ptLower: ptLower ? ptLower : state.ptLower,
2373
+ ptUpper: ptUpper ? ptUpper : state.ptUpper
2374
+ };
2375
+ case 'setWeight':
2376
+ return {
2377
+ ...state,
2378
+ weight
2379
+ };
2380
+ default:
2381
+ return state;
2382
+ }
2383
+ }
2384
+ function RectangleROIOptions({
2385
+ servicesManager,
2386
+ commandsManager
2387
+ }) {
2388
+ const {
2389
+ segmentationService
2390
+ } = servicesManager.services;
2391
+ const [selectedSegmentationId, setSelectedSegmentationId] = (0,react.useState)(null);
2392
+ const runCommand = (0,react.useCallback)((commandName, commandOptions = {}) => {
2393
+ return commandsManager.runCommand(commandName, commandOptions);
2394
+ }, [commandsManager]);
2395
+ const [config, dispatch] = (0,react.useReducer)(reducer, {
2396
+ strategy: DEFAULT_STRATEGY,
2397
+ ctLower: LOWER_CT_THRESHOLD_DEFAULT,
2398
+ ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
2399
+ ptLower: LOWER_PT_THRESHOLD_DEFAULT,
2400
+ ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
2401
+ weight: WEIGHT_DEFAULT
2402
+ });
2403
+ const handleROIThresholding = (0,react.useCallback)(() => {
2404
+ const segmentationId = selectedSegmentationId;
2405
+ const segmentation = segmentationService.getSegmentation(segmentationId);
2406
+ const activeSegmentIndex = esm.segmentation.segmentIndex.getActiveSegmentIndex(segmentationId);
2407
+
2408
+ // run the threshold based on the active segment index
2409
+ // Todo: later find a way to associate each rectangle with a segment (e.g., maybe with color?)
2410
+ const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
2411
+ segmentationId,
2412
+ config,
2413
+ segmentIndex: activeSegmentIndex
2414
+ });
2415
+
2416
+ // re-calculating the cached stats for the active segmentation
2417
+ const updatedPerSegmentCachedStats = {};
2418
+ segmentation.segments = segmentation.segments.map(segment => {
2419
+ if (!segment || !segment.segmentIndex) {
2420
+ return segment;
2421
+ }
2422
+ const segmentIndex = segment.segmentIndex;
2423
+ const lesionStats = runCommand('getLesionStats', {
2424
+ labelmap,
2425
+ segmentIndex
2426
+ });
2427
+ const suvPeak = runCommand('calculateSuvPeak', {
2428
+ labelmap,
2429
+ segmentIndex
2430
+ });
2431
+ const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
2432
+
2433
+ // update segDetails with the suv peak for the active segmentation
2434
+ const cachedStats = {
2435
+ lesionStats,
2436
+ suvPeak,
2437
+ lesionGlyoclysisStats
2438
+ };
2439
+ segment.cachedStats = cachedStats;
2440
+ segment.displayText = [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`, `Volume: ${lesionStats.volume.toFixed(2)} mm3`];
2441
+ updatedPerSegmentCachedStats[segmentIndex] = cachedStats;
2442
+ return segment;
2443
+ });
2444
+ const notYetUpdatedAtSource = true;
2445
+ const segmentations = segmentationService.getSegmentations();
2446
+ const tmtv = runCommand('calculateTMTV', {
2447
+ segmentations
2448
+ });
2449
+ segmentation.cachedStats = Object.assign(segmentation.cachedStats, updatedPerSegmentCachedStats, {
2450
+ tmtv: {
2451
+ value: tmtv.toFixed(3),
2452
+ config: {
2453
+ ...config
2454
+ }
2455
+ }
2456
+ });
2457
+ segmentationService.addOrUpdateSegmentation({
2458
+ ...segmentation
2459
+ }, false,
2460
+ // don't suppress events
2461
+ notYetUpdatedAtSource);
2462
+ }, [selectedSegmentationId, config]);
2463
+ (0,react.useEffect)(() => {
2464
+ const segmentations = segmentationService.getSegmentations();
2465
+ if (!segmentations.length) {
2466
+ return;
2467
+ }
2468
+ const isActive = segmentations.find(seg => seg.isActive);
2469
+ setSelectedSegmentationId(isActive.id);
2470
+ }, []);
2471
+
2472
+ /**
2473
+ * Update UI based on segmentation changes (added, removed, updated)
2474
+ */
2475
+ (0,react.useEffect)(() => {
2476
+ // ~~ Subscription
2477
+ const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
2478
+ const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
2479
+ const subscriptions = [];
2480
+ [added, updated].forEach(evt => {
2481
+ const {
2482
+ unsubscribe
2483
+ } = segmentationService.subscribe(evt, () => {
2484
+ const segmentations = segmentationService.getSegmentations();
2485
+ if (!segmentations.length) {
2486
+ return;
2487
+ }
2488
+ const isActive = segmentations.find(seg => seg.isActive);
2489
+ setSelectedSegmentationId(isActive.id);
2490
+ });
2491
+ subscriptions.push(unsubscribe);
2492
+ });
2493
+ return () => {
2494
+ subscriptions.forEach(unsub => {
2495
+ unsub();
2496
+ });
2497
+ };
2498
+ }, []);
2499
+ (0,react.useEffect)(() => {
2500
+ const {
2501
+ unsubscribe
2502
+ } = segmentationService.subscribe(segmentationService.EVENTS.SEGMENTATION_REMOVED, () => {
2503
+ const segmentations = segmentationService.getSegmentations();
2504
+ if (segmentations.length > 0) {
2505
+ setSelectedSegmentationId(segmentations[0].id);
2506
+ handleROIThresholding();
2507
+ } else {
2508
+ setSelectedSegmentationId(null);
2509
+ handleROIThresholding();
2510
+ }
2511
+ });
2512
+ return () => {
2513
+ unsubscribe();
2514
+ };
2515
+ }, []);
2516
+ return /*#__PURE__*/react.createElement("div", {
2517
+ className: "invisible-scrollbar mb-2 flex flex-col overflow-y-auto overflow-x-hidden"
2518
+ }, /*#__PURE__*/react.createElement(PanelROIThresholdSegmentation_ROIThresholdConfiguration, {
2519
+ config: config,
2520
+ dispatch: dispatch,
2521
+ runCommand: runCommand
2522
+ }), selectedSegmentationId !== null && /*#__PURE__*/react.createElement(src/* Button */.$n, {
2523
+ className: "mt-2 !h-[26px] !w-[75px]",
2524
+ onClick: handleROIThresholding
2525
+ }, "Run"));
2526
+ }
2527
+ /* harmony default export */ const Panels_RectangleROIOptions = (RectangleROIOptions);
2528
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/getToolbarModule.tsx
2529
+
2530
+ function getToolbarModule({
2531
+ commandsManager,
2532
+ servicesManager
2533
+ }) {
2534
+ return [{
2535
+ name: 'tmtv.RectangleROIThresholdOptions',
2536
+ defaultComponent: () => Panels_RectangleROIOptions({
2537
+ commandsManager,
2538
+ servicesManager
2539
+ })
2540
+ }];
2541
+ }
2542
+ ;// CONCATENATED MODULE: ../../../extensions/tmtv/src/index.tsx
2543
+
2544
+
2545
+
2546
+
2547
+
2548
+
2549
+
2550
+ /**
2551
+ *
2552
+ */
2553
+ const tmtvExtension = {
2554
+ /**
2555
+ * Only required property. Should be a unique value across all extensions.
2556
+ */
2557
+ id: id,
2558
+ preRegistration({
2559
+ servicesManager,
2560
+ commandsManager,
2561
+ extensionManager,
2562
+ configuration = {}
2563
+ }) {
2564
+ init({
2565
+ servicesManager,
2566
+ commandsManager,
2567
+ extensionManager,
2568
+ configuration
2569
+ });
2570
+ },
2571
+ getToolbarModule: getToolbarModule,
2572
+ getPanelModule: src_getPanelModule,
2573
+ getHangingProtocolModule: src_getHangingProtocolModule,
2574
+ getCommandsModule({
2575
+ servicesManager,
2576
+ commandsManager,
2577
+ extensionManager
2578
+ }) {
2579
+ return src_commandsModule({
2580
+ servicesManager,
2581
+ commandsManager,
2582
+ extensionManager
2583
+ });
2584
+ }
2585
+ };
2586
+ /* harmony default export */ const tmtv_src = (tmtvExtension);
2587
+
2588
+ /***/ })
2589
+
2590
+ }]);