@ohif/app 3.8.0-beta.7 → 3.8.0-beta.71
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.
- package/dist/{220.bundle.f7e1c96c94245e70f2be.js → 109.bundle.b4fee2a22b622839baf5.js} +4466 -3715
- package/dist/{471.bundle.49c8d281adbae4a2c4df.js → 121.bundle.21827fec690c01ee9ab3.js} +85 -112
- package/dist/{19.bundle.e5579df6d7b74af50b1d.js → 155.bundle.091ace1591aff1f6b679.js} +334 -287
- package/dist/{687.bundle.9d0330ea5d61fe3117da.js → 164.bundle.fcc94cd4e142a409769d.js} +22 -38
- package/dist/17dd54813d5acc10bf8f.wasm +0 -0
- package/dist/{506.bundle.ab8226d3d81abe874544.js → 188.bundle.0081530bd886a18676eb.js} +23 -28
- package/dist/191.bundle.ef35ed1f90a988b3952b.js +30360 -0
- package/dist/{221.bundle.c2dc03d8fa4235dc1285.js → 2.bundle.ab8937194aad592bc7b4.js} +351 -546
- package/dist/20fc4c659b85ccd2a9c0.wasm +0 -0
- package/dist/290.bundle.952de53057f98e2c5ef0.js +8883 -0
- package/dist/{451.bundle.57c21db5d003c75e9d61.js → 295.bundle.3a0d5062d65296c4bf5d.js} +102 -127
- package/dist/{125.bundle.253395f320b72180da63.js → 297.bundle.194d8985ab974839b5b6.js} +7 -8
- package/dist/{202.bundle.d3490836f71e001dd30f.js → 342.bundle.9be178d7555a64de203a.js} +544 -860
- package/dist/41.bundle.b5a6c70f88cf565cad3e.js +874 -0
- package/dist/425.bundle.e44cfce041ba5209a878.js +2957 -0
- package/dist/425.css +2 -0
- package/dist/{126.bundle.42df2dafc9c0310da188.js → 448.bundle.599d81471e1d7f7962bc.js} +361 -427
- package/dist/{957.bundle.9ea4506963ef8b2d84ba.js → 504.bundle.5ccd6d4269fa77a0a7e7.js} +14338 -27291
- package/dist/{886.bundle.c8dd3ecc42a4253de278.js → 530.bundle.566bfd08dccb4cf6d98b.js} +75 -105
- package/dist/{250.bundle.aea3335667054bdefe36.js → 544.bundle.1110b24e96863d719a95.js} +39 -56
- package/dist/{663.bundle.9f359963019cd8ccf8f9.js → 559.bundle.fb8ac10c41eb734e2f3d.js} +151 -147
- package/dist/{181.bundle.a62b9f0ec692299acb35.js → 574.bundle.b262cbe9f2afd7275271.js} +1286 -307
- package/dist/{181.css → 574.css} +1 -1
- package/dist/{410.bundle.38c9d3820e152e89288e.js → 594.bundle.b70ca7a91d85ebd5d8c4.js} +183 -221
- package/dist/{776.bundle.004382036bdbd8ee2b95.js → 595.bundle.c25147a450c67defb3d5.js} +3157 -1029
- package/dist/{774.bundle.4b2dc46a35012b898e1a.js → 644.bundle.1e77691d2eeb96a423b0.js} +1852 -8945
- package/dist/699.bundle.02c15c3cc4c04dbf7f51.js +785 -0
- package/dist/{359.bundle.8abe0036a7bf6b5fd115.js → 724.bundle.d50ce9fb0ab01b9378b7.js} +130 -254
- package/dist/{757.bundle.ec8301d8e70d2b990f65.js → 726.bundle.c8de818cf1a3ff0cf7d2.js} +512 -879
- package/dist/{530.bundle.a03b6f942ace3e1baa1e.js → 835.bundle.15aff0b7433bb0dd6d6d.js} +37 -30
- package/dist/{822.bundle.82cdc418f8f56da6060b.js → 862.bundle.809c87a7ba9da6fb29c8.js} +77 -96
- package/dist/{236.bundle.c9e70d55e7b2574c1ecd.js → 889.bundle.1c17d0d13e157ac21d38.js} +198 -197
- package/dist/{342.bundle.d9668551811e3a88aaa4.js → 90.bundle.27637ef740946d5c8948.js} +1429 -1055
- package/dist/{281.bundle.16a2933086a57e60c96c.js → 905.bundle.206e44c3bbd1df1a900b.js} +155 -122
- package/dist/{814.bundle.a1aba9c1e3d336008351.js → 907.bundle.11700f7af989b5af8bc3.js} +16 -30
- package/dist/{417.bundle.af0a207c29b109f84159.js → 931.bundle.d270a1fda9a2836c3cc5.js} +26 -26
- package/dist/{686.bundle.dccef1f36e4bc79bcc48.js → 939.bundle.9d93b2e47c52338747a2.js} +7 -8
- package/dist/{12.bundle.37a8b47d2ae587cb9226.js → 961.bundle.a1ffb667eb04cbe07210.js} +16 -31
- package/dist/987.bundle.6bdfb3cd8762b8889632.js +122950 -0
- package/dist/app-config.js +1 -0
- package/dist/app.bundle.css +15 -13
- package/dist/{app.bundle.437d085e13599d1e1ced.js → app.bundle.d1c8b09ab30d221fddf0.js} +148259 -61821
- package/dist/assets/images/CT-AAA.png +0 -0
- package/dist/assets/images/CT-AAA2.png +0 -0
- package/dist/assets/images/CT-Air.png +0 -0
- package/dist/assets/images/CT-Bone.png +0 -0
- package/dist/assets/images/CT-Bones.png +0 -0
- package/dist/assets/images/CT-Cardiac.png +0 -0
- package/dist/assets/images/CT-Cardiac2.png +0 -0
- package/dist/assets/images/CT-Cardiac3.png +0 -0
- package/dist/assets/images/CT-Chest-Contrast-Enhanced.png +0 -0
- package/dist/assets/images/CT-Chest-Vessels.png +0 -0
- package/dist/assets/images/CT-Coronary-Arteries-2.png +0 -0
- package/dist/assets/images/CT-Coronary-Arteries-3.png +0 -0
- package/dist/assets/images/CT-Coronary-Arteries.png +0 -0
- package/dist/assets/images/CT-Cropped-Volume-Bone.png +0 -0
- package/dist/assets/images/CT-Fat.png +0 -0
- package/dist/assets/images/CT-Liver-Vasculature.png +0 -0
- package/dist/assets/images/CT-Lung.png +0 -0
- package/dist/assets/images/CT-MIP.png +0 -0
- package/dist/assets/images/CT-Muscle.png +0 -0
- package/dist/assets/images/CT-Pulmonary-Arteries.png +0 -0
- package/dist/assets/images/CT-Soft-Tissue.png +0 -0
- package/dist/assets/images/DTI-FA-Brain.png +0 -0
- package/dist/assets/images/MR-Angio.png +0 -0
- package/dist/assets/images/MR-Default.png +0 -0
- package/dist/assets/images/MR-MIP.png +0 -0
- package/dist/assets/images/MR-T2-Brain.png +0 -0
- package/dist/assets/images/VolumeRendering.png +0 -0
- package/dist/cornerstoneDICOMImageLoader.min.js +1 -1
- package/dist/cornerstoneDICOMImageLoader.min.js.map +1 -1
- package/dist/{dicom-microscopy-viewer.bundle.2c146384eb9466d02ff8.js → dicom-microscopy-viewer.bundle.d3a56dc9f62df5e11019.js} +3 -3
- package/dist/index.html +1 -1
- package/dist/{index.worker.e62ecca63f1a2e124230.worker.js → index.worker.64c896c4316fcd506666.worker.js} +2 -2
- package/dist/index.worker.64c896c4316fcd506666.worker.js.map +1 -0
- package/dist/polySeg.bundle.01449e456b7d4a737d4f.js +252 -0
- package/dist/serve.json +12 -0
- package/dist/sw.js +1 -1
- package/package.json +25 -22
- package/dist/23.bundle.e008ad788170f2ed5569.js +0 -900
- package/dist/604.bundle.a51f83e64004bca5f497.js +0 -1848
- package/dist/613.bundle.aed640a7900dbcb688f5.js +0 -532
- package/dist/743.bundle.489f7df3a089d4d374e1.js +0 -78007
- package/dist/75788f12450d4c5ed494.wasm +0 -0
- package/dist/775.bundle.2285e7e0e67878948c0d.js +0 -1009
- package/dist/788.bundle.dcd53828d1bb2ac64d04.js +0 -2682
- package/dist/82.bundle.5a94dd7645e5c5476f59.js +0 -1049
- package/dist/index.worker.e62ecca63f1a2e124230.worker.js.map +0 -1
- /package/dist/{19.css → 155.css} +0 -0
- /package/dist/{221.css → 2.css} +0 -0
- /package/dist/{579.css → 481.css} +0 -0
- /package/dist/{250.css → 544.css} +0 -0
- /package/dist/{776.css → 595.css} +0 -0
|
@@ -0,0 +1,2957 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(globalThis["webpackChunk"] = globalThis["webpackChunk"] || []).push([[425,481],{
|
|
3
|
+
|
|
4
|
+
/***/ 42048:
|
|
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 + 542 modules
|
|
727
|
+
var src = __webpack_require__(48804);
|
|
728
|
+
// EXTERNAL MODULE: ../../core/src/index.ts + 68 modules
|
|
729
|
+
var core_src = __webpack_require__(85073);
|
|
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-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
|
+
// EXTERNAL MODULE: ../../../extensions/default/src/index.ts + 78 modules
|
|
974
|
+
var default_src = __webpack_require__(54090);
|
|
975
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/segmentationEditHandler.tsx
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
function segmentationItemEditHandler({
|
|
979
|
+
id,
|
|
980
|
+
servicesManager
|
|
981
|
+
}) {
|
|
982
|
+
const {
|
|
983
|
+
segmentationService,
|
|
984
|
+
uiDialogService
|
|
985
|
+
} = servicesManager.services;
|
|
986
|
+
const segmentation = segmentationService.getSegmentation(id);
|
|
987
|
+
const onSubmitHandler = ({
|
|
988
|
+
action,
|
|
989
|
+
value
|
|
990
|
+
}) => {
|
|
991
|
+
switch (action.id) {
|
|
992
|
+
case 'save':
|
|
993
|
+
{
|
|
994
|
+
segmentationService.addOrUpdateSegmentation({
|
|
995
|
+
...segmentation,
|
|
996
|
+
...value
|
|
997
|
+
}, false,
|
|
998
|
+
// don't suppress event
|
|
999
|
+
true // it should update cornerstone
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
uiDialogService.dismiss({
|
|
1004
|
+
id: 'enter-annotation'
|
|
1005
|
+
});
|
|
1006
|
+
};
|
|
1007
|
+
uiDialogService.create({
|
|
1008
|
+
id: 'enter-annotation',
|
|
1009
|
+
centralize: true,
|
|
1010
|
+
isDraggable: false,
|
|
1011
|
+
showOverlay: true,
|
|
1012
|
+
content: src/* Dialog */.lG,
|
|
1013
|
+
contentProps: {
|
|
1014
|
+
title: 'Enter your Segmentation',
|
|
1015
|
+
noCloseButton: true,
|
|
1016
|
+
value: {
|
|
1017
|
+
label: segmentation.label || ''
|
|
1018
|
+
},
|
|
1019
|
+
body: ({
|
|
1020
|
+
value,
|
|
1021
|
+
setValue
|
|
1022
|
+
}) => {
|
|
1023
|
+
const onChangeHandler = event => {
|
|
1024
|
+
event.persist();
|
|
1025
|
+
setValue(value => ({
|
|
1026
|
+
...value,
|
|
1027
|
+
label: event.target.value
|
|
1028
|
+
}));
|
|
1029
|
+
};
|
|
1030
|
+
const onKeyPressHandler = event => {
|
|
1031
|
+
if (event.key === 'Enter') {
|
|
1032
|
+
onSubmitHandler({
|
|
1033
|
+
value,
|
|
1034
|
+
action: {
|
|
1035
|
+
id: 'save'
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
return /*#__PURE__*/react.createElement(src/* Input */.pd, {
|
|
1041
|
+
autoFocus: true,
|
|
1042
|
+
className: "border-primary-main bg-black",
|
|
1043
|
+
type: "text",
|
|
1044
|
+
containerClassName: "mr-2",
|
|
1045
|
+
value: value.label,
|
|
1046
|
+
onChange: onChangeHandler,
|
|
1047
|
+
onKeyPress: onKeyPressHandler
|
|
1048
|
+
});
|
|
1049
|
+
},
|
|
1050
|
+
actions: [{
|
|
1051
|
+
id: 'cancel',
|
|
1052
|
+
text: 'Cancel',
|
|
1053
|
+
type: src/* ButtonEnums.type */.Ny.NW.secondary
|
|
1054
|
+
}, {
|
|
1055
|
+
id: 'save',
|
|
1056
|
+
text: 'Save',
|
|
1057
|
+
type: src/* ButtonEnums.type */.Ny.NW.primary
|
|
1058
|
+
}],
|
|
1059
|
+
onSubmit: onSubmitHandler
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
/* harmony default export */ const segmentationEditHandler = (segmentationItemEditHandler);
|
|
1064
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ExportReports.tsx
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
function ExportReports({
|
|
1069
|
+
segmentations,
|
|
1070
|
+
tmtvValue,
|
|
1071
|
+
config,
|
|
1072
|
+
commandsManager
|
|
1073
|
+
}) {
|
|
1074
|
+
const {
|
|
1075
|
+
t
|
|
1076
|
+
} = (0,es/* useTranslation */.Bd)('PanelSUVExport');
|
|
1077
|
+
return /*#__PURE__*/react.createElement(react.Fragment, null, segmentations?.length ? /*#__PURE__*/react.createElement("div", {
|
|
1078
|
+
className: "mt-4 flex justify-center space-x-2"
|
|
1079
|
+
}, /*#__PURE__*/react.createElement(src/* LegacyButtonGroup */.xA, {
|
|
1080
|
+
color: "black",
|
|
1081
|
+
size: "inherit"
|
|
1082
|
+
}, /*#__PURE__*/react.createElement(src/* LegacyButton */._H, {
|
|
1083
|
+
className: "px-2 py-2 text-base",
|
|
1084
|
+
disabled: tmtvValue === null,
|
|
1085
|
+
onClick: () => {
|
|
1086
|
+
commandsManager.runCommand('exportTMTVReportCSV', {
|
|
1087
|
+
segmentations,
|
|
1088
|
+
tmtv: tmtvValue,
|
|
1089
|
+
config
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}, t('Export CSV'))), /*#__PURE__*/react.createElement(src/* LegacyButtonGroup */.xA, {
|
|
1093
|
+
color: "black",
|
|
1094
|
+
size: "inherit"
|
|
1095
|
+
}, /*#__PURE__*/react.createElement(src/* LegacyButton */._H, {
|
|
1096
|
+
className: "px-2 py-2 text-base",
|
|
1097
|
+
onClick: () => {
|
|
1098
|
+
commandsManager.runCommand('createTMTVRTReport');
|
|
1099
|
+
},
|
|
1100
|
+
disabled: tmtvValue === null
|
|
1101
|
+
}, t('Create RT Report')))) : null);
|
|
1102
|
+
}
|
|
1103
|
+
/* harmony default export */ const PanelROIThresholdSegmentation_ExportReports = (ExportReports);
|
|
1104
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/callInputDialog.tsx
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
function callInputDialog(uiDialogService, label, callback) {
|
|
1108
|
+
const dialogId = 'enter-segment-label';
|
|
1109
|
+
const onSubmitHandler = ({
|
|
1110
|
+
action,
|
|
1111
|
+
value
|
|
1112
|
+
}) => {
|
|
1113
|
+
switch (action.id) {
|
|
1114
|
+
case 'save':
|
|
1115
|
+
callback(value.label, action.id);
|
|
1116
|
+
break;
|
|
1117
|
+
case 'cancel':
|
|
1118
|
+
callback('', action.id);
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
uiDialogService.dismiss({
|
|
1122
|
+
id: dialogId
|
|
1123
|
+
});
|
|
1124
|
+
};
|
|
1125
|
+
if (uiDialogService) {
|
|
1126
|
+
uiDialogService.create({
|
|
1127
|
+
id: dialogId,
|
|
1128
|
+
centralize: true,
|
|
1129
|
+
isDraggable: false,
|
|
1130
|
+
showOverlay: true,
|
|
1131
|
+
content: src/* Dialog */.lG,
|
|
1132
|
+
contentProps: {
|
|
1133
|
+
title: 'Segment',
|
|
1134
|
+
value: {
|
|
1135
|
+
label
|
|
1136
|
+
},
|
|
1137
|
+
noCloseButton: true,
|
|
1138
|
+
onClose: () => uiDialogService.dismiss({
|
|
1139
|
+
id: dialogId
|
|
1140
|
+
}),
|
|
1141
|
+
actions: [{
|
|
1142
|
+
id: 'cancel',
|
|
1143
|
+
text: 'Cancel',
|
|
1144
|
+
type: src/* ButtonEnums.type */.Ny.NW.secondary
|
|
1145
|
+
}, {
|
|
1146
|
+
id: 'save',
|
|
1147
|
+
text: 'Confirm',
|
|
1148
|
+
type: src/* ButtonEnums.type */.Ny.NW.primary
|
|
1149
|
+
}],
|
|
1150
|
+
onSubmit: onSubmitHandler,
|
|
1151
|
+
body: ({
|
|
1152
|
+
value,
|
|
1153
|
+
setValue
|
|
1154
|
+
}) => {
|
|
1155
|
+
return /*#__PURE__*/react.createElement(src/* Input */.pd, {
|
|
1156
|
+
label: "Enter the segment label",
|
|
1157
|
+
labelClassName: "text-white text-[14px] leading-[1.2]",
|
|
1158
|
+
autoFocus: true,
|
|
1159
|
+
className: "border-primary-main bg-black",
|
|
1160
|
+
type: "text",
|
|
1161
|
+
value: value.label,
|
|
1162
|
+
onChange: event => {
|
|
1163
|
+
event.persist();
|
|
1164
|
+
setValue(value => ({
|
|
1165
|
+
...value,
|
|
1166
|
+
label: event.target.value
|
|
1167
|
+
}));
|
|
1168
|
+
},
|
|
1169
|
+
onKeyPress: event => {
|
|
1170
|
+
if (event.key === 'Enter') {
|
|
1171
|
+
onSubmitHandler({
|
|
1172
|
+
value,
|
|
1173
|
+
action: {
|
|
1174
|
+
id: 'save'
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
/* harmony default export */ const PanelROIThresholdSegmentation_callInputDialog = (callInputDialog);
|
|
1186
|
+
// EXTERNAL MODULE: ../../../node_modules/react-color/es/index.js + 219 modules
|
|
1187
|
+
var react_color_es = __webpack_require__(13726);
|
|
1188
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/colorPickerDialog.css
|
|
1189
|
+
// extracted by mini-css-extract-plugin
|
|
1190
|
+
|
|
1191
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/colorPickerDialog.tsx
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
|
|
1197
|
+
const dialogId = 'pick-color';
|
|
1198
|
+
const onSubmitHandler = ({
|
|
1199
|
+
action,
|
|
1200
|
+
value
|
|
1201
|
+
}) => {
|
|
1202
|
+
switch (action.id) {
|
|
1203
|
+
case 'save':
|
|
1204
|
+
callback(value.rgbaColor, action.id);
|
|
1205
|
+
break;
|
|
1206
|
+
case 'cancel':
|
|
1207
|
+
callback('', action.id);
|
|
1208
|
+
break;
|
|
1209
|
+
}
|
|
1210
|
+
uiDialogService.dismiss({
|
|
1211
|
+
id: dialogId
|
|
1212
|
+
});
|
|
1213
|
+
};
|
|
1214
|
+
if (uiDialogService) {
|
|
1215
|
+
uiDialogService.create({
|
|
1216
|
+
id: dialogId,
|
|
1217
|
+
centralize: true,
|
|
1218
|
+
isDraggable: false,
|
|
1219
|
+
showOverlay: true,
|
|
1220
|
+
content: src/* Dialog */.lG,
|
|
1221
|
+
contentProps: {
|
|
1222
|
+
title: 'Segment Color',
|
|
1223
|
+
value: {
|
|
1224
|
+
rgbaColor
|
|
1225
|
+
},
|
|
1226
|
+
noCloseButton: true,
|
|
1227
|
+
onClose: () => uiDialogService.dismiss({
|
|
1228
|
+
id: dialogId
|
|
1229
|
+
}),
|
|
1230
|
+
actions: [{
|
|
1231
|
+
id: 'cancel',
|
|
1232
|
+
text: 'Cancel',
|
|
1233
|
+
type: 'primary'
|
|
1234
|
+
}, {
|
|
1235
|
+
id: 'save',
|
|
1236
|
+
text: 'Save',
|
|
1237
|
+
type: 'secondary'
|
|
1238
|
+
}],
|
|
1239
|
+
onSubmit: onSubmitHandler,
|
|
1240
|
+
body: ({
|
|
1241
|
+
value,
|
|
1242
|
+
setValue
|
|
1243
|
+
}) => {
|
|
1244
|
+
const handleChange = color => {
|
|
1245
|
+
setValue({
|
|
1246
|
+
rgbaColor: color.rgb
|
|
1247
|
+
});
|
|
1248
|
+
};
|
|
1249
|
+
return /*#__PURE__*/react.createElement(react_color_es/* ChromePicker */.xk, {
|
|
1250
|
+
color: value.rgbaColor,
|
|
1251
|
+
onChange: handleChange,
|
|
1252
|
+
presetColors: [],
|
|
1253
|
+
width: 300
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
/* harmony default export */ const colorPickerDialog = (callColorPickerDialog);
|
|
1261
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/PanelROIThresholdSegmentation.tsx
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
function PanelRoiThresholdSegmentation({
|
|
1271
|
+
servicesManager,
|
|
1272
|
+
commandsManager,
|
|
1273
|
+
extensionManager
|
|
1274
|
+
}) {
|
|
1275
|
+
const {
|
|
1276
|
+
segmentationService,
|
|
1277
|
+
viewportGridService,
|
|
1278
|
+
uiDialogService
|
|
1279
|
+
} = servicesManager.services;
|
|
1280
|
+
const [selectedSegmentationId, setSelectedSegmentationId] = (0,react.useState)(null);
|
|
1281
|
+
const [segmentations, setSegmentations] = (0,react.useState)(() => segmentationService.getSegmentations());
|
|
1282
|
+
const runCommand = (0,react.useCallback)((commandName, commandOptions = {}) => {
|
|
1283
|
+
return commandsManager.runCommand(commandName, commandOptions);
|
|
1284
|
+
}, [commandsManager]);
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* Update UI based on segmentation changes (added, removed, updated)
|
|
1288
|
+
*/
|
|
1289
|
+
(0,react.useEffect)(() => {
|
|
1290
|
+
// ~~ Subscription
|
|
1291
|
+
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
|
1292
|
+
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
|
1293
|
+
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
|
1294
|
+
const subscriptions = [];
|
|
1295
|
+
[added, updated, removed].forEach(evt => {
|
|
1296
|
+
const {
|
|
1297
|
+
unsubscribe
|
|
1298
|
+
} = segmentationService.subscribe(evt, () => {
|
|
1299
|
+
const segmentations = segmentationService.getSegmentations();
|
|
1300
|
+
setSegmentations(segmentations);
|
|
1301
|
+
});
|
|
1302
|
+
subscriptions.push(unsubscribe);
|
|
1303
|
+
});
|
|
1304
|
+
return () => {
|
|
1305
|
+
subscriptions.forEach(unsub => {
|
|
1306
|
+
unsub();
|
|
1307
|
+
});
|
|
1308
|
+
};
|
|
1309
|
+
}, []);
|
|
1310
|
+
const onSegmentationClick = segmentationId => {
|
|
1311
|
+
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
|
|
1312
|
+
setSelectedSegmentationId(segmentationId);
|
|
1313
|
+
};
|
|
1314
|
+
const onSegmentationAdd = async () => {
|
|
1315
|
+
runCommand('createNewLabelmapFromPT').then(segmentationId => {
|
|
1316
|
+
setSelectedSegmentationId(segmentationId);
|
|
1317
|
+
});
|
|
1318
|
+
};
|
|
1319
|
+
const onSegmentAdd = segmentationId => {
|
|
1320
|
+
segmentationService.addSegment(segmentationId);
|
|
1321
|
+
};
|
|
1322
|
+
const getToolGroupIds = segmentationId => {
|
|
1323
|
+
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
|
1324
|
+
return toolGroupIds;
|
|
1325
|
+
};
|
|
1326
|
+
const onSegmentClick = (segmentationId, segmentIndex) => {
|
|
1327
|
+
segmentationService.setActiveSegment(segmentationId, segmentIndex);
|
|
1328
|
+
const toolGroupIds = getToolGroupIds(segmentationId);
|
|
1329
|
+
toolGroupIds.forEach(toolGroupId => {
|
|
1330
|
+
// const toolGroupId =
|
|
1331
|
+
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
|
1332
|
+
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
|
|
1333
|
+
});
|
|
1334
|
+
};
|
|
1335
|
+
const _setSegmentationConfiguration = (0,react.useCallback)((segmentationId, key, value) => {
|
|
1336
|
+
segmentationService.setConfiguration({
|
|
1337
|
+
segmentationId,
|
|
1338
|
+
[key]: value
|
|
1339
|
+
});
|
|
1340
|
+
}, [segmentationService]);
|
|
1341
|
+
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
|
1342
|
+
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
1343
|
+
const segmentInfo = segmentation.segments[segmentIndex];
|
|
1344
|
+
const isVisible = !segmentInfo.isVisible;
|
|
1345
|
+
const toolGroupIds = getToolGroupIds(segmentationId);
|
|
1346
|
+
toolGroupIds.forEach(toolGroupId => {
|
|
1347
|
+
segmentationService.setSegmentVisibility(segmentationId, segmentIndex, isVisible, toolGroupId);
|
|
1348
|
+
});
|
|
1349
|
+
};
|
|
1350
|
+
const onSegmentDelete = (segmentationId, segmentIndex) => {
|
|
1351
|
+
segmentationService.removeSegment(segmentationId, segmentIndex);
|
|
1352
|
+
};
|
|
1353
|
+
const onSegmentEdit = (segmentationId, segmentIndex) => {
|
|
1354
|
+
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
1355
|
+
const segment = segmentation.segments[segmentIndex];
|
|
1356
|
+
const {
|
|
1357
|
+
label
|
|
1358
|
+
} = segment;
|
|
1359
|
+
PanelROIThresholdSegmentation_callInputDialog(uiDialogService, label, (label, actionId) => {
|
|
1360
|
+
if (label === '') {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
|
1364
|
+
});
|
|
1365
|
+
};
|
|
1366
|
+
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
|
|
1367
|
+
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
|
|
1368
|
+
};
|
|
1369
|
+
const onSegmentColorClick = (segmentationId, segmentIndex) => {
|
|
1370
|
+
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
1371
|
+
const segment = segmentation.segments[segmentIndex];
|
|
1372
|
+
const {
|
|
1373
|
+
color,
|
|
1374
|
+
opacity
|
|
1375
|
+
} = segment;
|
|
1376
|
+
const rgbaColor = {
|
|
1377
|
+
r: color[0],
|
|
1378
|
+
g: color[1],
|
|
1379
|
+
b: color[2],
|
|
1380
|
+
a: opacity / 255.0
|
|
1381
|
+
};
|
|
1382
|
+
colorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
|
1383
|
+
if (actionId === 'cancel') {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [newRgbaColor.r, newRgbaColor.g, newRgbaColor.b, newRgbaColor.a * 255.0]);
|
|
1387
|
+
});
|
|
1388
|
+
};
|
|
1389
|
+
const storeSegmentation = async segmentationId => {
|
|
1390
|
+
const datasources = extensionManager.getActiveDataSource();
|
|
1391
|
+
const displaySetInstanceUIDs = await (0,default_src.createReportAsync)({
|
|
1392
|
+
servicesManager,
|
|
1393
|
+
getReport: () => commandsManager.runCommand('storeSegmentation', {
|
|
1394
|
+
segmentationId,
|
|
1395
|
+
dataSource: datasources[0]
|
|
1396
|
+
}),
|
|
1397
|
+
reportType: 'Segmentation'
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
// Show the exported report in the active viewport as read only (similar to SR)
|
|
1401
|
+
if (displaySetInstanceUIDs) {
|
|
1402
|
+
// clear the segmentation that we exported, similar to the storeMeasurement
|
|
1403
|
+
// where we remove the measurements and prompt again the user if they would like
|
|
1404
|
+
// to re-read the measurements in a SR read only viewport
|
|
1405
|
+
segmentationService.remove(segmentationId);
|
|
1406
|
+
viewportGridService.setDisplaySetsForViewport({
|
|
1407
|
+
viewportId: viewportGridService.getActiveViewportId(),
|
|
1408
|
+
displaySetInstanceUIDs
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
const onSegmentationDownloadRTSS = segmentationId => {
|
|
1413
|
+
commandsManager.runCommand('downloadRTSS', {
|
|
1414
|
+
segmentationId
|
|
1415
|
+
});
|
|
1416
|
+
};
|
|
1417
|
+
const onSegmentationDownload = segmentationId => {
|
|
1418
|
+
commandsManager.runCommand('downloadSegmentation', {
|
|
1419
|
+
segmentationId
|
|
1420
|
+
});
|
|
1421
|
+
};
|
|
1422
|
+
const tmtvValue = segmentations?.[0]?.cachedStats?.tmtv?.value || null;
|
|
1423
|
+
const config = segmentations?.[0]?.cachedStats?.tmtv?.config || {};
|
|
1424
|
+
return /*#__PURE__*/react.createElement(react.Fragment, null, /*#__PURE__*/react.createElement("div", {
|
|
1425
|
+
className: "flex flex-col"
|
|
1426
|
+
}, /*#__PURE__*/react.createElement("div", {
|
|
1427
|
+
className: "invisible-scrollbar overflow-y-auto overflow-x-hidden"
|
|
1428
|
+
}, /*#__PURE__*/react.createElement("div", {
|
|
1429
|
+
className: "flex min-h-0 flex-col bg-black text-[13px] font-[300]"
|
|
1430
|
+
}, /*#__PURE__*/react.createElement(src/* SegmentationGroupTableExpanded */.fO, {
|
|
1431
|
+
disableEditing: false,
|
|
1432
|
+
showAddSegmentation: true,
|
|
1433
|
+
showAddSegment: false,
|
|
1434
|
+
showDeleteSegment: true,
|
|
1435
|
+
segmentations: segmentations,
|
|
1436
|
+
onSegmentationAdd: onSegmentationAdd,
|
|
1437
|
+
onSegmentationClick: onSegmentationClick,
|
|
1438
|
+
onToggleSegmentationVisibility: id => {
|
|
1439
|
+
segmentationService.toggleSegmentationVisibility(id);
|
|
1440
|
+
},
|
|
1441
|
+
onToggleSegmentVisibility: onToggleSegmentVisibility,
|
|
1442
|
+
onSegmentationDelete: id => {
|
|
1443
|
+
segmentationService.remove(id);
|
|
1444
|
+
},
|
|
1445
|
+
onSegmentationEdit: id => {
|
|
1446
|
+
segmentationEditHandler({
|
|
1447
|
+
id,
|
|
1448
|
+
servicesManager
|
|
1449
|
+
});
|
|
1450
|
+
},
|
|
1451
|
+
segmentationConfig: {
|
|
1452
|
+
initialConfig: segmentationService.getConfiguration()
|
|
1453
|
+
},
|
|
1454
|
+
onSegmentAdd: onSegmentAdd,
|
|
1455
|
+
onSegmentClick: onSegmentClick,
|
|
1456
|
+
onSegmentDelete: onSegmentDelete,
|
|
1457
|
+
onSegmentEdit: onSegmentEdit,
|
|
1458
|
+
onToggleSegmentLock: onToggleSegmentLock,
|
|
1459
|
+
onSegmentColorClick: onSegmentColorClick,
|
|
1460
|
+
storeSegmentation: storeSegmentation,
|
|
1461
|
+
onSegmentationDownloadRTSS: onSegmentationDownloadRTSS,
|
|
1462
|
+
onSegmentationDownload: onSegmentationDownload,
|
|
1463
|
+
setRenderOutline: value => _setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value),
|
|
1464
|
+
setOutlineOpacityActive: value => _setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value),
|
|
1465
|
+
setRenderFill: value => _setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value),
|
|
1466
|
+
setRenderInactiveSegmentations: value => _setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value),
|
|
1467
|
+
setOutlineWidthActive: value => _setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value),
|
|
1468
|
+
setFillAlpha: value => _setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value),
|
|
1469
|
+
setFillAlphaInactive: value => _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
|
1470
|
+
})), tmtvValue !== null ? /*#__PURE__*/react.createElement("div", {
|
|
1471
|
+
className: "bg-secondary-dark mt-1 flex items-baseline justify-between px-2 py-1"
|
|
1472
|
+
}, /*#__PURE__*/react.createElement("span", {
|
|
1473
|
+
className: "text-base font-bold uppercase tracking-widest text-white"
|
|
1474
|
+
}, 'TMTV:'), /*#__PURE__*/react.createElement("div", {
|
|
1475
|
+
className: "text-white"
|
|
1476
|
+
}, `${tmtvValue} mL`)) : null, /*#__PURE__*/react.createElement(PanelROIThresholdSegmentation_ExportReports, {
|
|
1477
|
+
segmentations: segmentations,
|
|
1478
|
+
tmtvValue: tmtvValue,
|
|
1479
|
+
config: config,
|
|
1480
|
+
commandsManager: commandsManager
|
|
1481
|
+
}))), /*#__PURE__*/react.createElement("div", {
|
|
1482
|
+
className: "absolute bottom-1 flex cursor-pointer items-center justify-center text-blue-400 opacity-50 hover:opacity-80",
|
|
1483
|
+
onClick: () => {
|
|
1484
|
+
// navigate to a url in a new tab
|
|
1485
|
+
window.open('https://github.com/OHIF/Viewers/blob/master/modes/tmtv/README.md', '_blank');
|
|
1486
|
+
}
|
|
1487
|
+
}, /*#__PURE__*/react.createElement(src/* Icon */.In, {
|
|
1488
|
+
width: "15px",
|
|
1489
|
+
height: "15px",
|
|
1490
|
+
name: 'info',
|
|
1491
|
+
className: 'text-primary-active ml-4 mr-3'
|
|
1492
|
+
}), /*#__PURE__*/react.createElement("span", null, 'User Guide')));
|
|
1493
|
+
}
|
|
1494
|
+
PanelRoiThresholdSegmentation.propTypes = {
|
|
1495
|
+
commandsManager: prop_types_default().shape({
|
|
1496
|
+
runCommand: (prop_types_default()).func.isRequired
|
|
1497
|
+
}),
|
|
1498
|
+
servicesManager: prop_types_default().shape({
|
|
1499
|
+
services: prop_types_default().shape({
|
|
1500
|
+
segmentationService: prop_types_default().shape({
|
|
1501
|
+
getSegmentation: (prop_types_default()).func.isRequired,
|
|
1502
|
+
getSegmentations: (prop_types_default()).func.isRequired,
|
|
1503
|
+
toggleSegmentationVisibility: (prop_types_default()).func.isRequired,
|
|
1504
|
+
subscribe: (prop_types_default()).func.isRequired,
|
|
1505
|
+
EVENTS: (prop_types_default()).object.isRequired
|
|
1506
|
+
}).isRequired
|
|
1507
|
+
}).isRequired
|
|
1508
|
+
}).isRequired
|
|
1509
|
+
};
|
|
1510
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/index.ts
|
|
1511
|
+
|
|
1512
|
+
/* harmony default export */ const PanelROIThresholdSegmentation = (PanelRoiThresholdSegmentation);
|
|
1513
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/index.tsx
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/getPanelModule.tsx
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
// TODO:
|
|
1523
|
+
// - No loading UI exists yet
|
|
1524
|
+
// - cancel promises when component is destroyed
|
|
1525
|
+
// - show errors in UI for thumbnails if promise fails
|
|
1526
|
+
|
|
1527
|
+
function getPanelModule({
|
|
1528
|
+
commandsManager,
|
|
1529
|
+
extensionManager,
|
|
1530
|
+
servicesManager
|
|
1531
|
+
}) {
|
|
1532
|
+
const wrappedPanelPetSuv = () => {
|
|
1533
|
+
return /*#__PURE__*/react.createElement(PanelPetSUV, {
|
|
1534
|
+
commandsManager: commandsManager,
|
|
1535
|
+
servicesManager: servicesManager
|
|
1536
|
+
});
|
|
1537
|
+
};
|
|
1538
|
+
const wrappedROIThresholdSeg = () => {
|
|
1539
|
+
return /*#__PURE__*/react.createElement(react.Fragment, null, /*#__PURE__*/react.createElement(src/* Toolbox */.OO, {
|
|
1540
|
+
commandsManager: commandsManager,
|
|
1541
|
+
servicesManager: servicesManager,
|
|
1542
|
+
extensionManager: extensionManager,
|
|
1543
|
+
buttonSectionId: "tmtvToolbox",
|
|
1544
|
+
title: "Threshold Tools"
|
|
1545
|
+
}), /*#__PURE__*/react.createElement(PanelROIThresholdSegmentation, {
|
|
1546
|
+
commandsManager: commandsManager,
|
|
1547
|
+
servicesManager: servicesManager,
|
|
1548
|
+
extensionManager: extensionManager
|
|
1549
|
+
}));
|
|
1550
|
+
};
|
|
1551
|
+
return [{
|
|
1552
|
+
name: 'petSUV',
|
|
1553
|
+
iconName: 'tab-patient-info',
|
|
1554
|
+
iconLabel: 'Patient Info',
|
|
1555
|
+
label: 'Patient Info',
|
|
1556
|
+
component: wrappedPanelPetSuv
|
|
1557
|
+
}, {
|
|
1558
|
+
name: 'ROIThresholdSeg',
|
|
1559
|
+
iconName: 'tab-segmentation',
|
|
1560
|
+
iconLabel: 'Segmentation',
|
|
1561
|
+
label: 'Segmentation',
|
|
1562
|
+
component: wrappedROIThresholdSeg
|
|
1563
|
+
}];
|
|
1564
|
+
}
|
|
1565
|
+
/* harmony default export */ const src_getPanelModule = (getPanelModule);
|
|
1566
|
+
// EXTERNAL MODULE: ../../../node_modules/@cornerstonejs/tools/dist/esm/index.js + 16 modules
|
|
1567
|
+
var esm = __webpack_require__(20767);
|
|
1568
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/measurementServiceMappings/constants/supportedTools.js
|
|
1569
|
+
/* harmony default export */ const supportedTools = (['RectangleROIStartEndThreshold']);
|
|
1570
|
+
// EXTERNAL MODULE: ../../../node_modules/@cornerstonejs/core/dist/esm/index.js + 383 modules
|
|
1571
|
+
var dist_esm = __webpack_require__(50719);
|
|
1572
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/measurementServiceMappings/utils/getSOPInstanceAttributes.js
|
|
1573
|
+
|
|
1574
|
+
function getSOPInstanceAttributes(imageId) {
|
|
1575
|
+
if (imageId) {
|
|
1576
|
+
return _getUIDFromImageID(imageId);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function _getUIDFromImageID(imageId) {
|
|
1580
|
+
const instance = dist_esm.metaData.get('instance', imageId);
|
|
1581
|
+
return {
|
|
1582
|
+
SOPInstanceUID: instance.SOPInstanceUID,
|
|
1583
|
+
SeriesInstanceUID: instance.SeriesInstanceUID,
|
|
1584
|
+
StudyInstanceUID: instance.StudyInstanceUID,
|
|
1585
|
+
frameNumber: instance.frameNumber || 1
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/measurementServiceMappings/RectangleROIStartEndThreshold.js
|
|
1589
|
+
|
|
1590
|
+
|
|
1591
|
+
const RectangleROIStartEndThreshold = {
|
|
1592
|
+
toAnnotation: (measurement, definition) => {},
|
|
1593
|
+
/**
|
|
1594
|
+
* Maps cornerstone annotation event data to measurement service format.
|
|
1595
|
+
*
|
|
1596
|
+
* @param {Object} cornerstone Cornerstone event data
|
|
1597
|
+
* @return {Measurement} Measurement instance
|
|
1598
|
+
*/
|
|
1599
|
+
toMeasurement: (csToolsEventDetail, displaySetService, cornerstoneViewportService) => {
|
|
1600
|
+
const {
|
|
1601
|
+
annotation,
|
|
1602
|
+
viewportId
|
|
1603
|
+
} = csToolsEventDetail;
|
|
1604
|
+
const {
|
|
1605
|
+
metadata,
|
|
1606
|
+
data,
|
|
1607
|
+
annotationUID
|
|
1608
|
+
} = annotation;
|
|
1609
|
+
if (!metadata || !data) {
|
|
1610
|
+
console.warn('Length tool: Missing metadata or data');
|
|
1611
|
+
return null;
|
|
1612
|
+
}
|
|
1613
|
+
const {
|
|
1614
|
+
toolName,
|
|
1615
|
+
referencedImageId,
|
|
1616
|
+
FrameOfReferenceUID
|
|
1617
|
+
} = metadata;
|
|
1618
|
+
const validToolType = supportedTools.includes(toolName);
|
|
1619
|
+
if (!validToolType) {
|
|
1620
|
+
throw new Error('Tool not supported');
|
|
1621
|
+
}
|
|
1622
|
+
const {
|
|
1623
|
+
SOPInstanceUID,
|
|
1624
|
+
SeriesInstanceUID,
|
|
1625
|
+
StudyInstanceUID
|
|
1626
|
+
} = getSOPInstanceAttributes(referencedImageId, cornerstoneViewportService, viewportId);
|
|
1627
|
+
let displaySet;
|
|
1628
|
+
if (SOPInstanceUID) {
|
|
1629
|
+
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(SOPInstanceUID, SeriesInstanceUID);
|
|
1630
|
+
} else {
|
|
1631
|
+
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
|
1632
|
+
}
|
|
1633
|
+
return {
|
|
1634
|
+
uid: annotationUID,
|
|
1635
|
+
SOPInstanceUID,
|
|
1636
|
+
FrameOfReferenceUID,
|
|
1637
|
+
// points,
|
|
1638
|
+
metadata,
|
|
1639
|
+
referenceSeriesUID: SeriesInstanceUID,
|
|
1640
|
+
referenceStudyUID: StudyInstanceUID,
|
|
1641
|
+
toolName: metadata.toolName,
|
|
1642
|
+
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
|
1643
|
+
label: metadata.label,
|
|
1644
|
+
data: data.cachedStats,
|
|
1645
|
+
type: 'RectangleROIStartEndThreshold'
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
/* harmony default export */ const measurementServiceMappings_RectangleROIStartEndThreshold = (RectangleROIStartEndThreshold);
|
|
1650
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/measurementServiceMappings/measurementServiceMappingsFactory.js
|
|
1651
|
+
|
|
1652
|
+
const measurementServiceMappingsFactory = (measurementService, displaySetService, cornerstoneViewportService) => {
|
|
1653
|
+
return {
|
|
1654
|
+
RectangleROIStartEndThreshold: {
|
|
1655
|
+
toAnnotation: measurementServiceMappings_RectangleROIStartEndThreshold.toAnnotation,
|
|
1656
|
+
toMeasurement: csToolsAnnotation => measurementServiceMappings_RectangleROIStartEndThreshold.toMeasurement(csToolsAnnotation, displaySetService, cornerstoneViewportService),
|
|
1657
|
+
matchingCriteria: [{
|
|
1658
|
+
valueType: measurementService.VALUE_TYPES.ROI_THRESHOLD_MANUAL
|
|
1659
|
+
}]
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
};
|
|
1663
|
+
/* harmony default export */ const measurementServiceMappings_measurementServiceMappingsFactory = (measurementServiceMappingsFactory);
|
|
1664
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/init.js
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
|
1668
|
+
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
|
1669
|
+
/**
|
|
1670
|
+
*
|
|
1671
|
+
* @param {Object} servicesManager
|
|
1672
|
+
* @param {Object} configuration
|
|
1673
|
+
* @param {Object|Array} configuration.csToolsConfig
|
|
1674
|
+
*/
|
|
1675
|
+
function init({
|
|
1676
|
+
servicesManager
|
|
1677
|
+
}) {
|
|
1678
|
+
const {
|
|
1679
|
+
measurementService,
|
|
1680
|
+
displaySetService,
|
|
1681
|
+
cornerstoneViewportService
|
|
1682
|
+
} = servicesManager.services;
|
|
1683
|
+
(0,esm.addTool)(esm.RectangleROIStartEndThresholdTool);
|
|
1684
|
+
const {
|
|
1685
|
+
RectangleROIStartEndThreshold
|
|
1686
|
+
} = measurementServiceMappings_measurementServiceMappingsFactory(measurementService, displaySetService, cornerstoneViewportService);
|
|
1687
|
+
const csTools3DVer1MeasurementSource = measurementService.getSource(CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION);
|
|
1688
|
+
measurementService.addMapping(csTools3DVer1MeasurementSource, 'RectangleROIStartEndThreshold', RectangleROIStartEndThreshold.matchingCriteria, RectangleROIStartEndThreshold.toAnnotation, RectangleROIStartEndThreshold.toMeasurement);
|
|
1689
|
+
}
|
|
1690
|
+
// EXTERNAL MODULE: ../../../node_modules/gl-matrix/esm/index.js + 10 modules
|
|
1691
|
+
var gl_matrix_esm = __webpack_require__(83636);
|
|
1692
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/getThresholdValue.ts
|
|
1693
|
+
|
|
1694
|
+
function getRoiStats(referencedVolume, annotations) {
|
|
1695
|
+
// roiStats
|
|
1696
|
+
const {
|
|
1697
|
+
imageData
|
|
1698
|
+
} = referencedVolume;
|
|
1699
|
+
const values = imageData.getPointData().getScalars().getData();
|
|
1700
|
+
|
|
1701
|
+
// Todo: add support for other strategies
|
|
1702
|
+
const {
|
|
1703
|
+
fn,
|
|
1704
|
+
baseValue
|
|
1705
|
+
} = _getStrategyFn('max');
|
|
1706
|
+
let value = baseValue;
|
|
1707
|
+
const boundsIJK = esm.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations(annotations, referencedVolume);
|
|
1708
|
+
const [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK;
|
|
1709
|
+
for (let i = iMin; i <= iMax; i++) {
|
|
1710
|
+
for (let j = jMin; j <= jMax; j++) {
|
|
1711
|
+
for (let k = kMin; k <= kMax; k++) {
|
|
1712
|
+
const offset = imageData.computeOffsetIndex([i, j, k]);
|
|
1713
|
+
value = fn(values[offset], value);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return value;
|
|
1718
|
+
}
|
|
1719
|
+
function getThresholdValues(annotationUIDs, referencedVolumes, config) {
|
|
1720
|
+
if (config.strategy === 'range') {
|
|
1721
|
+
return {
|
|
1722
|
+
ptLower: Number(config.ptLower),
|
|
1723
|
+
ptUpper: Number(config.ptUpper),
|
|
1724
|
+
ctLower: Number(config.ctLower),
|
|
1725
|
+
ctUpper: Number(config.ctUpper)
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
const {
|
|
1729
|
+
weight
|
|
1730
|
+
} = config;
|
|
1731
|
+
const annotations = annotationUIDs.map(annotationUID => esm.annotation.state.getAnnotation(annotationUID));
|
|
1732
|
+
const ptValue = getRoiStats(referencedVolumes[0], annotations);
|
|
1733
|
+
return {
|
|
1734
|
+
ctLower: -Infinity,
|
|
1735
|
+
ctUpper: +Infinity,
|
|
1736
|
+
ptLower: weight * ptValue,
|
|
1737
|
+
ptUpper: +Infinity
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
function _getStrategyFn(statistic) {
|
|
1741
|
+
const baseValue = -Infinity;
|
|
1742
|
+
const fn = (number, maxValue) => {
|
|
1743
|
+
if (number > maxValue) {
|
|
1744
|
+
maxValue = number;
|
|
1745
|
+
}
|
|
1746
|
+
return maxValue;
|
|
1747
|
+
};
|
|
1748
|
+
return {
|
|
1749
|
+
fn,
|
|
1750
|
+
baseValue
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
/* harmony default export */ const getThresholdValue = (getThresholdValues);
|
|
1754
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/calculateSUVPeak.ts
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* This method calculates the SUV peak on a segmented ROI from a reference PET
|
|
1759
|
+
* volume. If a rectangle annotation is provided, the peak is calculated within that
|
|
1760
|
+
* rectangle. Otherwise, the calculation is performed on the entire volume which
|
|
1761
|
+
* will be slower but same result.
|
|
1762
|
+
* @param viewport Viewport to use for the calculation
|
|
1763
|
+
* @param labelmap Labelmap from which the mask is taken
|
|
1764
|
+
* @param referenceVolume PET volume to use for SUV calculation
|
|
1765
|
+
* @param toolData [Optional] list of toolData to use for SUV calculation
|
|
1766
|
+
* @param segmentIndex The index of the segment to use for masking
|
|
1767
|
+
* @returns
|
|
1768
|
+
*/
|
|
1769
|
+
function calculateSuvPeak(labelmap, referenceVolume, annotations, segmentIndex = 1) {
|
|
1770
|
+
if (referenceVolume.metadata.Modality !== 'PT') {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
if (labelmap.scalarData.length !== referenceVolume.scalarData.length) {
|
|
1774
|
+
throw new Error('labelmap and referenceVolume must have the same number of pixels');
|
|
1775
|
+
}
|
|
1776
|
+
const {
|
|
1777
|
+
scalarData: labelmapData,
|
|
1778
|
+
dimensions,
|
|
1779
|
+
imageData: labelmapImageData
|
|
1780
|
+
} = labelmap;
|
|
1781
|
+
const {
|
|
1782
|
+
scalarData: referenceVolumeData,
|
|
1783
|
+
imageData: referenceVolumeImageData
|
|
1784
|
+
} = referenceVolume;
|
|
1785
|
+
let boundsIJK;
|
|
1786
|
+
// Todo: using the first annotation for now
|
|
1787
|
+
if (annotations && annotations[0].data?.cachedStats) {
|
|
1788
|
+
const {
|
|
1789
|
+
projectionPoints
|
|
1790
|
+
} = annotations[0].data.cachedStats;
|
|
1791
|
+
const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now
|
|
1792
|
+
|
|
1793
|
+
const rectangleCornersIJK = pointsToUse.map(world => {
|
|
1794
|
+
const ijk = gl_matrix_esm/* vec3.fromValues */.eR.fromValues(0, 0, 0);
|
|
1795
|
+
referenceVolumeImageData.worldToIndex(world, ijk);
|
|
1796
|
+
return ijk;
|
|
1797
|
+
});
|
|
1798
|
+
boundsIJK = esm.utilities.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions);
|
|
1799
|
+
}
|
|
1800
|
+
let max = 0;
|
|
1801
|
+
let maxIJK = [0, 0, 0];
|
|
1802
|
+
let maxLPS = [0, 0, 0];
|
|
1803
|
+
const callback = ({
|
|
1804
|
+
pointIJK,
|
|
1805
|
+
pointLPS
|
|
1806
|
+
}) => {
|
|
1807
|
+
const offset = referenceVolumeImageData.computeOffsetIndex(pointIJK);
|
|
1808
|
+
const value = labelmapData[offset];
|
|
1809
|
+
if (value !== segmentIndex) {
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
const referenceValue = referenceVolumeData[offset];
|
|
1813
|
+
if (referenceValue > max) {
|
|
1814
|
+
max = referenceValue;
|
|
1815
|
+
maxIJK = pointIJK;
|
|
1816
|
+
maxLPS = pointLPS;
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
esm.utilities.pointInShapeCallback(labelmapImageData, () => true, callback, boundsIJK);
|
|
1820
|
+
const direction = labelmapImageData.getDirection().slice(0, 3);
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* 2. Find the bottom and top of the great circle for the second sphere (1cc sphere)
|
|
1824
|
+
* V = (4/3)πr3
|
|
1825
|
+
*/
|
|
1826
|
+
const radius = Math.pow(1 / (4 / 3 * Math.PI), 1 / 3) * 10;
|
|
1827
|
+
const diameter = radius * 2;
|
|
1828
|
+
const secondaryCircleWorld = gl_matrix_esm/* vec3.create */.eR.create();
|
|
1829
|
+
const bottomWorld = gl_matrix_esm/* vec3.create */.eR.create();
|
|
1830
|
+
const topWorld = gl_matrix_esm/* vec3.create */.eR.create();
|
|
1831
|
+
referenceVolumeImageData.indexToWorld(maxIJK, secondaryCircleWorld);
|
|
1832
|
+
gl_matrix_esm/* vec3.scaleAndAdd */.eR.scaleAndAdd(bottomWorld, secondaryCircleWorld, direction, -diameter / 2);
|
|
1833
|
+
gl_matrix_esm/* vec3.scaleAndAdd */.eR.scaleAndAdd(topWorld, secondaryCircleWorld, direction, diameter / 2);
|
|
1834
|
+
const suvPeakCirclePoints = [bottomWorld, topWorld];
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* 3. Find the Mean and Max of the 1cc sphere centered on the suv Max of the previous
|
|
1838
|
+
* sphere
|
|
1839
|
+
*/
|
|
1840
|
+
let count = 0;
|
|
1841
|
+
let acc = 0;
|
|
1842
|
+
const suvPeakMeanCallback = ({
|
|
1843
|
+
value
|
|
1844
|
+
}) => {
|
|
1845
|
+
acc += value;
|
|
1846
|
+
count += 1;
|
|
1847
|
+
};
|
|
1848
|
+
esm.utilities.pointInSurroundingSphereCallback(referenceVolumeImageData, suvPeakCirclePoints, suvPeakMeanCallback);
|
|
1849
|
+
const mean = acc / count;
|
|
1850
|
+
return {
|
|
1851
|
+
max,
|
|
1852
|
+
maxIJK,
|
|
1853
|
+
maxLPS,
|
|
1854
|
+
mean
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
/* harmony default export */ const calculateSUVPeak = (calculateSuvPeak);
|
|
1858
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/calculateTMTV.ts
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* Given a list of labelmaps (with the possibility of overlapping regions),
|
|
1863
|
+
* and a referenceVolume, it calculates the total metabolic tumor volume (TMTV)
|
|
1864
|
+
* by flattening and rasterizing each segment into a single labelmap and summing
|
|
1865
|
+
* the total number of volume voxels. It should be noted that for this calculation
|
|
1866
|
+
* we do not double count voxels that are part of multiple labelmaps.
|
|
1867
|
+
* @param {} labelmaps
|
|
1868
|
+
* @param {number} segmentIndex
|
|
1869
|
+
* @returns {number} TMTV in ml
|
|
1870
|
+
*/
|
|
1871
|
+
function calculateTMTV(labelmaps, segmentIndex = 1) {
|
|
1872
|
+
const volumeId = 'mergedLabelmap';
|
|
1873
|
+
const mergedLabelmap = esm.utilities.segmentation.createMergedLabelmapForIndex(labelmaps, segmentIndex, volumeId);
|
|
1874
|
+
const {
|
|
1875
|
+
imageData,
|
|
1876
|
+
spacing
|
|
1877
|
+
} = mergedLabelmap;
|
|
1878
|
+
const values = imageData.getPointData().getScalars().getData();
|
|
1879
|
+
|
|
1880
|
+
// count non-zero values inside the outputData, this would
|
|
1881
|
+
// consider the overlapping regions to be only counted once
|
|
1882
|
+
const numVoxels = values.reduce((acc, curr) => {
|
|
1883
|
+
if (curr > 0) {
|
|
1884
|
+
return acc + 1;
|
|
1885
|
+
}
|
|
1886
|
+
return acc;
|
|
1887
|
+
}, 0);
|
|
1888
|
+
return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
|
|
1889
|
+
}
|
|
1890
|
+
/* harmony default export */ const utils_calculateTMTV = (calculateTMTV);
|
|
1891
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/createAndDownloadTMTVReport.js
|
|
1892
|
+
function createAndDownloadTMTVReport(segReport, additionalReportRows) {
|
|
1893
|
+
const firstReport = segReport[Object.keys(segReport)[0]];
|
|
1894
|
+
const columns = Object.keys(firstReport);
|
|
1895
|
+
const csv = [columns.join(',')];
|
|
1896
|
+
Object.values(segReport).forEach(segmentation => {
|
|
1897
|
+
const row = [];
|
|
1898
|
+
columns.forEach(column => {
|
|
1899
|
+
// if it is array then we need to replace , with space to avoid csv parsing error
|
|
1900
|
+
row.push(Array.isArray(segmentation[column]) ? segmentation[column].join(' ') : segmentation[column]);
|
|
1901
|
+
});
|
|
1902
|
+
csv.push(row.join(','));
|
|
1903
|
+
});
|
|
1904
|
+
csv.push('');
|
|
1905
|
+
csv.push('');
|
|
1906
|
+
csv.push('');
|
|
1907
|
+
csv.push(`Patient ID,${firstReport.PatientID}`);
|
|
1908
|
+
csv.push(`Study Date,${firstReport.StudyDate}`);
|
|
1909
|
+
csv.push('');
|
|
1910
|
+
additionalReportRows.forEach(({
|
|
1911
|
+
key,
|
|
1912
|
+
value: values
|
|
1913
|
+
}) => {
|
|
1914
|
+
const temp = [];
|
|
1915
|
+
temp.push(`${key}`);
|
|
1916
|
+
Object.keys(values).forEach(k => {
|
|
1917
|
+
temp.push(`${k}`);
|
|
1918
|
+
temp.push(`${values[k]}`);
|
|
1919
|
+
});
|
|
1920
|
+
csv.push(temp.join(','));
|
|
1921
|
+
});
|
|
1922
|
+
const blob = new Blob([csv.join('\n')], {
|
|
1923
|
+
type: 'text/csv;charset=utf-8'
|
|
1924
|
+
});
|
|
1925
|
+
const url = URL.createObjectURL(blob);
|
|
1926
|
+
const a = document.createElement('a');
|
|
1927
|
+
a.href = url;
|
|
1928
|
+
a.download = `${firstReport.PatientID}_tmtv.csv`;
|
|
1929
|
+
a.click();
|
|
1930
|
+
}
|
|
1931
|
+
// EXTERNAL MODULE: ../../../node_modules/dcmjs/build/dcmjs.es.js
|
|
1932
|
+
var dcmjs_es = __webpack_require__(31426);
|
|
1933
|
+
// EXTERNAL MODULE: ../../../node_modules/@cornerstonejs/adapters/dist/adapters.es.js
|
|
1934
|
+
var adapters_es = __webpack_require__(83342);
|
|
1935
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/dicomRTAnnotationExport.js
|
|
1936
|
+
|
|
1937
|
+
|
|
1938
|
+
|
|
1939
|
+
const {
|
|
1940
|
+
datasetToBlob
|
|
1941
|
+
} = dcmjs_es/* default.data */.Ay.data;
|
|
1942
|
+
const metadataProvider = core_src.classes.MetadataProvider;
|
|
1943
|
+
function dicomRTAnnotationExport(annotations) {
|
|
1944
|
+
const dataset = adapters_es/* adaptersRT */.f_.Cornerstone3D.RTSS.generateRTSSFromAnnotations(annotations, metadataProvider, core_src.DicomMetadataStore);
|
|
1945
|
+
const reportBlob = datasetToBlob(dataset);
|
|
1946
|
+
|
|
1947
|
+
//Create a URL for the binary.
|
|
1948
|
+
var objectUrl = URL.createObjectURL(reportBlob);
|
|
1949
|
+
window.location.assign(objectUrl);
|
|
1950
|
+
}
|
|
1951
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/utils/dicomRTAnnotationExport/RTStructureSet/index.js
|
|
1952
|
+
|
|
1953
|
+
/* harmony default export */ const RTStructureSet = (dicomRTAnnotationExport);
|
|
1954
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/commandsModule.js
|
|
1955
|
+
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
const commandsModule_metadataProvider = core_src.classes.MetadataProvider;
|
|
1966
|
+
const RECTANGLE_ROI_THRESHOLD_MANUAL = 'RectangleROIStartEndThreshold';
|
|
1967
|
+
const LABELMAP = esm.Enums.SegmentationRepresentations.Labelmap;
|
|
1968
|
+
const commandsModule = ({
|
|
1969
|
+
servicesManager,
|
|
1970
|
+
commandsManager,
|
|
1971
|
+
extensionManager
|
|
1972
|
+
}) => {
|
|
1973
|
+
const {
|
|
1974
|
+
viewportGridService,
|
|
1975
|
+
uiNotificationService,
|
|
1976
|
+
displaySetService,
|
|
1977
|
+
hangingProtocolService,
|
|
1978
|
+
toolGroupService,
|
|
1979
|
+
cornerstoneViewportService,
|
|
1980
|
+
segmentationService
|
|
1981
|
+
} = servicesManager.services;
|
|
1982
|
+
const utilityModule = extensionManager.getModuleEntry('@ohif/extension-cornerstone.utilityModule.common');
|
|
1983
|
+
const {
|
|
1984
|
+
getEnabledElement
|
|
1985
|
+
} = utilityModule.exports;
|
|
1986
|
+
function _getActiveViewportsEnabledElement() {
|
|
1987
|
+
const {
|
|
1988
|
+
activeViewportId
|
|
1989
|
+
} = viewportGridService.getState();
|
|
1990
|
+
const {
|
|
1991
|
+
element
|
|
1992
|
+
} = getEnabledElement(activeViewportId) || {};
|
|
1993
|
+
const enabledElement = dist_esm.getEnabledElement(element);
|
|
1994
|
+
return enabledElement;
|
|
1995
|
+
}
|
|
1996
|
+
function _getMatchedViewportsToolGroupIds() {
|
|
1997
|
+
const {
|
|
1998
|
+
viewportMatchDetails
|
|
1999
|
+
} = hangingProtocolService.getMatchDetails();
|
|
2000
|
+
const toolGroupIds = [];
|
|
2001
|
+
viewportMatchDetails.forEach(viewport => {
|
|
2002
|
+
const {
|
|
2003
|
+
viewportOptions
|
|
2004
|
+
} = viewport;
|
|
2005
|
+
const {
|
|
2006
|
+
toolGroupId
|
|
2007
|
+
} = viewportOptions;
|
|
2008
|
+
if (toolGroupIds.indexOf(toolGroupId) === -1) {
|
|
2009
|
+
toolGroupIds.push(toolGroupId);
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
return toolGroupIds;
|
|
2013
|
+
}
|
|
2014
|
+
const actions = {
|
|
2015
|
+
getMatchingPTDisplaySet: ({
|
|
2016
|
+
viewportMatchDetails
|
|
2017
|
+
}) => {
|
|
2018
|
+
// Todo: this is assuming that the hanging protocol has successfully matched
|
|
2019
|
+
// the correct PT. For future, we should have a way to filter out the PTs
|
|
2020
|
+
// that are in the viewer layout (but then we have the problem of the attenuation
|
|
2021
|
+
// corrected PT vs the non-attenuation correct PT)
|
|
2022
|
+
|
|
2023
|
+
let ptDisplaySet = null;
|
|
2024
|
+
for (const [viewportId, viewportDetails] of viewportMatchDetails) {
|
|
2025
|
+
const {
|
|
2026
|
+
displaySetsInfo
|
|
2027
|
+
} = viewportDetails;
|
|
2028
|
+
const displaySets = displaySetsInfo.map(({
|
|
2029
|
+
displaySetInstanceUID
|
|
2030
|
+
}) => displaySetService.getDisplaySetByUID(displaySetInstanceUID));
|
|
2031
|
+
if (!displaySets || displaySets.length === 0) {
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
ptDisplaySet = displaySets.find(displaySet => displaySet.Modality === 'PT');
|
|
2035
|
+
if (ptDisplaySet) {
|
|
2036
|
+
break;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
return ptDisplaySet;
|
|
2040
|
+
},
|
|
2041
|
+
getPTMetadata: ({
|
|
2042
|
+
ptDisplaySet
|
|
2043
|
+
}) => {
|
|
2044
|
+
const dataSource = extensionManager.getDataSources()[0];
|
|
2045
|
+
const imageIds = dataSource.getImageIdsForDisplaySet(ptDisplaySet);
|
|
2046
|
+
const firstImageId = imageIds[0];
|
|
2047
|
+
const instance = commandsModule_metadataProvider.get('instance', firstImageId);
|
|
2048
|
+
if (instance.Modality !== 'PT') {
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const metadata = {
|
|
2052
|
+
SeriesTime: instance.SeriesTime,
|
|
2053
|
+
Modality: instance.Modality,
|
|
2054
|
+
PatientSex: instance.PatientSex,
|
|
2055
|
+
PatientWeight: instance.PatientWeight,
|
|
2056
|
+
RadiopharmaceuticalInformationSequence: {
|
|
2057
|
+
RadionuclideTotalDose: instance.RadiopharmaceuticalInformationSequence[0].RadionuclideTotalDose,
|
|
2058
|
+
RadionuclideHalfLife: instance.RadiopharmaceuticalInformationSequence[0].RadionuclideHalfLife,
|
|
2059
|
+
RadiopharmaceuticalStartTime: instance.RadiopharmaceuticalInformationSequence[0].RadiopharmaceuticalStartTime,
|
|
2060
|
+
RadiopharmaceuticalStartDateTime: instance.RadiopharmaceuticalInformationSequence[0].RadiopharmaceuticalStartDateTime
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
return metadata;
|
|
2064
|
+
},
|
|
2065
|
+
createNewLabelmapFromPT: async () => {
|
|
2066
|
+
// Create a segmentation of the same resolution as the source data
|
|
2067
|
+
// using volumeLoader.createAndCacheDerivedVolume.
|
|
2068
|
+
const {
|
|
2069
|
+
viewportMatchDetails
|
|
2070
|
+
} = hangingProtocolService.getMatchDetails();
|
|
2071
|
+
const ptDisplaySet = actions.getMatchingPTDisplaySet({
|
|
2072
|
+
viewportMatchDetails
|
|
2073
|
+
});
|
|
2074
|
+
if (!ptDisplaySet) {
|
|
2075
|
+
uiNotificationService.error('No matching PT display set found');
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
const currentSegmentations = segmentationService.getSegmentations();
|
|
2079
|
+
const segmentationId = await segmentationService.createSegmentationForDisplaySet(ptDisplaySet.displaySetInstanceUID, {
|
|
2080
|
+
label: `Segmentation ${currentSegmentations.length + 1}`
|
|
2081
|
+
});
|
|
2082
|
+
|
|
2083
|
+
// Add Segmentation to all toolGroupIds in the viewer
|
|
2084
|
+
const toolGroupIds = _getMatchedViewportsToolGroupIds();
|
|
2085
|
+
const representationType = LABELMAP;
|
|
2086
|
+
for (const toolGroupId of toolGroupIds) {
|
|
2087
|
+
const hydrateSegmentation = true;
|
|
2088
|
+
await segmentationService.addSegmentationRepresentationToToolGroup(toolGroupId, segmentationId, hydrateSegmentation, representationType);
|
|
2089
|
+
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
|
2090
|
+
}
|
|
2091
|
+
segmentationService.addSegment(segmentationId, {
|
|
2092
|
+
segmentIndex: 1,
|
|
2093
|
+
properties: {
|
|
2094
|
+
label: 'Segment 1'
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
return segmentationId;
|
|
2098
|
+
},
|
|
2099
|
+
setSegmentationActiveForToolGroups: ({
|
|
2100
|
+
segmentationId
|
|
2101
|
+
}) => {
|
|
2102
|
+
const toolGroupIds = _getMatchedViewportsToolGroupIds();
|
|
2103
|
+
toolGroupIds.forEach(toolGroupId => {
|
|
2104
|
+
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
|
2105
|
+
});
|
|
2106
|
+
},
|
|
2107
|
+
thresholdSegmentationByRectangleROITool: ({
|
|
2108
|
+
segmentationId,
|
|
2109
|
+
config,
|
|
2110
|
+
segmentIndex
|
|
2111
|
+
}) => {
|
|
2112
|
+
const segmentation = esm.segmentation.state.getSegmentation(segmentationId);
|
|
2113
|
+
const {
|
|
2114
|
+
representationData
|
|
2115
|
+
} = segmentation;
|
|
2116
|
+
const {
|
|
2117
|
+
displaySetMatchDetails: matchDetails
|
|
2118
|
+
} = hangingProtocolService.getMatchDetails();
|
|
2119
|
+
const volumeLoaderScheme = 'cornerstoneStreamingImageVolume'; // Loader id which defines which volume loader to use
|
|
2120
|
+
|
|
2121
|
+
const ctDisplaySet = matchDetails.get('ctDisplaySet');
|
|
2122
|
+
const ctVolumeId = `${volumeLoaderScheme}:${ctDisplaySet.displaySetInstanceUID}`; // VolumeId with loader id + volume id
|
|
2123
|
+
|
|
2124
|
+
const {
|
|
2125
|
+
volumeId: segVolumeId
|
|
2126
|
+
} = representationData[LABELMAP];
|
|
2127
|
+
const {
|
|
2128
|
+
referencedVolumeId
|
|
2129
|
+
} = dist_esm.cache.getVolume(segVolumeId);
|
|
2130
|
+
const labelmapVolume = dist_esm.cache.getVolume(segmentationId);
|
|
2131
|
+
const referencedVolume = dist_esm.cache.getVolume(referencedVolumeId);
|
|
2132
|
+
const ctReferencedVolume = dist_esm.cache.getVolume(ctVolumeId);
|
|
2133
|
+
if (!referencedVolume) {
|
|
2134
|
+
throw new Error('No Reference volume found');
|
|
2135
|
+
}
|
|
2136
|
+
if (!labelmapVolume) {
|
|
2137
|
+
throw new Error('No Reference labelmap found');
|
|
2138
|
+
}
|
|
2139
|
+
const annotationUIDs = esm.annotation.selection.getAnnotationsSelectedByToolName(RECTANGLE_ROI_THRESHOLD_MANUAL);
|
|
2140
|
+
if (annotationUIDs.length === 0) {
|
|
2141
|
+
uiNotificationService.show({
|
|
2142
|
+
title: 'Commands Module',
|
|
2143
|
+
message: 'No ROIThreshold Tool is Selected',
|
|
2144
|
+
type: 'error'
|
|
2145
|
+
});
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
const {
|
|
2149
|
+
ptLower,
|
|
2150
|
+
ptUpper,
|
|
2151
|
+
ctLower,
|
|
2152
|
+
ctUpper
|
|
2153
|
+
} = getThresholdValue(annotationUIDs, [referencedVolume, ctReferencedVolume], config);
|
|
2154
|
+
return esm.utilities.segmentation.rectangleROIThresholdVolumeByRange(annotationUIDs, labelmapVolume, [{
|
|
2155
|
+
volume: referencedVolume,
|
|
2156
|
+
lower: ptLower,
|
|
2157
|
+
upper: ptUpper
|
|
2158
|
+
}, {
|
|
2159
|
+
volume: ctReferencedVolume,
|
|
2160
|
+
lower: ctLower,
|
|
2161
|
+
upper: ctUpper
|
|
2162
|
+
}], {
|
|
2163
|
+
overwrite: true,
|
|
2164
|
+
segmentIndex
|
|
2165
|
+
});
|
|
2166
|
+
},
|
|
2167
|
+
calculateSuvPeak: ({
|
|
2168
|
+
labelmap,
|
|
2169
|
+
segmentIndex
|
|
2170
|
+
}) => {
|
|
2171
|
+
const {
|
|
2172
|
+
referencedVolumeId
|
|
2173
|
+
} = labelmap;
|
|
2174
|
+
const referencedVolume = dist_esm.cache.getVolume(referencedVolumeId);
|
|
2175
|
+
const annotationUIDs = esm.annotation.selection.getAnnotationsSelectedByToolName(RECTANGLE_ROI_THRESHOLD_MANUAL);
|
|
2176
|
+
const annotations = annotationUIDs.map(annotationUID => esm.annotation.state.getAnnotation(annotationUID));
|
|
2177
|
+
const suvPeak = calculateSUVPeak(labelmap, referencedVolume, annotations, segmentIndex);
|
|
2178
|
+
return {
|
|
2179
|
+
suvPeak: suvPeak.mean,
|
|
2180
|
+
suvMax: suvPeak.max,
|
|
2181
|
+
suvMaxIJK: suvPeak.maxIJK,
|
|
2182
|
+
suvMaxLPS: suvPeak.maxLPS
|
|
2183
|
+
};
|
|
2184
|
+
},
|
|
2185
|
+
getLesionStats: ({
|
|
2186
|
+
labelmap,
|
|
2187
|
+
segmentIndex = 1
|
|
2188
|
+
}) => {
|
|
2189
|
+
const {
|
|
2190
|
+
scalarData,
|
|
2191
|
+
spacing
|
|
2192
|
+
} = labelmap;
|
|
2193
|
+
const {
|
|
2194
|
+
scalarData: referencedScalarData
|
|
2195
|
+
} = dist_esm.cache.getVolume(labelmap.referencedVolumeId);
|
|
2196
|
+
let segmentationMax = -Infinity;
|
|
2197
|
+
let segmentationMin = Infinity;
|
|
2198
|
+
let segmentationValues = [];
|
|
2199
|
+
let voxelCount = 0;
|
|
2200
|
+
for (let i = 0; i < scalarData.length; i++) {
|
|
2201
|
+
if (scalarData[i] === segmentIndex) {
|
|
2202
|
+
const value = referencedScalarData[i];
|
|
2203
|
+
segmentationValues.push(value);
|
|
2204
|
+
if (value > segmentationMax) {
|
|
2205
|
+
segmentationMax = value;
|
|
2206
|
+
}
|
|
2207
|
+
if (value < segmentationMin) {
|
|
2208
|
+
segmentationMin = value;
|
|
2209
|
+
}
|
|
2210
|
+
voxelCount++;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
const stats = {
|
|
2214
|
+
minValue: segmentationMin,
|
|
2215
|
+
maxValue: segmentationMax,
|
|
2216
|
+
meanValue: segmentationValues.reduce((a, b) => a + b, 0) / voxelCount,
|
|
2217
|
+
stdValue: Math.sqrt(segmentationValues.reduce((a, b) => a + b * b, 0) / voxelCount - segmentationValues.reduce((a, b) => a + b, 0) / voxelCount ** 2),
|
|
2218
|
+
volume: voxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3
|
|
2219
|
+
};
|
|
2220
|
+
return stats;
|
|
2221
|
+
},
|
|
2222
|
+
calculateLesionGlycolysis: ({
|
|
2223
|
+
lesionStats
|
|
2224
|
+
}) => {
|
|
2225
|
+
const {
|
|
2226
|
+
meanValue,
|
|
2227
|
+
volume
|
|
2228
|
+
} = lesionStats;
|
|
2229
|
+
return {
|
|
2230
|
+
lesionGlyoclysisStats: volume * meanValue
|
|
2231
|
+
};
|
|
2232
|
+
},
|
|
2233
|
+
calculateTMTV: ({
|
|
2234
|
+
segmentations
|
|
2235
|
+
}) => {
|
|
2236
|
+
const labelmaps = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
|
|
2237
|
+
if (!labelmaps.length) {
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
return utils_calculateTMTV(labelmaps);
|
|
2241
|
+
},
|
|
2242
|
+
exportTMTVReportCSV: ({
|
|
2243
|
+
segmentations,
|
|
2244
|
+
tmtv,
|
|
2245
|
+
config
|
|
2246
|
+
}) => {
|
|
2247
|
+
const segReport = commandsManager.runCommand('getSegmentationCSVReport', {
|
|
2248
|
+
segmentations
|
|
2249
|
+
});
|
|
2250
|
+
const tlg = actions.getTotalLesionGlycolysis({
|
|
2251
|
+
segmentations
|
|
2252
|
+
});
|
|
2253
|
+
const additionalReportRows = [{
|
|
2254
|
+
key: 'Total Metabolic Tumor Volume',
|
|
2255
|
+
value: {
|
|
2256
|
+
tmtv
|
|
2257
|
+
}
|
|
2258
|
+
}, {
|
|
2259
|
+
key: 'Total Lesion Glycolysis',
|
|
2260
|
+
value: {
|
|
2261
|
+
tlg: tlg.toFixed(4)
|
|
2262
|
+
}
|
|
2263
|
+
}, {
|
|
2264
|
+
key: 'Threshold Configuration',
|
|
2265
|
+
value: {
|
|
2266
|
+
...config
|
|
2267
|
+
}
|
|
2268
|
+
}];
|
|
2269
|
+
createAndDownloadTMTVReport(segReport, additionalReportRows);
|
|
2270
|
+
},
|
|
2271
|
+
getTotalLesionGlycolysis: ({
|
|
2272
|
+
segmentations
|
|
2273
|
+
}) => {
|
|
2274
|
+
const labelmapVolumes = segmentations.map(s => segmentationService.getLabelmapVolume(s.id));
|
|
2275
|
+
let mergedLabelmap;
|
|
2276
|
+
// merge labelmap will through an error if labels maps are not the same size
|
|
2277
|
+
// or same direction or ....
|
|
2278
|
+
try {
|
|
2279
|
+
mergedLabelmap = esm.utilities.segmentation.createMergedLabelmapForIndex(labelmapVolumes);
|
|
2280
|
+
} catch (e) {
|
|
2281
|
+
console.error('commandsModule::getTotalLesionGlycolysis', e);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// grabbing the first labelmap referenceVolume since it will be the same for all
|
|
2286
|
+
const {
|
|
2287
|
+
referencedVolumeId,
|
|
2288
|
+
spacing
|
|
2289
|
+
} = labelmapVolumes[0];
|
|
2290
|
+
if (!referencedVolumeId) {
|
|
2291
|
+
console.error('commandsModule::getTotalLesionGlycolysis:No referencedVolumeId found');
|
|
2292
|
+
}
|
|
2293
|
+
const ptVolume = dist_esm.cache.getVolume(referencedVolumeId);
|
|
2294
|
+
const mergedLabelData = mergedLabelmap.scalarData;
|
|
2295
|
+
if (mergedLabelData.length !== ptVolume.scalarData.length) {
|
|
2296
|
+
console.error('commandsModule::getTotalLesionGlycolysis:Labelmap and ptVolume are not the same size');
|
|
2297
|
+
}
|
|
2298
|
+
let suv = 0;
|
|
2299
|
+
let totalLesionVoxelCount = 0;
|
|
2300
|
+
for (let i = 0; i < mergedLabelData.length; i++) {
|
|
2301
|
+
// if not background
|
|
2302
|
+
if (mergedLabelData[i] !== 0) {
|
|
2303
|
+
suv += ptVolume.scalarData[i];
|
|
2304
|
+
totalLesionVoxelCount += 1;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// Average SUV for the merged labelmap
|
|
2309
|
+
const averageSuv = suv / totalLesionVoxelCount;
|
|
2310
|
+
|
|
2311
|
+
// total Lesion Glycolysis [suv * ml]
|
|
2312
|
+
return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3;
|
|
2313
|
+
},
|
|
2314
|
+
setStartSliceForROIThresholdTool: () => {
|
|
2315
|
+
const {
|
|
2316
|
+
viewport
|
|
2317
|
+
} = _getActiveViewportsEnabledElement();
|
|
2318
|
+
const {
|
|
2319
|
+
focalPoint,
|
|
2320
|
+
viewPlaneNormal
|
|
2321
|
+
} = viewport.getCamera();
|
|
2322
|
+
const selectedAnnotationUIDs = esm.annotation.selection.getAnnotationsSelectedByToolName(RECTANGLE_ROI_THRESHOLD_MANUAL);
|
|
2323
|
+
const annotationUID = selectedAnnotationUIDs[0];
|
|
2324
|
+
const annotation = esm.annotation.state.getAnnotation(annotationUID);
|
|
2325
|
+
const {
|
|
2326
|
+
handles
|
|
2327
|
+
} = annotation.data;
|
|
2328
|
+
const {
|
|
2329
|
+
points
|
|
2330
|
+
} = handles;
|
|
2331
|
+
|
|
2332
|
+
// get the current slice Index
|
|
2333
|
+
const sliceIndex = viewport.getCurrentImageIdIndex();
|
|
2334
|
+
annotation.data.startSlice = sliceIndex;
|
|
2335
|
+
|
|
2336
|
+
// distance between camera focal point and each point on the rectangle
|
|
2337
|
+
const newPoints = points.map(point => {
|
|
2338
|
+
const distance = gl_matrix_esm/* vec3.create */.eR.create();
|
|
2339
|
+
gl_matrix_esm/* vec3.subtract */.eR.subtract(distance, focalPoint, point);
|
|
2340
|
+
// distance in the direction of the viewPlaneNormal
|
|
2341
|
+
const distanceInViewPlane = gl_matrix_esm/* vec3.dot */.eR.dot(distance, viewPlaneNormal);
|
|
2342
|
+
// new point is current point minus distanceInViewPlane
|
|
2343
|
+
const newPoint = gl_matrix_esm/* vec3.create */.eR.create();
|
|
2344
|
+
gl_matrix_esm/* vec3.scaleAndAdd */.eR.scaleAndAdd(newPoint, point, viewPlaneNormal, distanceInViewPlane);
|
|
2345
|
+
return newPoint;
|
|
2346
|
+
//
|
|
2347
|
+
});
|
|
2348
|
+
handles.points = newPoints;
|
|
2349
|
+
// IMPORTANT: invalidate the toolData for the cached stat to get updated
|
|
2350
|
+
// and re-calculate the projection points
|
|
2351
|
+
annotation.invalidated = true;
|
|
2352
|
+
viewport.render();
|
|
2353
|
+
},
|
|
2354
|
+
setEndSliceForROIThresholdTool: () => {
|
|
2355
|
+
const {
|
|
2356
|
+
viewport
|
|
2357
|
+
} = _getActiveViewportsEnabledElement();
|
|
2358
|
+
const selectedAnnotationUIDs = esm.annotation.selection.getAnnotationsSelectedByToolName(RECTANGLE_ROI_THRESHOLD_MANUAL);
|
|
2359
|
+
const annotationUID = selectedAnnotationUIDs[0];
|
|
2360
|
+
const annotation = esm.annotation.state.getAnnotation(annotationUID);
|
|
2361
|
+
|
|
2362
|
+
// get the current slice Index
|
|
2363
|
+
const sliceIndex = viewport.getCurrentImageIdIndex();
|
|
2364
|
+
annotation.data.endSlice = sliceIndex;
|
|
2365
|
+
|
|
2366
|
+
// IMPORTANT: invalidate the toolData for the cached stat to get updated
|
|
2367
|
+
// and re-calculate the projection points
|
|
2368
|
+
annotation.invalidated = true;
|
|
2369
|
+
viewport.render();
|
|
2370
|
+
},
|
|
2371
|
+
createTMTVRTReport: () => {
|
|
2372
|
+
// get all Rectangle ROI annotation
|
|
2373
|
+
const stateManager = esm.annotation.state.getAnnotationManager();
|
|
2374
|
+
const annotations = [];
|
|
2375
|
+
Object.keys(stateManager.annotations).forEach(frameOfReferenceUID => {
|
|
2376
|
+
const forAnnotations = stateManager.annotations[frameOfReferenceUID];
|
|
2377
|
+
const ROIAnnotations = forAnnotations[RECTANGLE_ROI_THRESHOLD_MANUAL];
|
|
2378
|
+
annotations.push(...ROIAnnotations);
|
|
2379
|
+
});
|
|
2380
|
+
commandsManager.runCommand('exportRTReportForAnnotations', {
|
|
2381
|
+
annotations
|
|
2382
|
+
});
|
|
2383
|
+
},
|
|
2384
|
+
getSegmentationCSVReport: ({
|
|
2385
|
+
segmentations
|
|
2386
|
+
}) => {
|
|
2387
|
+
if (!segmentations || !segmentations.length) {
|
|
2388
|
+
segmentations = segmentationService.getSegmentations();
|
|
2389
|
+
}
|
|
2390
|
+
let report = {};
|
|
2391
|
+
for (const segmentation of segmentations) {
|
|
2392
|
+
const {
|
|
2393
|
+
id,
|
|
2394
|
+
label,
|
|
2395
|
+
cachedStats: data
|
|
2396
|
+
} = segmentation;
|
|
2397
|
+
const segReport = {
|
|
2398
|
+
id,
|
|
2399
|
+
label
|
|
2400
|
+
};
|
|
2401
|
+
if (!data) {
|
|
2402
|
+
report[id] = segReport;
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
Object.keys(data).forEach(key => {
|
|
2406
|
+
if (typeof data[key] !== 'object') {
|
|
2407
|
+
segReport[key] = data[key];
|
|
2408
|
+
} else {
|
|
2409
|
+
Object.keys(data[key]).forEach(subKey => {
|
|
2410
|
+
const newKey = `${key}_${subKey}`;
|
|
2411
|
+
segReport[newKey] = data[key][subKey];
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
});
|
|
2415
|
+
const labelmapVolume = segmentationService.getLabelmapVolume(id);
|
|
2416
|
+
if (!labelmapVolume) {
|
|
2417
|
+
report[id] = segReport;
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
const referencedVolumeId = labelmapVolume.referencedVolumeId;
|
|
2421
|
+
segReport.referencedVolumeId = referencedVolumeId;
|
|
2422
|
+
const referencedVolume = segmentationService.getLabelmapVolume(referencedVolumeId);
|
|
2423
|
+
if (!referencedVolume) {
|
|
2424
|
+
report[id] = segReport;
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
if (!referencedVolume.imageIds || !referencedVolume.imageIds.length) {
|
|
2428
|
+
report[id] = segReport;
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2431
|
+
const firstImageId = referencedVolume.imageIds[0];
|
|
2432
|
+
const instance = core_src/* default.classes */.Ay.classes.MetadataProvider.get('instance', firstImageId);
|
|
2433
|
+
if (!instance) {
|
|
2434
|
+
report[id] = segReport;
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
report[id] = {
|
|
2438
|
+
...segReport,
|
|
2439
|
+
PatientID: instance.PatientID,
|
|
2440
|
+
PatientName: instance.PatientName.Alphabetic,
|
|
2441
|
+
StudyInstanceUID: instance.StudyInstanceUID,
|
|
2442
|
+
SeriesInstanceUID: instance.SeriesInstanceUID,
|
|
2443
|
+
StudyDate: instance.StudyDate
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
return report;
|
|
2447
|
+
},
|
|
2448
|
+
exportRTReportForAnnotations: ({
|
|
2449
|
+
annotations
|
|
2450
|
+
}) => {
|
|
2451
|
+
RTStructureSet(annotations);
|
|
2452
|
+
},
|
|
2453
|
+
setFusionPTColormap: ({
|
|
2454
|
+
toolGroupId,
|
|
2455
|
+
colormap
|
|
2456
|
+
}) => {
|
|
2457
|
+
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
|
2458
|
+
const {
|
|
2459
|
+
viewportMatchDetails
|
|
2460
|
+
} = hangingProtocolService.getMatchDetails();
|
|
2461
|
+
const ptDisplaySet = actions.getMatchingPTDisplaySet({
|
|
2462
|
+
viewportMatchDetails
|
|
2463
|
+
});
|
|
2464
|
+
if (!ptDisplaySet) {
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
const fusionViewportIds = toolGroup.getViewportIds();
|
|
2468
|
+
let viewports = [];
|
|
2469
|
+
fusionViewportIds.forEach(viewportId => {
|
|
2470
|
+
commandsManager.runCommand('setViewportColormap', {
|
|
2471
|
+
viewportId,
|
|
2472
|
+
displaySetInstanceUID: ptDisplaySet.displaySetInstanceUID,
|
|
2473
|
+
colormap: {
|
|
2474
|
+
name: colormap
|
|
2475
|
+
}
|
|
2476
|
+
});
|
|
2477
|
+
viewports.push(cornerstoneViewportService.getCornerstoneViewport(viewportId));
|
|
2478
|
+
});
|
|
2479
|
+
viewports.forEach(viewport => {
|
|
2480
|
+
viewport.render();
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
const definitions = {
|
|
2485
|
+
setEndSliceForROIThresholdTool: {
|
|
2486
|
+
commandFn: actions.setEndSliceForROIThresholdTool
|
|
2487
|
+
},
|
|
2488
|
+
setStartSliceForROIThresholdTool: {
|
|
2489
|
+
commandFn: actions.setStartSliceForROIThresholdTool
|
|
2490
|
+
},
|
|
2491
|
+
getMatchingPTDisplaySet: {
|
|
2492
|
+
commandFn: actions.getMatchingPTDisplaySet
|
|
2493
|
+
},
|
|
2494
|
+
getPTMetadata: {
|
|
2495
|
+
commandFn: actions.getPTMetadata
|
|
2496
|
+
},
|
|
2497
|
+
createNewLabelmapFromPT: {
|
|
2498
|
+
commandFn: actions.createNewLabelmapFromPT
|
|
2499
|
+
},
|
|
2500
|
+
setSegmentationActiveForToolGroups: {
|
|
2501
|
+
commandFn: actions.setSegmentationActiveForToolGroups
|
|
2502
|
+
},
|
|
2503
|
+
thresholdSegmentationByRectangleROITool: {
|
|
2504
|
+
commandFn: actions.thresholdSegmentationByRectangleROITool
|
|
2505
|
+
},
|
|
2506
|
+
getTotalLesionGlycolysis: {
|
|
2507
|
+
commandFn: actions.getTotalLesionGlycolysis
|
|
2508
|
+
},
|
|
2509
|
+
calculateSuvPeak: {
|
|
2510
|
+
commandFn: actions.calculateSuvPeak
|
|
2511
|
+
},
|
|
2512
|
+
getLesionStats: {
|
|
2513
|
+
commandFn: actions.getLesionStats
|
|
2514
|
+
},
|
|
2515
|
+
calculateTMTV: {
|
|
2516
|
+
commandFn: actions.calculateTMTV
|
|
2517
|
+
},
|
|
2518
|
+
exportTMTVReportCSV: {
|
|
2519
|
+
commandFn: actions.exportTMTVReportCSV
|
|
2520
|
+
},
|
|
2521
|
+
createTMTVRTReport: {
|
|
2522
|
+
commandFn: actions.createTMTVRTReport
|
|
2523
|
+
},
|
|
2524
|
+
getSegmentationCSVReport: {
|
|
2525
|
+
commandFn: actions.getSegmentationCSVReport
|
|
2526
|
+
},
|
|
2527
|
+
exportRTReportForAnnotations: {
|
|
2528
|
+
commandFn: actions.exportRTReportForAnnotations
|
|
2529
|
+
},
|
|
2530
|
+
setFusionPTColormap: {
|
|
2531
|
+
commandFn: actions.setFusionPTColormap
|
|
2532
|
+
}
|
|
2533
|
+
};
|
|
2534
|
+
return {
|
|
2535
|
+
actions,
|
|
2536
|
+
definitions,
|
|
2537
|
+
defaultContext: 'TMTV:CORNERSTONE'
|
|
2538
|
+
};
|
|
2539
|
+
};
|
|
2540
|
+
/* harmony default export */ const src_commandsModule = (commandsModule);
|
|
2541
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/PanelROIThresholdSegmentation/ROIThresholdConfiguration.tsx
|
|
2542
|
+
|
|
2543
|
+
|
|
2544
|
+
|
|
2545
|
+
const ROI_STAT = 'roi_stat';
|
|
2546
|
+
const RANGE = 'range';
|
|
2547
|
+
const options = [{
|
|
2548
|
+
value: ROI_STAT,
|
|
2549
|
+
label: 'Max',
|
|
2550
|
+
placeHolder: 'Max'
|
|
2551
|
+
}, {
|
|
2552
|
+
value: RANGE,
|
|
2553
|
+
label: 'Range',
|
|
2554
|
+
placeHolder: 'Range'
|
|
2555
|
+
}];
|
|
2556
|
+
function ROIThresholdConfiguration({
|
|
2557
|
+
config,
|
|
2558
|
+
dispatch,
|
|
2559
|
+
runCommand
|
|
2560
|
+
}) {
|
|
2561
|
+
const {
|
|
2562
|
+
t
|
|
2563
|
+
} = (0,es/* useTranslation */.Bd)('ROIThresholdConfiguration');
|
|
2564
|
+
return /*#__PURE__*/react.createElement("div", {
|
|
2565
|
+
className: "bg-primary-dark flex flex-col space-y-4"
|
|
2566
|
+
}, /*#__PURE__*/react.createElement("div", {
|
|
2567
|
+
className: "flex items-end space-x-2"
|
|
2568
|
+
}, /*#__PURE__*/react.createElement("div", {
|
|
2569
|
+
className: "flex w-1/2 flex-col"
|
|
2570
|
+
}, /*#__PURE__*/react.createElement(src/* Select */.l6, {
|
|
2571
|
+
label: t('Strategy'),
|
|
2572
|
+
closeMenuOnSelect: true,
|
|
2573
|
+
className: "border-primary-main mr-2 bg-black text-white ",
|
|
2574
|
+
options: options,
|
|
2575
|
+
placeholder: options.find(option => option.value === config.strategy).placeHolder,
|
|
2576
|
+
value: config.strategy,
|
|
2577
|
+
onChange: ({
|
|
2578
|
+
value
|
|
2579
|
+
}) => {
|
|
2580
|
+
dispatch({
|
|
2581
|
+
type: 'setStrategy',
|
|
2582
|
+
payload: {
|
|
2583
|
+
strategy: value
|
|
2584
|
+
}
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
})), /*#__PURE__*/react.createElement("div", {
|
|
2588
|
+
className: "w-1/2"
|
|
2589
|
+
}, /*#__PURE__*/react.createElement(src/* LegacyButtonGroup */.xA, null, /*#__PURE__*/react.createElement(src/* LegacyButton */._H, {
|
|
2590
|
+
size: "initial",
|
|
2591
|
+
className: "px-2 py-2 text-base text-white",
|
|
2592
|
+
color: "primaryLight",
|
|
2593
|
+
variant: "outlined",
|
|
2594
|
+
onClick: () => runCommand('setStartSliceForROIThresholdTool')
|
|
2595
|
+
}, t('Start')), /*#__PURE__*/react.createElement(src/* LegacyButton */._H, {
|
|
2596
|
+
size: "initial",
|
|
2597
|
+
color: "primaryLight",
|
|
2598
|
+
variant: "outlined",
|
|
2599
|
+
className: "px-2 py-2 text-base text-white",
|
|
2600
|
+
onClick: () => runCommand('setEndSliceForROIThresholdTool')
|
|
2601
|
+
}, t('End'))))), config.strategy === ROI_STAT && /*#__PURE__*/react.createElement(src/* Input */.pd, {
|
|
2602
|
+
label: t('Percentage of Max SUV'),
|
|
2603
|
+
labelClassName: "text-[13px] font-inter text-white",
|
|
2604
|
+
className: "border-primary-main bg-black",
|
|
2605
|
+
type: "text",
|
|
2606
|
+
containerClassName: "mr-2",
|
|
2607
|
+
value: config.weight,
|
|
2608
|
+
onChange: e => {
|
|
2609
|
+
dispatch({
|
|
2610
|
+
type: 'setWeight',
|
|
2611
|
+
payload: {
|
|
2612
|
+
weight: e.target.value
|
|
2613
|
+
}
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
}), config.strategy !== ROI_STAT && /*#__PURE__*/react.createElement("div", {
|
|
2617
|
+
className: "mr-2 text-sm"
|
|
2618
|
+
}, /*#__PURE__*/react.createElement("table", null, /*#__PURE__*/react.createElement("tbody", null, /*#__PURE__*/react.createElement("tr", {
|
|
2619
|
+
className: "mt-2"
|
|
2620
|
+
}, /*#__PURE__*/react.createElement("td", {
|
|
2621
|
+
className: "pr-4",
|
|
2622
|
+
colSpan: "3"
|
|
2623
|
+
}, /*#__PURE__*/react.createElement(src/* Label */.JU, {
|
|
2624
|
+
className: "font-inter text-[13px] text-white",
|
|
2625
|
+
text: "Lower & Upper Ranges"
|
|
2626
|
+
}))), /*#__PURE__*/react.createElement("tr", {
|
|
2627
|
+
className: "mt-2"
|
|
2628
|
+
}, /*#__PURE__*/react.createElement("td", {
|
|
2629
|
+
className: "pr-4 pt-2 text-center"
|
|
2630
|
+
}, /*#__PURE__*/react.createElement(src/* Label */.JU, {
|
|
2631
|
+
className: "text-white",
|
|
2632
|
+
text: "CT"
|
|
2633
|
+
})), /*#__PURE__*/react.createElement("td", null, /*#__PURE__*/react.createElement("div", {
|
|
2634
|
+
className: "flex justify-between"
|
|
2635
|
+
}, /*#__PURE__*/react.createElement(src/* Input */.pd, {
|
|
2636
|
+
label: t(''),
|
|
2637
|
+
labelClassName: "text-white",
|
|
2638
|
+
className: "border-primary-main mt-2 bg-black",
|
|
2639
|
+
type: "text",
|
|
2640
|
+
containerClassName: "mr-2",
|
|
2641
|
+
value: config.ctLower,
|
|
2642
|
+
onChange: e => {
|
|
2643
|
+
dispatch({
|
|
2644
|
+
type: 'setThreshold',
|
|
2645
|
+
payload: {
|
|
2646
|
+
ctLower: e.target.value
|
|
2647
|
+
}
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
}), /*#__PURE__*/react.createElement(src/* Input */.pd, {
|
|
2651
|
+
label: t(''),
|
|
2652
|
+
labelClassName: "text-white",
|
|
2653
|
+
className: "border-primary-main mt-2 bg-black",
|
|
2654
|
+
type: "text",
|
|
2655
|
+
containerClassName: "mr-2",
|
|
2656
|
+
value: config.ctUpper,
|
|
2657
|
+
onChange: e => {
|
|
2658
|
+
dispatch({
|
|
2659
|
+
type: 'setThreshold',
|
|
2660
|
+
payload: {
|
|
2661
|
+
ctUpper: e.target.value
|
|
2662
|
+
}
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
})))), /*#__PURE__*/react.createElement("tr", null, /*#__PURE__*/react.createElement("td", {
|
|
2666
|
+
className: "pr-4 pt-2 text-center"
|
|
2667
|
+
}, /*#__PURE__*/react.createElement(src/* Label */.JU, {
|
|
2668
|
+
className: "text-white",
|
|
2669
|
+
text: "PT"
|
|
2670
|
+
})), /*#__PURE__*/react.createElement("td", null, /*#__PURE__*/react.createElement("div", {
|
|
2671
|
+
className: "flex justify-between"
|
|
2672
|
+
}, /*#__PURE__*/react.createElement(src/* Input */.pd, {
|
|
2673
|
+
label: t(''),
|
|
2674
|
+
labelClassName: "text-white",
|
|
2675
|
+
className: "border-primary-main mt-2 bg-black",
|
|
2676
|
+
type: "text",
|
|
2677
|
+
containerClassName: "mr-2",
|
|
2678
|
+
value: config.ptLower,
|
|
2679
|
+
onChange: e => {
|
|
2680
|
+
dispatch({
|
|
2681
|
+
type: 'setThreshold',
|
|
2682
|
+
payload: {
|
|
2683
|
+
ptLower: e.target.value
|
|
2684
|
+
}
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
}), /*#__PURE__*/react.createElement(src/* Input */.pd, {
|
|
2688
|
+
label: t(''),
|
|
2689
|
+
labelClassName: "text-white",
|
|
2690
|
+
className: "border-primary-main mt-2 bg-black",
|
|
2691
|
+
type: "text",
|
|
2692
|
+
containerClassName: "mr-2",
|
|
2693
|
+
value: config.ptUpper,
|
|
2694
|
+
onChange: e => {
|
|
2695
|
+
dispatch({
|
|
2696
|
+
type: 'setThreshold',
|
|
2697
|
+
payload: {
|
|
2698
|
+
ptUpper: e.target.value
|
|
2699
|
+
}
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
}))))))));
|
|
2703
|
+
}
|
|
2704
|
+
/* harmony default export */ const PanelROIThresholdSegmentation_ROIThresholdConfiguration = (ROIThresholdConfiguration);
|
|
2705
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/Panels/RectangleROIOptions.tsx
|
|
2706
|
+
|
|
2707
|
+
|
|
2708
|
+
|
|
2709
|
+
|
|
2710
|
+
const LOWER_CT_THRESHOLD_DEFAULT = -1024;
|
|
2711
|
+
const UPPER_CT_THRESHOLD_DEFAULT = 1024;
|
|
2712
|
+
const LOWER_PT_THRESHOLD_DEFAULT = 2.5;
|
|
2713
|
+
const UPPER_PT_THRESHOLD_DEFAULT = 100;
|
|
2714
|
+
const WEIGHT_DEFAULT = 0.41; // a default weight for suv max often used in the literature
|
|
2715
|
+
const DEFAULT_STRATEGY = ROI_STAT;
|
|
2716
|
+
function reducer(state, action) {
|
|
2717
|
+
const {
|
|
2718
|
+
payload
|
|
2719
|
+
} = action;
|
|
2720
|
+
const {
|
|
2721
|
+
strategy,
|
|
2722
|
+
ctLower,
|
|
2723
|
+
ctUpper,
|
|
2724
|
+
ptLower,
|
|
2725
|
+
ptUpper,
|
|
2726
|
+
weight
|
|
2727
|
+
} = payload;
|
|
2728
|
+
switch (action.type) {
|
|
2729
|
+
case 'setStrategy':
|
|
2730
|
+
return {
|
|
2731
|
+
...state,
|
|
2732
|
+
strategy
|
|
2733
|
+
};
|
|
2734
|
+
case 'setThreshold':
|
|
2735
|
+
return {
|
|
2736
|
+
...state,
|
|
2737
|
+
ctLower: ctLower ? ctLower : state.ctLower,
|
|
2738
|
+
ctUpper: ctUpper ? ctUpper : state.ctUpper,
|
|
2739
|
+
ptLower: ptLower ? ptLower : state.ptLower,
|
|
2740
|
+
ptUpper: ptUpper ? ptUpper : state.ptUpper
|
|
2741
|
+
};
|
|
2742
|
+
case 'setWeight':
|
|
2743
|
+
return {
|
|
2744
|
+
...state,
|
|
2745
|
+
weight
|
|
2746
|
+
};
|
|
2747
|
+
default:
|
|
2748
|
+
return state;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
function RectangleROIOptions({
|
|
2752
|
+
servicesManager,
|
|
2753
|
+
commandsManager
|
|
2754
|
+
}) {
|
|
2755
|
+
const {
|
|
2756
|
+
segmentationService
|
|
2757
|
+
} = servicesManager.services;
|
|
2758
|
+
const [selectedSegmentationId, setSelectedSegmentationId] = (0,react.useState)(null);
|
|
2759
|
+
const runCommand = (0,react.useCallback)((commandName, commandOptions = {}) => {
|
|
2760
|
+
return commandsManager.runCommand(commandName, commandOptions);
|
|
2761
|
+
}, [commandsManager]);
|
|
2762
|
+
const [config, dispatch] = (0,react.useReducer)(reducer, {
|
|
2763
|
+
strategy: DEFAULT_STRATEGY,
|
|
2764
|
+
ctLower: LOWER_CT_THRESHOLD_DEFAULT,
|
|
2765
|
+
ctUpper: UPPER_CT_THRESHOLD_DEFAULT,
|
|
2766
|
+
ptLower: LOWER_PT_THRESHOLD_DEFAULT,
|
|
2767
|
+
ptUpper: UPPER_PT_THRESHOLD_DEFAULT,
|
|
2768
|
+
weight: WEIGHT_DEFAULT
|
|
2769
|
+
});
|
|
2770
|
+
const handleROIThresholding = (0,react.useCallback)(() => {
|
|
2771
|
+
const segmentationId = selectedSegmentationId;
|
|
2772
|
+
const segmentation = segmentationService.getSegmentation(segmentationId);
|
|
2773
|
+
const activeSegmentIndex = esm.segmentation.segmentIndex.getActiveSegmentIndex(segmentationId);
|
|
2774
|
+
|
|
2775
|
+
// run the threshold based on the active segment index
|
|
2776
|
+
// Todo: later find a way to associate each rectangle with a segment (e.g., maybe with color?)
|
|
2777
|
+
const labelmap = runCommand('thresholdSegmentationByRectangleROITool', {
|
|
2778
|
+
segmentationId,
|
|
2779
|
+
config,
|
|
2780
|
+
segmentIndex: activeSegmentIndex
|
|
2781
|
+
});
|
|
2782
|
+
|
|
2783
|
+
// re-calculating the cached stats for the active segmentation
|
|
2784
|
+
const updatedPerSegmentCachedStats = {};
|
|
2785
|
+
segmentation.segments = segmentation.segments.map(segment => {
|
|
2786
|
+
if (!segment || !segment.segmentIndex) {
|
|
2787
|
+
return segment;
|
|
2788
|
+
}
|
|
2789
|
+
const segmentIndex = segment.segmentIndex;
|
|
2790
|
+
const lesionStats = runCommand('getLesionStats', {
|
|
2791
|
+
labelmap,
|
|
2792
|
+
segmentIndex
|
|
2793
|
+
});
|
|
2794
|
+
const suvPeak = runCommand('calculateSuvPeak', {
|
|
2795
|
+
labelmap,
|
|
2796
|
+
segmentIndex
|
|
2797
|
+
});
|
|
2798
|
+
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
|
2799
|
+
|
|
2800
|
+
// update segDetails with the suv peak for the active segmentation
|
|
2801
|
+
const cachedStats = {
|
|
2802
|
+
lesionStats,
|
|
2803
|
+
suvPeak,
|
|
2804
|
+
lesionGlyoclysisStats
|
|
2805
|
+
};
|
|
2806
|
+
segment.cachedStats = cachedStats;
|
|
2807
|
+
segment.displayText = [`SUV Peak: ${suvPeak.suvPeak.toFixed(2)}`, `Volume: ${lesionStats.volume.toFixed(2)} mm3`];
|
|
2808
|
+
updatedPerSegmentCachedStats[segmentIndex] = cachedStats;
|
|
2809
|
+
return segment;
|
|
2810
|
+
});
|
|
2811
|
+
const notYetUpdatedAtSource = true;
|
|
2812
|
+
const segmentations = segmentationService.getSegmentations();
|
|
2813
|
+
const tmtv = runCommand('calculateTMTV', {
|
|
2814
|
+
segmentations
|
|
2815
|
+
});
|
|
2816
|
+
segmentation.cachedStats = Object.assign(segmentation.cachedStats, updatedPerSegmentCachedStats, {
|
|
2817
|
+
tmtv: {
|
|
2818
|
+
value: tmtv.toFixed(3),
|
|
2819
|
+
config: {
|
|
2820
|
+
...config
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
});
|
|
2824
|
+
segmentationService.addOrUpdateSegmentation({
|
|
2825
|
+
...segmentation
|
|
2826
|
+
}, false,
|
|
2827
|
+
// don't suppress events
|
|
2828
|
+
notYetUpdatedAtSource);
|
|
2829
|
+
}, [selectedSegmentationId, config]);
|
|
2830
|
+
(0,react.useEffect)(() => {
|
|
2831
|
+
const segmentations = segmentationService.getSegmentations();
|
|
2832
|
+
if (!segmentations.length) {
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2835
|
+
const isActive = segmentations.find(seg => seg.isActive);
|
|
2836
|
+
setSelectedSegmentationId(isActive.id);
|
|
2837
|
+
}, []);
|
|
2838
|
+
|
|
2839
|
+
/**
|
|
2840
|
+
* Update UI based on segmentation changes (added, removed, updated)
|
|
2841
|
+
*/
|
|
2842
|
+
(0,react.useEffect)(() => {
|
|
2843
|
+
// ~~ Subscription
|
|
2844
|
+
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
|
2845
|
+
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
|
2846
|
+
const subscriptions = [];
|
|
2847
|
+
[added, updated].forEach(evt => {
|
|
2848
|
+
const {
|
|
2849
|
+
unsubscribe
|
|
2850
|
+
} = segmentationService.subscribe(evt, () => {
|
|
2851
|
+
const segmentations = segmentationService.getSegmentations();
|
|
2852
|
+
if (!segmentations.length) {
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
const isActive = segmentations.find(seg => seg.isActive);
|
|
2856
|
+
setSelectedSegmentationId(isActive.id);
|
|
2857
|
+
});
|
|
2858
|
+
subscriptions.push(unsubscribe);
|
|
2859
|
+
});
|
|
2860
|
+
return () => {
|
|
2861
|
+
subscriptions.forEach(unsub => {
|
|
2862
|
+
unsub();
|
|
2863
|
+
});
|
|
2864
|
+
};
|
|
2865
|
+
}, []);
|
|
2866
|
+
(0,react.useEffect)(() => {
|
|
2867
|
+
const {
|
|
2868
|
+
unsubscribe
|
|
2869
|
+
} = segmentationService.subscribe(segmentationService.EVENTS.SEGMENTATION_REMOVED, () => {
|
|
2870
|
+
const segmentations = segmentationService.getSegmentations();
|
|
2871
|
+
if (segmentations.length > 0) {
|
|
2872
|
+
setSelectedSegmentationId(segmentations[0].id);
|
|
2873
|
+
handleROIThresholding();
|
|
2874
|
+
} else {
|
|
2875
|
+
setSelectedSegmentationId(null);
|
|
2876
|
+
handleROIThresholding();
|
|
2877
|
+
}
|
|
2878
|
+
});
|
|
2879
|
+
return () => {
|
|
2880
|
+
unsubscribe();
|
|
2881
|
+
};
|
|
2882
|
+
}, []);
|
|
2883
|
+
return /*#__PURE__*/react.createElement("div", {
|
|
2884
|
+
className: "invisible-scrollbar mb-2 flex flex-col overflow-y-auto overflow-x-hidden"
|
|
2885
|
+
}, /*#__PURE__*/react.createElement(PanelROIThresholdSegmentation_ROIThresholdConfiguration, {
|
|
2886
|
+
config: config,
|
|
2887
|
+
dispatch: dispatch,
|
|
2888
|
+
runCommand: runCommand
|
|
2889
|
+
}), selectedSegmentationId !== null && /*#__PURE__*/react.createElement(src/* Button */.$n, {
|
|
2890
|
+
className: "mt-2 !h-[26px] !w-[75px]",
|
|
2891
|
+
onClick: handleROIThresholding
|
|
2892
|
+
}, "Run"));
|
|
2893
|
+
}
|
|
2894
|
+
/* harmony default export */ const Panels_RectangleROIOptions = (RectangleROIOptions);
|
|
2895
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/getToolbarModule.tsx
|
|
2896
|
+
|
|
2897
|
+
function getToolbarModule({
|
|
2898
|
+
commandsManager,
|
|
2899
|
+
servicesManager
|
|
2900
|
+
}) {
|
|
2901
|
+
return [{
|
|
2902
|
+
name: 'tmtv.RectangleROIThresholdOptions',
|
|
2903
|
+
defaultComponent: () => Panels_RectangleROIOptions({
|
|
2904
|
+
commandsManager,
|
|
2905
|
+
servicesManager
|
|
2906
|
+
})
|
|
2907
|
+
}];
|
|
2908
|
+
}
|
|
2909
|
+
;// CONCATENATED MODULE: ../../../extensions/tmtv/src/index.tsx
|
|
2910
|
+
|
|
2911
|
+
|
|
2912
|
+
|
|
2913
|
+
|
|
2914
|
+
|
|
2915
|
+
|
|
2916
|
+
|
|
2917
|
+
/**
|
|
2918
|
+
*
|
|
2919
|
+
*/
|
|
2920
|
+
const tmtvExtension = {
|
|
2921
|
+
/**
|
|
2922
|
+
* Only required property. Should be a unique value across all extensions.
|
|
2923
|
+
*/
|
|
2924
|
+
id: id,
|
|
2925
|
+
preRegistration({
|
|
2926
|
+
servicesManager,
|
|
2927
|
+
commandsManager,
|
|
2928
|
+
extensionManager,
|
|
2929
|
+
configuration = {}
|
|
2930
|
+
}) {
|
|
2931
|
+
init({
|
|
2932
|
+
servicesManager,
|
|
2933
|
+
commandsManager,
|
|
2934
|
+
extensionManager,
|
|
2935
|
+
configuration
|
|
2936
|
+
});
|
|
2937
|
+
},
|
|
2938
|
+
getToolbarModule: getToolbarModule,
|
|
2939
|
+
getPanelModule: src_getPanelModule,
|
|
2940
|
+
getHangingProtocolModule: src_getHangingProtocolModule,
|
|
2941
|
+
getCommandsModule({
|
|
2942
|
+
servicesManager,
|
|
2943
|
+
commandsManager,
|
|
2944
|
+
extensionManager
|
|
2945
|
+
}) {
|
|
2946
|
+
return src_commandsModule({
|
|
2947
|
+
servicesManager,
|
|
2948
|
+
commandsManager,
|
|
2949
|
+
extensionManager
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
/* harmony default export */ const tmtv_src = (tmtvExtension);
|
|
2954
|
+
|
|
2955
|
+
/***/ })
|
|
2956
|
+
|
|
2957
|
+
}]);
|