Compare commits
507
Commits
v3.7.0-beta.54
...
v3.8.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e9eb99b62 | ||
|
|
1ac149a2de | ||
|
|
339299113c | ||
|
|
1a57e54f74 | ||
|
|
aebcf962bb | ||
|
|
b7ebb0aa8f | ||
|
|
29c02121df | ||
|
|
a3d7f88b03 | ||
|
|
a3d8129089 | ||
|
|
b427af622e | ||
|
|
634abf353f | ||
|
|
471e6a1559 | ||
|
|
2f811470a8 | ||
|
|
6262a7f9d4 | ||
|
|
53c2740d75 | ||
|
|
85a82fac0e | ||
|
|
4c8a808a12 | ||
|
|
711c177cf2 | ||
|
|
f82ab4fda4 | ||
|
|
2d4e049d8a | ||
|
|
9f5165de47 | ||
|
|
1b290bdaa1 | ||
|
|
8feef28bff | ||
|
|
60d68e3410 | ||
|
|
3539e04019 | ||
|
|
c11c3b7e61 | ||
|
|
4029a8445b | ||
|
|
19bdcbb453 | ||
|
|
0386c29480 | ||
|
|
1dcdc9dabe | ||
|
|
feaebd7f61 | ||
|
|
6a8f4b9f38 | ||
|
|
956fff4e5b | ||
|
|
f1a736d193 | ||
|
|
4784638717 | ||
|
|
49e82f4756 | ||
|
|
c09311d3b7 | ||
|
|
b4ed80d99b | ||
|
|
c87081d924 | ||
|
|
f09dfaf911 | ||
|
|
11a4ba3c99 | ||
|
|
56ccb4f7e0 | ||
|
|
6ebd2cc7cb | ||
|
|
25c01fc66c | ||
|
|
2ea882e299 | ||
|
|
52a0c59294 | ||
|
|
3ec7e51216 | ||
|
|
1b366c92ee | ||
|
|
13d67d90fc | ||
|
|
f0ed3fd7b9 | ||
|
|
65d419dd52 | ||
|
|
4349fc1085 | ||
|
|
8192e348ec | ||
|
|
8c5ab942b9 | ||
|
|
ab19d9c0a2 | ||
|
|
d250d04580 | ||
|
|
28bc8914a8 | ||
|
|
3952d465dc | ||
|
|
1a013909a1 | ||
|
|
8f42baf36e | ||
|
|
2633ef7323 | ||
|
|
da7c1a5d8c | ||
|
|
058da93863 | ||
|
|
bc819409b3 | ||
|
|
170bb96983 | ||
|
|
c4e6556259 | ||
|
|
ab61cc9b1f | ||
|
|
3754c224b4 | ||
|
|
d3cdfd8506 | ||
|
|
aa572daa2c | ||
|
|
f99a0bfb31 | ||
|
|
a6c6fff43a | ||
|
|
e687384933 | ||
|
|
e80fc6f477 | ||
|
|
41a8d105f1 | ||
|
|
60b2f69334 | ||
|
|
df49a653be | ||
|
|
77693ddef4 | ||
|
|
6a757f69d8 | ||
|
|
2b83393f91 | ||
|
|
ac0e1c0a2c | ||
|
|
b0b65c582d | ||
|
|
4f99244d86 | ||
|
|
be9bb06565 | ||
|
|
c7e53bdbf8 | ||
|
|
7c3d67c2a8 | ||
|
|
e9c30a108e | ||
|
|
603d5cc53a | ||
|
|
5cc693d26c | ||
|
|
7fac49b449 | ||
|
|
7014e6ca4a | ||
|
|
87b68bba4a | ||
|
|
d57e8bc157 | ||
|
|
dc0b183dd4 | ||
|
|
06920ab927 | ||
|
|
dc37802ec1 | ||
|
|
894a227978 | ||
|
|
1c9fee1bec | ||
|
|
5645ac1b27 | ||
|
|
6e5892d202 | ||
|
|
9d8997ad84 | ||
|
|
cea27d438d | ||
|
|
78fd8cce86 | ||
|
|
f7c9a5cb64 | ||
|
|
56b1eae635 | ||
|
|
807afb8ce5 | ||
|
|
5f494598bb | ||
|
|
fa38e6a07a | ||
|
|
d8f66856cc | ||
|
|
9c39689451 | ||
|
|
9f3235ff09 | ||
|
|
e8e209291c | ||
|
|
bc71861619 | ||
|
|
b7f90e3951 | ||
|
|
07301fc020 | ||
|
|
1203477dd4 | ||
|
|
158a181670 | ||
|
|
aac3546bed | ||
|
|
1256a4d565 | ||
|
|
617043fe0d | ||
|
|
6f610b09e1 | ||
|
|
a2e320f9f8 | ||
|
|
566b25a544 | ||
|
|
79d5c36bda | ||
|
|
f47a94317d | ||
|
|
45b68e841d | ||
|
|
d70aae3233 | ||
|
|
345b137bec | ||
|
|
9bcd1ae6f5 | ||
|
|
6b79dff5c1 | ||
|
|
88859f75d5 | ||
|
|
feed2304c1 | ||
|
|
08815909c7 | ||
|
|
ed2234a67c | ||
|
|
f7fe91c5f6 | ||
|
|
21ec6860f2 | ||
|
|
ac474b7f8f | ||
|
|
e485d68fd4 | ||
|
|
5089229ff9 | ||
|
|
df09176d63 | ||
|
|
bb603bd1f8 | ||
|
|
82a4a7d377 | ||
|
|
c27fa9c802 | ||
|
|
4cdfdae814 | ||
|
|
596340803a | ||
|
|
ff4e17f5c2 | ||
|
|
21e8a2bd0b | ||
|
|
2a442e30eb | ||
|
|
8e8a66bd29 | ||
|
|
b3a0faf5f5 | ||
|
|
a2a0090eeb | ||
|
|
ea552027c5 | ||
|
|
0eac386a31 | ||
|
|
40b590d75a | ||
|
|
01df1763ce | ||
|
|
5bba98ed84 | ||
|
|
16785936d9 | ||
|
|
3deb73d642 | ||
|
|
ef46b5a704 | ||
|
|
ed2bf23cd2 | ||
|
|
2ff2f3f34d | ||
|
|
a47aeb8bd7 | ||
|
|
94f40192f5 | ||
|
|
1c43aa791a | ||
|
|
e4a116b074 | ||
|
|
2460d28afc | ||
|
|
4a958b8989 | ||
|
|
31b837fa90 | ||
|
|
9bb00d71fa | ||
|
|
40bbe33a9d | ||
|
|
911d672835 | ||
|
|
9c553fded4 | ||
|
|
144dd39393 | ||
|
|
df2f50748e | ||
|
|
83a1523408 | ||
|
|
5a62a983e5 | ||
|
|
d00a86b022 | ||
|
|
710a2dca72 | ||
|
|
9100b1f02e | ||
|
|
0049f4c030 | ||
|
|
b5cdf88a57 | ||
|
|
c793df727b | ||
|
|
1456a493d6 | ||
|
|
7c551f5043 | ||
|
|
0b2c2a235f | ||
|
|
b1efe40aa1 | ||
|
|
a8a0bdb2df | ||
|
|
6878c1a264 | ||
|
|
d181eb4fdc | ||
|
|
fd809e4a0b | ||
|
|
4c29670d76 | ||
|
|
f58725ce40 | ||
|
|
5a1cb842f0 | ||
|
|
c5bd79552a | ||
|
|
61bf22c6f8 | ||
|
|
3d98548072 | ||
|
|
bf9770a55c | ||
|
|
2049c0936c | ||
|
|
7d5607073d | ||
|
|
1b6910bf76 | ||
|
|
98650302c7 | ||
|
|
bf846c94c3 | ||
|
|
b5dc9047ba | ||
|
|
636d8b0458 | ||
|
|
e8858f3eb5 | ||
|
|
eb7c950d7e | ||
|
|
c6214d66b9 | ||
|
|
0eb502aaf0 | ||
|
|
44a5a24d4f | ||
|
|
b0bf5dc780 | ||
|
|
5915fada83 | ||
|
|
2fa335c6d9 | ||
|
|
de4ad88a09 | ||
|
|
6ca13c0a4c | ||
|
|
0177b625ba | ||
|
|
c4064acfbc | ||
|
|
7a05d169ef | ||
|
|
13d0c883f8 | ||
|
|
c45f60dce2 | ||
|
|
7bb2713c5c | ||
|
|
97cb1f94e1 | ||
|
|
f99645f3a1 | ||
|
|
ae16c7f972 | ||
|
|
9c6e1cd305 | ||
|
|
7f0ca857fa | ||
|
|
920256d3a4 | ||
|
|
e1f55e65f2 | ||
|
|
d1c07d90f0 | ||
|
|
b1a04ecac3 | ||
|
|
f707b4ebc9 | ||
|
|
805c53270f | ||
|
|
d14a8f0199 | ||
|
|
5a5c4f58af | ||
|
|
6e5d8b691e | ||
|
|
59576d695d | ||
|
|
78d4bea04d | ||
|
|
806c2f4b36 | ||
|
|
6651008fbb | ||
|
|
74c98b6e9f | ||
|
|
ecee25b1d5 | ||
|
|
f1a67647ae | ||
|
|
4f9a00f872 | ||
|
|
7673a887e4 | ||
|
|
924affa7b5 | ||
|
|
abac6f583e | ||
|
|
36ba20d6e8 | ||
|
|
9d1884d7d8 | ||
|
|
622f18bd3e | ||
|
|
8b6256e76f | ||
|
|
c4ff2c2f09 | ||
|
|
1be5613946 | ||
|
|
6edb1c1e4c | ||
|
|
404b0a5d53 | ||
|
|
786898fd63 | ||
|
|
dfa851600c | ||
|
|
00e751933a | ||
|
|
c4e22c251f | ||
|
|
3cf4b6dea5 | ||
|
|
8c8924af37 | ||
|
|
04121e3713 | ||
|
|
d1eef6dbdb | ||
|
|
d83beb7c62 | ||
|
|
7867b29742 | ||
|
|
4a6f1bd407 | ||
|
|
b694228dd5 | ||
|
|
013068bcb0 | ||
|
|
3fbc23b04c | ||
|
|
323401418e | ||
|
|
fc438e90ae | ||
|
|
9b7f38467b | ||
|
|
fd1251f751 | ||
|
|
14793493a4 | ||
|
|
73d8848707 | ||
|
|
43b1c17209 | ||
|
|
5b513fd30e | ||
|
|
26c9567c38 | ||
|
|
8bbcd0e692 | ||
|
|
8c2f495405 | ||
|
|
e1e1e60b7d | ||
|
|
8af1046803 | ||
|
|
4bbba871f5 | ||
|
|
73e804c311 | ||
|
|
442f99d5eb | ||
|
|
9575a6f54c | ||
|
|
dc345d1875 | ||
|
|
bf252bcec2 | ||
|
|
1a68c06c3b | ||
|
|
a2361fa615 | ||
|
|
ee67bcc0f5 | ||
|
|
f9f16bc69f | ||
|
|
a29f5bc9d3 | ||
|
|
b23eeff937 | ||
|
|
09ff293f5f | ||
|
|
71a30dc9f2 | ||
|
|
330e11c7ff | ||
|
|
c73a403cdc | ||
|
|
5b3b6eed16 | ||
|
|
2a15ef0e44 | ||
|
|
db395852b6 | ||
|
|
423ba7c269 | ||
|
|
cafba997bf | ||
|
|
dd6d9768bb | ||
|
|
2145b42920 | ||
|
|
a5ad1cc02a | ||
|
|
d98439fe7f | ||
|
|
3bbbcea0d7 | ||
|
|
6e993511b7 | ||
|
|
eec9c33b76 | ||
|
|
c31c7f8e79 | ||
|
|
a7125f9385 | ||
|
|
49514aedfe | ||
|
|
43c882ac87 | ||
|
|
912b3c7c53 | ||
|
|
d9258eca70 | ||
|
|
fa599d05fb | ||
|
|
5f8325e338 | ||
|
|
93d798db99 | ||
|
|
33f1259408 | ||
|
|
c54658a320 | ||
|
|
55002fbd71 | ||
|
|
df32a05a4d | ||
|
|
e205bf5c4e | ||
|
|
5ddf8a1602 | ||
|
|
83fccad46b | ||
|
|
3f9ee1451b | ||
|
|
70ea13ec1f | ||
|
|
157b88c909 | ||
|
|
75c3f94177 | ||
|
|
6874d5f0b9 | ||
|
|
a3f2a1a7b0 | ||
|
|
8067f0c62c | ||
|
|
a894b6ffc5 | ||
|
|
c3a5847dcd | ||
|
|
132938bbe0 | ||
|
|
5ba63bd010 | ||
|
|
7c57f67844 | ||
|
|
dc929d3df5 | ||
|
|
5a3af83168 | ||
|
|
a9a6ad50ea | ||
|
|
f8455056cd | ||
|
|
f9c3d510d3 | ||
|
|
52f34c64d0 | ||
|
|
7d87766ac4 | ||
|
|
820bb83b70 | ||
|
|
0a42d573bb | ||
|
|
f9f33f74c1 | ||
|
|
6a5fe59136 | ||
|
|
4911e4796c | ||
|
|
349c503f04 | ||
|
|
a6a6aed361 | ||
|
|
40673f64b3 | ||
|
|
eab42c1032 | ||
|
|
a790be9f5a | ||
|
|
8bc12a37d0 | ||
|
|
29aeb070e0 | ||
|
|
cdb66dcdef | ||
|
|
1fd98d9220 | ||
|
|
748acaf9c1 | ||
|
|
3f27c3820f | ||
|
|
c1d5ee7e3f | ||
|
|
646bef1d93 | ||
|
|
83c001f8e4 | ||
|
|
ebe8f71da2 | ||
|
|
17f528bcde | ||
|
|
ed82511acc | ||
|
|
745050a28e | ||
|
|
953c564d98 | ||
|
|
c418185f0b | ||
|
|
29748d46a1 | ||
|
|
a755cce7f1 | ||
|
|
42b7d9983c | ||
|
|
28cec04ff4 | ||
|
|
01d843f267 | ||
|
|
15eca78b68 | ||
|
|
108383b9ef | ||
|
|
1b207f14d2 | ||
|
|
38cd3d8afc | ||
|
|
5e7fe91617 | ||
|
|
64f68f58c2 | ||
|
|
49bc346661 | ||
|
|
5cc1dc9b9c | ||
|
|
2a086a699a | ||
|
|
58f214bca5 | ||
|
|
4dc2acdefa | ||
|
|
4c88a4a2fd | ||
|
|
06facd8ea2 | ||
|
|
eb22328fc0 | ||
|
|
b33ad0c7e9 | ||
|
|
3415cf909f | ||
|
|
e36a604331 | ||
|
|
8a335bd03d | ||
|
|
b3429729f1 | ||
|
|
e2e806f188 | ||
|
|
1129c155d2 | ||
|
|
c9c57b04d3 | ||
|
|
d09938b72a | ||
|
|
a67d72de85 | ||
|
|
3dabee2342 | ||
|
|
66abdace30 | ||
|
|
dc73b18748 | ||
|
|
52da92fed4 | ||
|
|
cfbb3cfded | ||
|
|
86f54d0d07 | ||
|
|
485cf037a1 | ||
|
|
3d38122b84 | ||
|
|
8feb5ee3f9 | ||
|
|
1f906963e7 | ||
|
|
9a4aa1a5aa | ||
|
|
fcd323fa8c | ||
|
|
b6e33224c8 | ||
|
|
b8e519a4a9 | ||
|
|
0bf9949f45 | ||
|
|
3431b103e2 | ||
|
|
00918d86d3 | ||
|
|
c86b129f58 | ||
|
|
10ca35d5f4 | ||
|
|
a308f7b4b6 | ||
|
|
04ca10d877 | ||
|
|
85c899b399 | ||
|
|
48bbd6281a | ||
|
|
896866e9d0 | ||
|
|
a02a5ded9a | ||
|
|
2bd3b26a6a | ||
|
|
034a6d7696 | ||
|
|
2fc28978da | ||
|
|
221dedde5d | ||
|
|
807bd663b3 | ||
|
|
0318504597 | ||
|
|
d609ae1edb | ||
|
|
2132f0777e | ||
|
|
10ecd4997b | ||
|
|
2d7721cb58 | ||
|
|
c49b833274 | ||
|
|
3279df543f | ||
|
|
2737903386 | ||
|
|
9ccdaef812 | ||
|
|
8c6764a936 | ||
|
|
38af3112ec | ||
|
|
bd7f9592ea | ||
|
|
fd74e46280 | ||
|
|
74e62a1763 | ||
|
|
7eda0c5f46 | ||
|
|
15e1af2526 | ||
|
|
11ca5b6eae | ||
|
|
64079e0720 | ||
|
|
7f683a336b | ||
|
|
0d10f46b88 | ||
|
|
ca19baeafd | ||
|
|
3e73ddf4c1 | ||
|
|
c0fb2b4892 | ||
|
|
2a7787132a | ||
|
|
3cc35e7ed5 | ||
|
|
de0a742733 | ||
|
|
c158279a53 | ||
|
|
12acc396d6 | ||
|
|
a6e668f8d4 | ||
|
|
6dd74b6371 | ||
|
|
908fb8e14f | ||
|
|
94f7cfb08e | ||
|
|
6c8364835b | ||
|
|
a39929d4d5 | ||
|
|
dd6a8812d1 | ||
|
|
ce72929367 | ||
|
|
38ab1ae5a6 | ||
|
|
69115da06d | ||
|
|
ef58893c27 | ||
|
|
7987f0effa | ||
|
|
3ce72254b3 | ||
|
|
6d47dce4dd | ||
|
|
f7cddd0842 | ||
|
|
9045ddaedc | ||
|
|
01500a2d4c | ||
|
|
b170e7f5ae | ||
|
|
2d7da9c0bb | ||
|
|
4c6ff873e8 | ||
|
|
5dafac7c92 | ||
|
|
7ecaf0da5d | ||
|
|
adedc8c382 | ||
|
|
5856ae2464 | ||
|
|
84b7de5d96 | ||
|
|
35fc30c535 | ||
|
|
eafc716d00 | ||
|
|
88d695cf4f | ||
|
|
44f101d3f2 | ||
|
|
a9d6869198 | ||
|
|
77c102f993 | ||
|
|
75f61f85fe | ||
|
|
9fbdf37f44 | ||
|
|
7ecd2f1628 | ||
|
|
a336992971 | ||
|
|
a2ef2b0fcb | ||
|
|
eb57c2f8bd | ||
|
|
65b49aeb1b | ||
|
|
fe0ee050ed | ||
|
|
ee6f399787 | ||
|
|
b117a54aa1 | ||
|
|
ede6300099 | ||
|
|
a8717c8625 | ||
|
|
0f9b36906f | ||
|
|
72ec0bffb5 | ||
|
|
82c6c3c353 | ||
|
|
1d38fe30a4 | ||
|
|
666964b6d7 | ||
|
|
fa4ee5c559 | ||
|
|
cd58560e9c | ||
|
|
16afa740b6 | ||
|
|
6715ad9bd6 |
No files matched your search
+219
-172
@@ -13,22 +13,24 @@ version: 2.1
|
||||
##
|
||||
orbs:
|
||||
codecov: codecov/codecov@1.0.5
|
||||
cypress: cypress-io/cypress@1.26.0
|
||||
cypress: cypress-io/cypress@3.3.1
|
||||
|
||||
executors:
|
||||
# Custom executor to override Cypress config
|
||||
deploy-to-prod-executor:
|
||||
cypress-custom:
|
||||
description: |
|
||||
Single Docker container used to run Cypress Tests
|
||||
docker:
|
||||
- image: cimg/node:16.14
|
||||
environment:
|
||||
CYPRESS_BASE_URL: https://ohif-staging.netlify.com/
|
||||
chrome-and-pacs:
|
||||
docker:
|
||||
# Primary container image where all steps run.
|
||||
- image: 'cypress/browsers:node16.14.2-slim-chrome103-ff102'
|
||||
- image: cimg/node:<< parameters.node-version >>-browsers
|
||||
parameters:
|
||||
node-version:
|
||||
default: '18.16.1'
|
||||
description: |
|
||||
The version of Node to run your tests with.
|
||||
type: string
|
||||
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: cimg/node:16.14-browsers
|
||||
- image: cimg/node:18.18
|
||||
environment:
|
||||
TERM: xterm # Enable colors in term
|
||||
QUICK_BUILD: true
|
||||
@@ -43,6 +45,7 @@ jobs:
|
||||
steps:
|
||||
# Update yarn
|
||||
- run: yarn -v
|
||||
- run: node --version
|
||||
# Checkout code and ALL Git Tags
|
||||
- checkout
|
||||
- restore_cache:
|
||||
@@ -90,43 +93,6 @@ jobs:
|
||||
file: '/home/circleci/repo/platform/core/coverage/reports'
|
||||
flags: 'core'
|
||||
|
||||
###
|
||||
# Workflow: PR_OPTIONAL_DOCKER_PUBLISH
|
||||
###
|
||||
DOCKER_PR_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
# Enable yarn workspaces
|
||||
- run: yarn config set workspaces-experimental true
|
||||
|
||||
# Checkout code and ALL Git Tags
|
||||
- checkout
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
# when lock file changes, use increasingly general patterns to restore cache
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
|
||||
- run:
|
||||
name: Build and push Docker image
|
||||
command: |
|
||||
# Remove npm config
|
||||
rm -f ./.npmrc
|
||||
# Set our version number using vars
|
||||
echo $CIRCLE_BUILD_NUM
|
||||
# Build our image, auth, and push
|
||||
docker build --tag ohif/app:PR_BUILD-$CIRCLE_BUILD_NUM .
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
docker push ohif/app:PR_BUILD-$CIRCLE_BUILD_NUM
|
||||
|
||||
###
|
||||
# Workflow: DEPLOY
|
||||
###
|
||||
@@ -177,62 +143,52 @@ jobs:
|
||||
- commit.txt
|
||||
- version.json
|
||||
|
||||
# DEPLOY_TO_DEV:
|
||||
# docker:
|
||||
# - image: circleci/node:16.14.0
|
||||
# environment:
|
||||
# TERM: xterm
|
||||
# NETLIFY_SITE_ID: 32708787-c9b0-4634-b50f-7ca41952da77
|
||||
# working_directory: ~/repo
|
||||
# steps:
|
||||
# - attach_workspace:
|
||||
# at: ~/repo
|
||||
# - run: cd .netlify && npm install
|
||||
# - run: cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects
|
||||
# - run: cd .netlify && npm run deploy
|
||||
|
||||
# DEPLOY_TO_STAGING:
|
||||
# docker:
|
||||
# - image: circleci/node:16.14.0
|
||||
# environment:
|
||||
# TERM: xterm
|
||||
# NETLIFY_SITE_ID: c7502ae3-b150-493c-8422-05701e44a969
|
||||
# working_directory: ~/repo
|
||||
# steps:
|
||||
# - attach_workspace:
|
||||
# at: ~/repo
|
||||
# - run: cd .netlify && npm install
|
||||
# - run: cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects
|
||||
# - run: cd .netlify && npm run deploy
|
||||
|
||||
# DEPLOY_TO_PRODUCTION:
|
||||
# docker:
|
||||
# - image: circleci/node:16.14.0
|
||||
# environment:
|
||||
# TERM: xterm
|
||||
# NETLIFY_SITE_ID: 79c4a5da-5c95-4dc9-84f7-45fd9dfe21b0
|
||||
# working_directory: ~/repo
|
||||
# steps:
|
||||
# - attach_workspace:
|
||||
# at: ~/repo
|
||||
# - run: cd .netlify && npm install
|
||||
# - run: cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects
|
||||
# - run: cd .netlify && npm run deploy
|
||||
|
||||
# DEPLOY_TO_RELEASE_DEV:
|
||||
# docker:
|
||||
# - image: circleci/node:16.14.0
|
||||
# environment:
|
||||
# TERM: xterm
|
||||
# NETLIFY_SITE_ID: 3270878-22
|
||||
# working_directory: ~/repo
|
||||
# steps:
|
||||
# - attach_workspace:
|
||||
# at: ~/repo
|
||||
# - run: cd .netlify && npm install
|
||||
# - run:
|
||||
# cp .netlify/deploy-workflow/_redirects platform/app/dist/_redirects
|
||||
# - run: cd .netlify && npm run deploy
|
||||
# just to make sure later on we can publish them
|
||||
BUILD_PACKAGES_QUICK:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- run: yarn -v
|
||||
# Checkout code and ALL Git Tags
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
# Use increasingly general patterns to restore cache
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
name: Save Yarn Package Cache
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: Avoid hosts unknown for github
|
||||
command: |
|
||||
rm -rf ~/.ssh
|
||||
mkdir ~/.ssh/
|
||||
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
|
||||
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
|
||||
git config --global user.name "ohif-bot"
|
||||
- run:
|
||||
name: Authenticate with NPM registry
|
||||
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
- run:
|
||||
name: Increase the event emitter limit
|
||||
command: |
|
||||
node ./increaseEventEmitterLimit.mjs
|
||||
- run:
|
||||
name: build half of the packages (to avoid out of memory in circleci)
|
||||
command: |
|
||||
yarn run build:package-all
|
||||
- run:
|
||||
name: build the other half of the packages
|
||||
command: |
|
||||
yarn run build:package-all-1
|
||||
|
||||
###
|
||||
# Workflow: RELEASE
|
||||
@@ -269,8 +225,7 @@ jobs:
|
||||
git config --global user.name "ohif-bot"
|
||||
- run:
|
||||
name: Authenticate with NPM registry
|
||||
command:
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
- run:
|
||||
name: Increase the event emitter limit
|
||||
command: |
|
||||
@@ -283,14 +238,21 @@ jobs:
|
||||
name: build the other half of the packages
|
||||
command: |
|
||||
yarn run build:package-all-1
|
||||
- run:
|
||||
name: increase min time out
|
||||
command: |
|
||||
npm config set fetch-retry-mintimeout 20000
|
||||
- run:
|
||||
name: increase max time out
|
||||
command: |
|
||||
npm config set fetch-retry-maxtimeout 120000
|
||||
- run:
|
||||
name: publish package versions
|
||||
command: |
|
||||
node ./publish-version.mjs
|
||||
- run:
|
||||
name: Again set the NPM registry (was deleted in the version script)
|
||||
command:
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
- run:
|
||||
name: publish package dist
|
||||
command: |
|
||||
@@ -337,8 +299,7 @@ jobs:
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name:
|
||||
Build and push Docker image from the master branch (beta releases)
|
||||
name: Build and push Docker image from the master branch (beta releases)
|
||||
command: |
|
||||
echo $(ls -l)
|
||||
|
||||
@@ -365,71 +326,152 @@ jobs:
|
||||
docker push ohif/app:$IMAGE_VERSION_FULL
|
||||
fi
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
# This is copied from the Cypress orb since the default for cypress/run is node 16 and
|
||||
# we migrated to 18
|
||||
CYPRESS_CUSTOM_RUN:
|
||||
description: |
|
||||
A single, complete job to run Cypress end-to-end tests in your application.
|
||||
executor: cypress-custom
|
||||
parallelism: << parameters.parallelism >>
|
||||
parameters:
|
||||
cypress-cache-key:
|
||||
default: cypress-cache-{{ arch }}-{{ checksum "package.json" }}
|
||||
description: Cache key used to cache the Cypress binary.
|
||||
type: string
|
||||
cypress-cache-path:
|
||||
default: ~/.cache/Cypress
|
||||
description: |
|
||||
By default, this will cache the '~/.cache/Cypress' directory so that the Cypress binary is cached. You can override this by providing your own cache path.
|
||||
type: string
|
||||
cypress-command:
|
||||
default: npx cypress run
|
||||
description: Command used to run your Cypress tests
|
||||
type: string
|
||||
include-branch-in-node-cache-key:
|
||||
default: false
|
||||
description: |
|
||||
If true, this cache will only apply to runs within the same branch. (Adds -{{ .Branch }}- to the node cache key)
|
||||
type: boolean
|
||||
install-browsers:
|
||||
default: false
|
||||
description: |
|
||||
Cypress runs by default in the Electron browser. Use this flag to install additional browsers to run your tests in.
|
||||
This is only needed if you are passing the `--browser` flag in your `cypress-command`.
|
||||
This parameter leverages the `circleci/browser-tools` orb and includes Chrome and FireFox.
|
||||
If you need additional browser support you can set this to false and use an executor with a docker image
|
||||
that includes the browsers of your choosing. See https://hub.docker.com/r/cypress/browsers/tags
|
||||
type: boolean
|
||||
install-command:
|
||||
default: ''
|
||||
description: Overrides the default NPM command (npm ci)
|
||||
type: string
|
||||
node-cache-version:
|
||||
default: v1
|
||||
description:
|
||||
Change the default node cache version if you need to clear the cache for any reason.
|
||||
type: string
|
||||
package-manager:
|
||||
default: npm
|
||||
description: Select the default node package manager to use. NPM v5+ Required.
|
||||
enum:
|
||||
- npm
|
||||
- yarn
|
||||
- yarn-berry
|
||||
type: enum
|
||||
parallelism:
|
||||
default: 1
|
||||
description: |
|
||||
Number of Circle machines to use for load balancing, min 1
|
||||
(requires `parallel` and `record` flags in your `cypress-command`)
|
||||
type: integer
|
||||
post-install:
|
||||
default: ''
|
||||
description: |
|
||||
Additional commands to run after running install but before verifying Cypress and saving cache.
|
||||
type: string
|
||||
start-command:
|
||||
default: ''
|
||||
description: Command used to start your local dev server for Cypress to tests against
|
||||
type: string
|
||||
working-directory:
|
||||
default: ''
|
||||
description: Directory containing package.json
|
||||
type: string
|
||||
steps:
|
||||
- cypress/install:
|
||||
cypress-cache-key: << parameters.cypress-cache-key >>
|
||||
cypress-cache-path: << parameters.cypress-cache-path >>
|
||||
include-branch-in-node-cache-key: << parameters.include-branch-in-node-cache-key >>
|
||||
install-browsers: << parameters.install-browsers >>
|
||||
install-command: << parameters.install-command >>
|
||||
node-cache-version: << parameters.node-cache-version >>
|
||||
package-manager: << parameters.package-manager >>
|
||||
post-install: << parameters.post-install >>
|
||||
working-directory: << parameters.working-directory >>
|
||||
- cypress/run-tests:
|
||||
cypress-command: << parameters.cypress-command >>
|
||||
start-command: << parameters.start-command >>
|
||||
working-directory: << parameters.working-directory >>
|
||||
|
||||
workflows:
|
||||
PR_CHECKS:
|
||||
jobs:
|
||||
- UNIT_TESTS
|
||||
|
||||
# E2E: PWA
|
||||
- cypress/run:
|
||||
name: 'E2E: PWA'
|
||||
executor: chrome-and-pacs
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: |
|
||||
# Clear yarn cache; use yarn from image (update image to update yarn)
|
||||
rm -rf ~/.yarn
|
||||
yarn -v
|
||||
yarn: true
|
||||
record: true
|
||||
store_artifacts: true
|
||||
working_directory: platform/app
|
||||
build: yarn test:data
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/**/*'
|
||||
wait-on: 'http://localhost:3000'
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/app/cypress/screenshots
|
||||
- store_artifacts:
|
||||
path: platform/app/cypress/videos
|
||||
- store_test_results:
|
||||
path: platform/app/cypress/results
|
||||
- BUILD_PACKAGES_QUICK:
|
||||
filters:
|
||||
branches:
|
||||
ignore: master
|
||||
- UNIT_TESTS:
|
||||
requires:
|
||||
- UNIT_TESTS
|
||||
|
||||
PR_OPTIONAL_VISUAL_TESTS:
|
||||
jobs:
|
||||
- AWAIT_APPROVAL:
|
||||
type: approval
|
||||
# Update hub.docker.org
|
||||
- cypress/run:
|
||||
name: 'Generate Percy Snapshots'
|
||||
executor: cypress/browsers-chrome76
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on'
|
||||
yarn: true
|
||||
store_artifacts: false
|
||||
working_directory: platform/app
|
||||
build:
|
||||
yarn test:data && npx cross-env QUICK_BUILD=true
|
||||
APP_CONFIG=config/dicomweb-server.js yarn run build
|
||||
# start server --> verify running --> percy + chrome + cypress
|
||||
command: yarn run test:e2e:dist
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/app/cypress/screenshots
|
||||
- store_artifacts:
|
||||
path: platform/app/cypress/videos
|
||||
- BUILD_PACKAGES_QUICK
|
||||
- CYPRESS_CUSTOM_RUN:
|
||||
name: 'Cypress Tests'
|
||||
context: cypress
|
||||
matrix:
|
||||
parameters:
|
||||
start-command:
|
||||
- yarn run test:data && yarn run test:e2e:serve
|
||||
install-browsers:
|
||||
- true
|
||||
cypress-command:
|
||||
- 'npx wait-on@latest http://localhost:3000 && cd platform/app && npx cypress run
|
||||
--record --browser chrome --parallel'
|
||||
package-manager:
|
||||
- 'yarn'
|
||||
cypress-cache-key:
|
||||
- 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
cypress-cache-path:
|
||||
- '~/.cache/Cypress'
|
||||
requires:
|
||||
- AWAIT_APPROVAL
|
||||
- BUILD_PACKAGES_QUICK
|
||||
|
||||
# PR_OPTIONAL_VISUAL_TESTS:
|
||||
# jobs:
|
||||
# - AWAIT_APPROVAL:
|
||||
# type: approval
|
||||
# # Update hub.docker.org
|
||||
# - cypress/run:
|
||||
# name: 'Generate Percy Snapshots'
|
||||
# executor: cypress/browsers-chrome76
|
||||
# browser: chrome
|
||||
# pre-steps:
|
||||
# - run: 'rm -rf ~/.yarn && yarn -v && yarn global add wait-on'
|
||||
# yarn: true
|
||||
# store_artifacts: false
|
||||
# working_directory: platform/app
|
||||
# build:
|
||||
# yarn test:data && npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
# yarn run build
|
||||
# # start server --> verify running --> percy + chrome + cypress
|
||||
# command: yarn run test:e2e:dist
|
||||
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
# no-workspace: true # Don't persist workspace
|
||||
# post-steps:
|
||||
# - store_artifacts:
|
||||
# path: platform/app/cypress/screenshots
|
||||
# - store_artifacts:
|
||||
# path: platform/app/cypress/videos
|
||||
# requires:
|
||||
# - AWAIT_APPROVAL
|
||||
|
||||
# Our master branch deploys to viewer-dev.ohif.org, the viewer.ohif.org is
|
||||
# deployed from the release branch which is more stable and less frequently updated.
|
||||
@@ -453,13 +495,18 @@ workflows:
|
||||
- BUILD:
|
||||
filters:
|
||||
branches:
|
||||
only: release
|
||||
- NPM_PUBLISH:
|
||||
only: /^release\/.*/
|
||||
- HOLD_FOR_APPROVAL:
|
||||
type: approval
|
||||
requires:
|
||||
- BUILD
|
||||
- NPM_PUBLISH:
|
||||
requires:
|
||||
- HOLD_FOR_APPROVAL
|
||||
- DOCKER_RELEASE_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
|
||||
###
|
||||
# Unit and E2E tests have already run for PR_CHECKS
|
||||
# Re-running should not gain us any confidence here
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# ABOUT:
|
||||
# https://docs.codecov.io/docs/codecov-yaml
|
||||
#
|
||||
#
|
||||
# COMMIT STATUS:
|
||||
# https://docs.codecov.io/docs/commit-status
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
threshold: 0.5%
|
||||
core:
|
||||
flags: core
|
||||
threshold: 0.5%
|
||||
viewer:
|
||||
flags: viewer
|
||||
threshold: 0.5%
|
||||
patch: off
|
||||
flags:
|
||||
core:
|
||||
paths:
|
||||
- platform/core
|
||||
viewer:
|
||||
paths:
|
||||
- platform/app
|
||||
@@ -0,0 +1,6 @@
|
||||
[codespell]
|
||||
skip = .git,*.pdf,*.svg,yarn.lock,*.min.js,locales
|
||||
# ignore words ending with … and some camelcased variables and names
|
||||
ignore-regex = \b\S+…\S*|\b(doubleClick|afterAll|PostgresSQL)\b|\bWee, L\.|.*te.*Telugu.*
|
||||
# some odd variables
|
||||
ignore-words-list = datea,ser,childrens
|
||||
@@ -1,48 +0,0 @@
|
||||
worker_processes 1;
|
||||
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
|
||||
upstream orthanc-server {
|
||||
server orthanc:8042;
|
||||
}
|
||||
|
||||
server {
|
||||
listen [::]:80 default_server;
|
||||
listen 80;
|
||||
|
||||
# CORS Magic
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow_Credentials' 'true';
|
||||
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
|
||||
|
||||
location / {
|
||||
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow_Credentials' 'true';
|
||||
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
proxy_pass http://orthanc:8042;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
|
||||
# CORS Magic
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow_Credentials' 'true';
|
||||
add_header 'Access-Control-Allow-Headers' 'Authorization,Accept,Origin,DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS,PUT,DELETE,PATCH';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
{
|
||||
"Name": "Orthanc inside Docker",
|
||||
"StorageDirectory": "/var/lib/orthanc/db",
|
||||
"IndexDirectory": "/var/lib/orthanc/db",
|
||||
"StorageCompression": false,
|
||||
"MaximumStorageSize": 0,
|
||||
"MaximumPatientCount": 0,
|
||||
"LuaScripts": [],
|
||||
"Plugins": ["/usr/share/orthanc/plugins", "/usr/local/share/orthanc/plugins"],
|
||||
"ConcurrentJobs": 2,
|
||||
"HttpServerEnabled": true,
|
||||
"HttpPort": 8042,
|
||||
"HttpDescribeErrors": true,
|
||||
"HttpCompressionEnabled": true,
|
||||
"DicomServerEnabled": true,
|
||||
"DicomAet": "ORTHANC",
|
||||
"DicomCheckCalledAet": false,
|
||||
"DicomPort": 4242,
|
||||
"DefaultEncoding": "Latin1",
|
||||
"DeflatedTransferSyntaxAccepted": true,
|
||||
"JpegTransferSyntaxAccepted": true,
|
||||
"Jpeg2000TransferSyntaxAccepted": true,
|
||||
"JpegLosslessTransferSyntaxAccepted": true,
|
||||
"JpipTransferSyntaxAccepted": true,
|
||||
"Mpeg2TransferSyntaxAccepted": true,
|
||||
"RleTransferSyntaxAccepted": true,
|
||||
"UnknownSopClassAccepted": false,
|
||||
"DicomScpTimeout": 30,
|
||||
|
||||
"RemoteAccessAllowed": true,
|
||||
"SslEnabled": false,
|
||||
"SslCertificate": "certificate.pem",
|
||||
"AuthenticationEnabled": false,
|
||||
"RegisteredUsers": {
|
||||
"test": "test"
|
||||
},
|
||||
"DicomModalities": {},
|
||||
"DicomModalitiesInDatabase": false,
|
||||
"DicomAlwaysAllowEcho": true,
|
||||
"DicomAlwaysAllowStore": true,
|
||||
"DicomCheckModalityHost": false,
|
||||
"DicomScuTimeout": 10,
|
||||
"OrthancPeers": {},
|
||||
"OrthancPeersInDatabase": false,
|
||||
"HttpProxy": "",
|
||||
|
||||
"HttpVerbose": true,
|
||||
|
||||
"HttpTimeout": 10,
|
||||
"HttpsVerifyPeers": true,
|
||||
"HttpsCACertificates": "",
|
||||
"UserMetadata": {},
|
||||
"UserContentType": {},
|
||||
"StableAge": 60,
|
||||
"StrictAetComparison": false,
|
||||
"StoreMD5ForAttachments": true,
|
||||
"LimitFindResults": 0,
|
||||
"LimitFindInstances": 0,
|
||||
"LimitJobs": 10,
|
||||
"LogExportedResources": false,
|
||||
"KeepAlive": true,
|
||||
"TcpNoDelay": true,
|
||||
"HttpThreadsCount": 50,
|
||||
"StoreDicom": true,
|
||||
"DicomAssociationCloseDelay": 5,
|
||||
"QueryRetrieveSize": 10,
|
||||
"CaseSensitivePN": false,
|
||||
"LoadPrivateDictionary": true,
|
||||
"Dictionary": {},
|
||||
"SynchronousCMove": true,
|
||||
"JobsHistorySize": 10,
|
||||
"SaveJobs": true,
|
||||
"OverwriteInstances": false,
|
||||
"MediaArchiveSize": 1,
|
||||
"StorageAccessOnFind": "Always",
|
||||
"MetricsEnabled": true,
|
||||
|
||||
"DicomWeb": {
|
||||
"Enable": true,
|
||||
"Root": "/dicom-web/",
|
||||
"EnableWado": true,
|
||||
"WadoRoot": "/wado",
|
||||
"Host": "127.0.0.1",
|
||||
"Ssl": false,
|
||||
"StowMaxInstances": 10,
|
||||
"StowMaxSize": 10,
|
||||
"QidoCaseSensitive": false
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
orthanc:
|
||||
image: jodogne/orthanc-plugins:1.11.0
|
||||
hostname: orthanc
|
||||
volumes:
|
||||
# Config
|
||||
- ./config/orthanc.json:/etc/orthanc/orthanc.json:ro
|
||||
# Persist data
|
||||
- ./volumes/orthanc-db/:/var/lib/orthanc/db/
|
||||
ports:
|
||||
- '4242:4242' # DICOM
|
||||
- '8042:8042' # Web
|
||||
restart: unless-stopped
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- ./config/nginx.conf:/etc/nginx/nginx.conf
|
||||
ports:
|
||||
- '80:80' #ngnix proxy
|
||||
depends_on:
|
||||
- orthanc
|
||||
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,6 +1,12 @@
|
||||
# Reduces size of context and hides
|
||||
# files from Docker (can't COPY or ADD these)
|
||||
|
||||
# Note that typically the Docker context for various OHIF containers is the
|
||||
# directory of this file (i.e. the root of the source). As such, this is
|
||||
# the .dockerignore file for ALL Docker containers that are built. For example,
|
||||
# the Docker containers built from the recipes in ./platform/app/.recipes will
|
||||
# have this file as their .dockerignore.
|
||||
|
||||
# Output
|
||||
dist/
|
||||
build/
|
||||
@@ -28,3 +34,4 @@ dockerfile
|
||||
.vscode/
|
||||
coverage/
|
||||
docs/
|
||||
testdata/
|
||||
+1
-6
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"import",
|
||||
"eslint-plugin-tsdoc",
|
||||
"prettier"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "import", "eslint-plugin-tsdoc", "prettier"],
|
||||
"extends": [
|
||||
"react-app",
|
||||
"eslint:recommended",
|
||||
|
||||
@@ -39,8 +39,7 @@ body:
|
||||
attributes:
|
||||
label: The current behavior
|
||||
description:
|
||||
'A clear and concise description of what happens instead of the expected
|
||||
behavior.'
|
||||
'A clear and concise description of what happens instead of the expected behavior.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -48,8 +47,7 @@ body:
|
||||
id: expected_behavior
|
||||
attributes:
|
||||
label: The expected behavior
|
||||
description:
|
||||
'A clear and concise description of what you expected to happen.'
|
||||
description: 'A clear and concise description of what you expected to happen.'
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -66,7 +64,7 @@ body:
|
||||
attributes:
|
||||
label: 'Node version'
|
||||
description: 'Your Node.js version.'
|
||||
placeholder: 'e.g., 16.14.0'
|
||||
placeholder: 'e.g., 18.16.1'
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -81,6 +79,6 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
> :warning: Reports we cannot reproduce are at risk of being marked
|
||||
stale and > closed. The more information you can provide, the more
|
||||
likely we are to look > into and address your issue.
|
||||
> :warning: Reports we cannot reproduce are at risk of being marked stale and > closed. The
|
||||
more information you can provide, the more likely we are to look > into and address your
|
||||
issue.
|
||||
@@ -20,17 +20,15 @@ body:
|
||||
attributes:
|
||||
label: 'What feature or change would you like to see made?'
|
||||
description:
|
||||
'Please include as much detail as possible including possibly mock up
|
||||
screen shots, workflow or logic flow diagrams etc.'
|
||||
'Please include as much detail as possible including possibly mock up screen shots, workflow
|
||||
or logic flow diagrams etc.'
|
||||
placeholder: '...'
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 'Why should we prioritize this feature?'
|
||||
description:
|
||||
'Discuss if and how the requested feature interacts with existing
|
||||
features.'
|
||||
description: 'Discuss if and how the requested feature interacts with existing features.'
|
||||
placeholder: '...'
|
||||
validations:
|
||||
required: true
|
||||
@@ -83,7 +83,7 @@ after the commits are squashed.
|
||||
#### Tested Environment
|
||||
|
||||
- [] OS: <!--[e.g. Windows 10, macOS 10.15.4]-->
|
||||
- [] Node version: <!--[e.g. 16.14.0]-->
|
||||
- [] Node version: <!--[e.g. 18.16.1]-->
|
||||
- [] Browser:
|
||||
<!--[e.g. Chrome 83.0.4103.116, Firefox 77.0.1, Safari 13.1.1]-->
|
||||
|
||||
|
||||
+2
-3
@@ -19,8 +19,7 @@ exemptLabels:
|
||||
staleLabel: 'Stale :baguette_bread:'
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
This issue has been automatically marked as stale because it has not had recent activity. It will
|
||||
be closed if no further activity occurs. Thank you for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
@@ -24,6 +24,7 @@ yarn-error.log
|
||||
.DS_Store
|
||||
.env
|
||||
*.code-workspace
|
||||
.directory
|
||||
|
||||
# Common Example Data Directories
|
||||
sampledata/
|
||||
@@ -42,3 +43,6 @@ platform/app/src/pluginImports.js
|
||||
/Viewers.iml
|
||||
platform/app/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/*
|
||||
platform/app/.recipes/OpenResty-Orthanc/logs/*
|
||||
.vercel
|
||||
|
||||
.vs
|
||||
+17
-19
@@ -1,23 +1,21 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>OHIF Viewer: Deploy Preview</title>
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<title>OHIF Viewer: Deploy Preview</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Index of Previews</h1>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/pwa">OHIF Viewer</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs">Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/ui">UI: Component Library</a>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
<body>
|
||||
<h1>Index of Previews</h1>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/pwa">OHIF Viewer</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/docs">Documentation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/ui">UI: Component Library</a>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
+1
-1
@@ -1 +1 @@
|
||||
16.14.0
|
||||
18.16.1
|
||||
+5
-2
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"],
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80,
|
||||
"printWidth": 100,
|
||||
"proseWrap": "always",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid"
|
||||
"arrowParens": "avoid",
|
||||
"singleAttributePerLine": true,
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* This script uses nodejs to generate a JSON file from a DICOM study folder.
|
||||
* You need to have dcmjs installed in your project.
|
||||
* The JSON file can be used to load the study into the OHIF Viewer. You can get more detail
|
||||
* in the DICOM JSON Data source on docs.ohif.org
|
||||
*
|
||||
* Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>
|
||||
*
|
||||
* params:
|
||||
* - studyFolder: path to the study folder
|
||||
* - urlPrefix: prefix to the url that will be used to load the study into the viewer. For instance
|
||||
* we use https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data as the urlPrefix for the
|
||||
* example since the data is hosted on S3 and each study is in a folder. So the url in the generated
|
||||
* json file for the first instance of the first series of the first study will be
|
||||
* dicomweb:https://ohif-assets.s3.us-east-2.amazonaws.com/dicom-json/data/Series1/Instance1
|
||||
* - outputJSONPath: path to the output JSON file
|
||||
*/
|
||||
const dcmjs = require('dcmjs');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const [studyDirectory, urlPrefix, outputPath] = args;
|
||||
|
||||
if (args.length !== 3) {
|
||||
console.error('Usage: node dicomStudyToJSONLaunch.js <studyFolder> <urlPrefix> <outputJSONPath>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const model = {
|
||||
studies: [],
|
||||
};
|
||||
|
||||
async function convertDICOMToJSON(studyDirectory, urlPrefix, outputPath) {
|
||||
try {
|
||||
const files = await recursiveReadDir(studyDirectory);
|
||||
console.debug('Processing...');
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.includes('.DS_Store') && !file.includes('.xml')) {
|
||||
const arrayBuffer = await fs.readFile(file);
|
||||
const dicomDict = dcmjs.data.DicomMessage.readFile(arrayBuffer.buffer);
|
||||
const instance = dcmjs.data.DicomMetaDictionary.naturalizeDataset(dicomDict.dict);
|
||||
|
||||
instance.fileLocation = createImageId(file, urlPrefix, studyDirectory);
|
||||
processInstance(instance);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Successfully loaded data');
|
||||
|
||||
model.studies.forEach(study => {
|
||||
study.NumInstances = findInstancesNumber(study);
|
||||
study.Modalities = findModalities(study).join('/');
|
||||
});
|
||||
|
||||
await fs.writeFile(outputPath, JSON.stringify(model, null, 2));
|
||||
console.log('JSON saved');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function recursiveReadDir(dir) {
|
||||
let results = [];
|
||||
const list = await fs.readdir(dir);
|
||||
for (const file of list) {
|
||||
const filePath = path.resolve(dir, file);
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
const res = await recursiveReadDir(filePath);
|
||||
results = results.concat(res);
|
||||
} else {
|
||||
results.push(filePath);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function createImageId(fileLocation, urlPrefix, studyDirectory) {
|
||||
const relativePath = path.relative(studyDirectory, fileLocation);
|
||||
const normalizedPath = path.normalize(relativePath).replace(/\\/g, '/');
|
||||
return `dicomweb:${urlPrefix}${normalizedPath}`;
|
||||
}
|
||||
|
||||
function processInstance(instance) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = instance;
|
||||
let study = getStudy(StudyInstanceUID);
|
||||
|
||||
if (!study) {
|
||||
study = createStudyMetadata(StudyInstanceUID, instance);
|
||||
model.studies.push(study);
|
||||
}
|
||||
|
||||
let series = getSeries(StudyInstanceUID, SeriesInstanceUID);
|
||||
|
||||
if (!series) {
|
||||
series = createSeriesMetadata(instance);
|
||||
study.series.push(series);
|
||||
}
|
||||
|
||||
const instanceMetaData =
|
||||
instance.NumberOfFrames > 1
|
||||
? createInstanceMetaDataMultiFrame(instance)
|
||||
: createInstanceMetaData(instance);
|
||||
|
||||
series.instances.push(...[].concat(instanceMetaData));
|
||||
}
|
||||
|
||||
function getStudy(StudyInstanceUID) {
|
||||
return model.studies.find(study => study.StudyInstanceUID === StudyInstanceUID);
|
||||
}
|
||||
|
||||
function getSeries(StudyInstanceUID, SeriesInstanceUID) {
|
||||
const study = getStudy(StudyInstanceUID);
|
||||
return study
|
||||
? study.series.find(series => series.SeriesInstanceUID === SeriesInstanceUID)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const findInstancesNumber = study => {
|
||||
let numInstances = 0;
|
||||
study.series.forEach(aSeries => {
|
||||
numInstances = numInstances + aSeries.instances.length;
|
||||
});
|
||||
return numInstances;
|
||||
};
|
||||
|
||||
const findModalities = study => {
|
||||
let modalities = new Set();
|
||||
study.series.forEach(aSeries => {
|
||||
modalities.add(aSeries.Modality);
|
||||
});
|
||||
return Array.from(modalities);
|
||||
};
|
||||
|
||||
function createStudyMetadata(StudyInstanceUID, instance) {
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
StudyDescription: instance.StudyDescription,
|
||||
StudyDate: instance.StudyDate,
|
||||
StudyTime: instance.StudyTime,
|
||||
PatientName: instance.PatientName,
|
||||
PatientID: instance.PatientID || '1234', // this is critical to have
|
||||
AccessionNumber: instance.AccessionNumber,
|
||||
PatientAge: instance.PatientAge,
|
||||
PatientSex: instance.PatientSex,
|
||||
PatientWeight: instance.PatientWeight,
|
||||
series: [],
|
||||
};
|
||||
}
|
||||
function createSeriesMetadata(instance) {
|
||||
return {
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
SeriesDescription: instance.SeriesDescription,
|
||||
SeriesNumber: instance.SeriesNumber,
|
||||
SeriesTime: instance.SeriesTime,
|
||||
Modality: instance.Modality,
|
||||
SliceThickness: instance.SliceThickness,
|
||||
instances: [],
|
||||
};
|
||||
}
|
||||
function commonMetaData(instance) {
|
||||
return {
|
||||
Columns: instance.Columns,
|
||||
Rows: instance.Rows,
|
||||
InstanceNumber: instance.InstanceNumber,
|
||||
SOPClassUID: instance.SOPClassUID,
|
||||
AcquisitionNumber: instance.AcquisitionNumber,
|
||||
PhotometricInterpretation: instance.PhotometricInterpretation,
|
||||
BitsAllocated: instance.BitsAllocated,
|
||||
BitsStored: instance.BitsStored,
|
||||
PixelRepresentation: instance.PixelRepresentation,
|
||||
SamplesPerPixel: instance.SamplesPerPixel,
|
||||
PixelSpacing: instance.PixelSpacing,
|
||||
HighBit: instance.HighBit,
|
||||
ImageOrientationPatient: instance.ImageOrientationPatient,
|
||||
ImagePositionPatient: instance.ImagePositionPatient,
|
||||
FrameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
ImageType: instance.ImageType,
|
||||
Modality: instance.Modality,
|
||||
SOPInstanceUID: instance.SOPInstanceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
WindowCenter: instance.WindowCenter,
|
||||
WindowWidth: instance.WindowWidth,
|
||||
RescaleIntercept: instance.RescaleIntercept,
|
||||
RescaleSlope: instance.RescaleSlope,
|
||||
};
|
||||
}
|
||||
|
||||
function conditionalMetaData(instance) {
|
||||
return {
|
||||
...(instance.ConceptNameCodeSequence && {
|
||||
ConceptNameCodeSequence: instance.ConceptNameCodeSequence,
|
||||
}),
|
||||
...(instance.SeriesDate && { SeriesDate: instance.SeriesDate }),
|
||||
...(instance.ReferencedSeriesSequence && {
|
||||
ReferencedSeriesSequence: instance.ReferencedSeriesSequence,
|
||||
}),
|
||||
...(instance.SharedFunctionalGroupsSequence && {
|
||||
SharedFunctionalGroupsSequence: instance.SharedFunctionalGroupsSequence,
|
||||
}),
|
||||
...(instance.PerFrameFunctionalGroupsSequence && {
|
||||
PerFrameFunctionalGroupsSequence: instance.PerFrameFunctionalGroupsSequence,
|
||||
}),
|
||||
...(instance.ContentSequence && { ContentSequence: instance.ContentSequence }),
|
||||
...(instance.ContentTemplateSequence && {
|
||||
ContentTemplateSequence: instance.ContentTemplateSequence,
|
||||
}),
|
||||
...(instance.CurrentRequestedProcedureEvidenceSequence && {
|
||||
CurrentRequestedProcedureEvidenceSequence: instance.CurrentRequestedProcedureEvidenceSequence,
|
||||
}),
|
||||
...(instance.CodingSchemeIdentificationSequence && {
|
||||
CodingSchemeIdentificationSequence: instance.CodingSchemeIdentificationSequence,
|
||||
}),
|
||||
...(instance.RadiopharmaceuticalInformationSequence && {
|
||||
RadiopharmaceuticalInformationSequence: instance.RadiopharmaceuticalInformationSequence,
|
||||
}),
|
||||
...(instance.ROIContourSequence && {
|
||||
ROIContourSequence: instance.ROIContourSequence,
|
||||
}),
|
||||
...(instance.StructureSetROISequence && {
|
||||
StructureSetROISequence: instance.StructureSetROISequence,
|
||||
}),
|
||||
...(instance.ReferencedFrameOfReferenceSequence && {
|
||||
ReferencedFrameOfReferenceSequence: instance.ReferencedFrameOfReferenceSequence,
|
||||
}),
|
||||
...(instance.CorrectedImage && { CorrectedImage: instance.CorrectedImage }),
|
||||
...(instance.Units && { Units: instance.Units }),
|
||||
...(instance.DecayCorrection && { DecayCorrection: instance.DecayCorrection }),
|
||||
...(instance.AcquisitionDate && { AcquisitionDate: instance.AcquisitionDate }),
|
||||
...(instance.AcquisitionTime && { AcquisitionTime: instance.AcquisitionTime }),
|
||||
...(instance.PatientWeight && { PatientWeight: instance.PatientWeight }),
|
||||
...(instance.NumberOfFrames && { NumberOfFrames: instance.NumberOfFrames }),
|
||||
...(instance.FrameTime && { FrameTime: instance.FrameTime }),
|
||||
...(instance.EncapsulatedDocument && { EncapsulatedDocument: instance.EncapsulatedDocument }),
|
||||
...(instance.SequenceOfUltrasoundRegions && {
|
||||
SequenceOfUltrasoundRegions: instance.SequenceOfUltrasoundRegions,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function createInstanceMetaData(instance) {
|
||||
const metadata = {
|
||||
...commonMetaData(instance),
|
||||
...conditionalMetaData(instance),
|
||||
};
|
||||
return { metadata, url: instance.fileLocation };
|
||||
}
|
||||
|
||||
function createInstanceMetaDataMultiFrame(instance) {
|
||||
const instances = [];
|
||||
const commonData = commonMetaData(instance);
|
||||
const conditionalData = conditionalMetaData(instance);
|
||||
|
||||
for (let i = 1; i <= instance.NumberOfFrames; i++) {
|
||||
const metadata = { ...commonData, ...conditionalData };
|
||||
const result = { metadata, url: instance.fileLocation + `?frame=${i}` };
|
||||
instances.push(result);
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
convertDICOMToJSON(studyDirectory, urlPrefix, outputPath);
|
||||
Vendored
+1
-1
@@ -31,6 +31,6 @@
|
||||
"prettier.endOfLine": "lf",
|
||||
"workbench.colorCustomizations": {},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ function excludeNodeModulesExcept(modules) {
|
||||
if (pathSep == '\\')
|
||||
// must be quoted for use in a regexp:
|
||||
pathSep = '\\\\';
|
||||
var moduleRegExps = modules.map(function(modName) {
|
||||
var moduleRegExps = modules.map(function (modName) {
|
||||
return new RegExp('node_modules' + pathSep + modName);
|
||||
});
|
||||
|
||||
return function(modulePath) {
|
||||
return function (modulePath) {
|
||||
if (/node_modules/.test(modulePath)) {
|
||||
for (var i = 0; i < moduleRegExps.length; i++)
|
||||
if (moduleRegExps[i].test(modulePath)) return false;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const path = require('path');
|
||||
const tailwindcss = require('tailwindcss');
|
||||
const tailwindConfigPath = path.resolve(
|
||||
'../../platform/app/tailwind.config.js'
|
||||
);
|
||||
const tailwindConfigPath = path.resolve('../../platform/app/tailwind.config.js');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const devMode = process.env.NODE_ENV !== 'production';
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const stylusToJavaScript = {
|
||||
test: /\.styl$/,
|
||||
use: [
|
||||
{ loader: 'style-loader' }, // 3. Style nodes from JS Strings
|
||||
{ loader: 'css-loader' }, // 2. CSS to CommonJS
|
||||
{ loader: 'stylus-loader' }, // 1. Stylus to CSS
|
||||
],
|
||||
};
|
||||
|
||||
module.exports = stylusToJavaScript;
|
||||
+49
-31
@@ -7,8 +7,7 @@ const fs = require('fs');
|
||||
const webpack = require('webpack');
|
||||
|
||||
// ~~ PLUGINS
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
|
||||
.BundleAnalyzerPlugin;
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||
|
||||
// ~~ PackageJSON
|
||||
@@ -19,6 +18,7 @@ const loadShadersRule = require('./rules/loadShaders.js');
|
||||
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
||||
const transpileJavaScriptRule = require('./rules/transpileJavaScript.js');
|
||||
const cssToJavaScript = require('./rules/cssToJavaScript.js');
|
||||
const stylusToJavaScript = require('./rules/stylusToJavaScript.js');
|
||||
|
||||
// ~~ ENV VARS
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
@@ -26,15 +26,34 @@ const QUICK_BUILD = process.env.QUICK_BUILD;
|
||||
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
|
||||
|
||||
// read from ../version.txt
|
||||
const VERSION_NUMBER =
|
||||
fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
|
||||
const VERSION_NUMBER = fs.readFileSync(path.join(__dirname, '../version.txt'), 'utf8') || '';
|
||||
|
||||
const COMMIT_HASH =
|
||||
fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8') || '';
|
||||
const COMMIT_HASH = fs.readFileSync(path.join(__dirname, '../commit.txt'), 'utf8') || '';
|
||||
|
||||
//
|
||||
dotenv.config();
|
||||
|
||||
const defineValues = {
|
||||
/* Application */
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.NODE_DEBUG': JSON.stringify(process.env.NODE_DEBUG),
|
||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
|
||||
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
|
||||
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
|
||||
'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER),
|
||||
'process.env.COMMIT_HASH': JSON.stringify(COMMIT_HASH),
|
||||
/* i18n */
|
||||
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
|
||||
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
|
||||
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
|
||||
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(process.env.REACT_APP_I18N_DEBUG || ''),
|
||||
};
|
||||
|
||||
// Only redefine updated values. This avoids warning messages in the logs
|
||||
if (!process.env.APP_CONFIG) {
|
||||
defineValues['process.env.APP_CONFIG'] = '';
|
||||
}
|
||||
|
||||
module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
if (!process.env.NODE_ENV) {
|
||||
throw new Error('process.env.NODE_ENV not set');
|
||||
@@ -94,10 +113,24 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
},
|
||||
},
|
||||
cssToJavaScript,
|
||||
// Note: Only uncomment the following if you are using the old style of stylus in v2
|
||||
// Also you need to uncomment this platform/app/.webpack/rules/extractStyleChunks.js
|
||||
// stylusToJavaScript,
|
||||
{
|
||||
test: /\.wasm/,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: 'assets/images/[name].[ext]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
], //.concat(vtkRules),
|
||||
},
|
||||
resolve: {
|
||||
@@ -105,10 +138,7 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
alias: {
|
||||
// Viewer project
|
||||
'@': path.resolve(__dirname, '../platform/app/src'),
|
||||
'@components': path.resolve(
|
||||
__dirname,
|
||||
'../platform/app/src/components'
|
||||
),
|
||||
'@components': path.resolve(__dirname, '../platform/app/src/components'),
|
||||
'@hooks': path.resolve(__dirname, '../platform/app/src/hooks'),
|
||||
'@routes': path.resolve(__dirname, '../platform/app/src/routes'),
|
||||
'@state': path.resolve(__dirname, '../platform/app/src/state'),
|
||||
@@ -131,29 +161,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '*'],
|
||||
// symlinked resources are resolved to their real path, not their symlinked location
|
||||
symlinks: true,
|
||||
fallback: { fs: false, path: false, zlib: false },
|
||||
fallback: {
|
||||
fs: false,
|
||||
path: false,
|
||||
zlib: false,
|
||||
buffer: require.resolve('buffer'),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
/* Application */
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
|
||||
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
|
||||
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
|
||||
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
|
||||
'process.env.VERSION_NUMBER': JSON.stringify(VERSION_NUMBER),
|
||||
'process.env.COMMIT_HASH': JSON.stringify(COMMIT_HASH),
|
||||
/* i18n */
|
||||
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
|
||||
'process.env.LOCIZE_PROJECTID': JSON.stringify(
|
||||
process.env.LOCIZE_PROJECTID || ''
|
||||
),
|
||||
'process.env.LOCIZE_API_KEY': JSON.stringify(
|
||||
process.env.LOCIZE_API_KEY || ''
|
||||
),
|
||||
'process.env.REACT_APP_I18N_DEBUG': JSON.stringify(
|
||||
process.env.REACT_APP_I18N_DEBUG || ''
|
||||
),
|
||||
new webpack.DefinePlugin(defineValues),
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
// Uncomment to generate bundle analyzer
|
||||
// new BundleAnalyzerPlugin(),
|
||||
|
||||
+1635
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -21,7 +21,7 @@
|
||||
|
||||
# Stage 1: Build the application
|
||||
# docker build -t ohif/viewer:latest .
|
||||
FROM node:16.15.0-slim as json-copier
|
||||
FROM node:18.16.1-slim as json-copier
|
||||
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
@@ -37,7 +37,7 @@ COPY platform /usr/src/app/platform
|
||||
#RUN find platform \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
|
||||
|
||||
# Copy Files
|
||||
FROM node:16.15.0-slim as builder
|
||||
FROM node:18.16.1-slim as builder
|
||||
RUN apt-get update && apt-get install -y build-essential python3
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -17,6 +17,10 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
|
||||
<div align="center">
|
||||
📰 <a href="https://ohif.org/news/"><strong>Join OHIF Newsletter</strong></a> 📰
|
||||
</div>
|
||||
<div align="center">
|
||||
📰 <a href="https://ohif.org/news/"><strong>Join OHIF Newsletter</strong></a> 📰
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<hr />
|
||||
@@ -37,12 +41,15 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
|
||||
|
||||
| | | |
|
||||
| :-: | :--- | :--- |
|
||||
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.275741864483510678566144889372061815320) |
|
||||
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
|
||||
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-measurements.webp?raw=true" alt="Measurement tracking" width="350"/> | Measurement Tracking | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-segmentation.webp?raw=true" alt="Segmentations" width="350"/> | Labelmap Segmentations | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-ptct.webp?raw=true" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols | [Demo](https://viewer.ohif.org/tmtv?StudyInstanceUIDs=1.3.6.1.4.1.14519.5.2.1.7009.2403.334240657131972136850343327463) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-volume-rendering.webp?raw=true" alt="Volume Rendering" width="350"/> | Volume Rendering | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&hangingprotocolId=mprAnd3DVolumeViewport) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-pdf.webp?raw=true" alt="PDF" width="350"/> | PDF | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.317377619501274872606137091638706705333) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-rtstruct.webp?raw=true" alt="RTSTRUCT" width="350"/> | RT STRUCT | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=1.3.6.1.4.1.5962.99.1.2968617883.1314880426.1493322302363.3.0) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-4d.webp?raw=true" alt="4D" width="350"/> | 4D | [Demo](https://viewer.ohif.org/dynamic-volume?StudyInstanceUIDs=2.25.232704420736447710317909004159492840763) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/demo-video.webp?raw=true" alt="VIDEO" width="350"/> | Video | [Demo](https://viewer.ohif.org/viewer?StudyInstanceUIDs=2.25.96975534054447904995905761963464388233) |
|
||||
| <img src="https://github.com/OHIF/Viewers/blob/master/platform/docs/docs/assets/img/microscopy.webp?raw=true" alt="microscopy" width="350"/> | Slide Microscopy | [Demo](https://viewer.ohif.org/microscopy?StudyInstanceUIDs=2.25.141277760791347900862109212450152067508) |
|
||||
|
||||
|
||||
## About
|
||||
|
||||
+3
-5
@@ -3,11 +3,7 @@ const { extendDefaultPlugins } = require('svgo');
|
||||
|
||||
module.exports = {
|
||||
babelrcRoots: ['./platform/*', './extensions/*', './modes/*'],
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
|
||||
plugins: [
|
||||
[
|
||||
'inline-react-svg',
|
||||
@@ -30,6 +26,7 @@ module.exports = {
|
||||
'@babel/plugin-transform-typescript',
|
||||
['@babel/plugin-proposal-private-property-in-object', { loose: true }],
|
||||
['@babel/plugin-proposal-private-methods', { loose: true }],
|
||||
'@babel/plugin-transform-class-static-block',
|
||||
],
|
||||
env: {
|
||||
test: {
|
||||
@@ -49,6 +46,7 @@ module.exports = {
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
'@babel/plugin-transform-regenerator',
|
||||
'@babel/transform-destructuring',
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@babel/plugin-transform-typescript',
|
||||
],
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
f845f87716512da78b44c633378d16027813d7b9
|
||||
339299113c65b5ce4032b02b5b4043a8968107ed
|
||||
@@ -1,138 +0,0 @@
|
||||
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
||||
import { IWebApiDataSource } from '@ohif/core';
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default {
|
||||
id: '@ohif/extension-*',
|
||||
|
||||
/**
|
||||
* LIFECYCLE HOOKS
|
||||
*/
|
||||
preRegistration() {},
|
||||
beforeExtInit() {},
|
||||
beforeExtDestroy() {},
|
||||
|
||||
/**
|
||||
* MODULES
|
||||
*/
|
||||
getCommandsModule,
|
||||
getContextModule,
|
||||
getDataSourcesModule,
|
||||
getLayoutTemplateModule,
|
||||
getPanelModule,
|
||||
getSopClassHandlerModule,
|
||||
getToolbarModule() {},
|
||||
getViewportModule,
|
||||
};
|
||||
|
||||
// appConfig,
|
||||
// extensionConfig,
|
||||
// dataSources,
|
||||
// servicesManager,
|
||||
// extensionManager,
|
||||
// commandsManager,
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
const getCommandsModule = () => ({
|
||||
definitions: {
|
||||
exampleActionDef: {
|
||||
commandFn: ({ param1 }) => {
|
||||
console.log(`param1's value is: ${param1}`);
|
||||
},
|
||||
// storeContexts: ['viewports'],
|
||||
options: { param1: 'hello world' },
|
||||
context: 'VIEWER', // optional
|
||||
},
|
||||
},
|
||||
defaultContext: 'ACTIVE_VIEWPORT::DICOMSR',
|
||||
});
|
||||
|
||||
const ExampleContext = React.createContext();
|
||||
|
||||
function ExampleContextProvider({ children }) {
|
||||
return (
|
||||
<ExampleContext.Provider value={{ example: 'value' }}>
|
||||
{children}
|
||||
</ExampleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const getContextModule = () => [
|
||||
{
|
||||
name: 'ExampleContext',
|
||||
context: ExampleContext,
|
||||
provider: ExampleContextProvider,
|
||||
},
|
||||
];
|
||||
|
||||
const getDataSourcesModule = () => [
|
||||
{
|
||||
name: 'exampleDataSource',
|
||||
type: 'webApi', // 'webApi' | 'local' | 'other'
|
||||
createDataSource: dataSourceConfig => {
|
||||
return IWebApiDataSource.create(/* */);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getLayoutTemplateModule = (/* ... */) => [
|
||||
{
|
||||
id: 'exampleLayout',
|
||||
name: 'exampleLayout',
|
||||
component: ExampleLayoutComponent,
|
||||
},
|
||||
];
|
||||
|
||||
const getPanelModule = () => {
|
||||
return [
|
||||
{
|
||||
name: 'exampleSidePanel',
|
||||
iconName: 'info-circle-o',
|
||||
iconLabel: 'Example',
|
||||
label: 'Hello World',
|
||||
isDisabled: studies => {}, // optional
|
||||
component: ExamplePanelContentComponent,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getSopClassHandlerModule = (/* ... */) => {
|
||||
const BASIC_TEXT_SR = '1.2.840.10008.5.1.4.1.1.88.11';
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'ExampleSopClassHandle',
|
||||
sopClassUids: [BASIC_TEXT_SR],
|
||||
getDisplaySetsFromSeries: instances => {
|
||||
const imageSet = new ImageSet(instances);
|
||||
|
||||
imageSet.setAttributes(/** */);
|
||||
imageSet.sortBy((a, b) => 0);
|
||||
|
||||
return imageSet;
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const getToolbarModule = () => {};
|
||||
|
||||
// displaySet, viewportIndex, dataSource
|
||||
const getViewportModule = () => {
|
||||
const wrappedViewport = props => {
|
||||
return (
|
||||
<ExampleViewport
|
||||
{...props}
|
||||
onEvent={data => {
|
||||
commandsManager.runCommand('commandName', data);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [{ name: 'example', component: wrappedViewport }];
|
||||
};
|
||||
@@ -3,8 +3,7 @@ const { merge } = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||
const pkg = require('./../package.json');
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
|
||||
.BundleAnalyzerPlugin;
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './..');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
@@ -38,13 +37,7 @@ module.exports = (env, argv) => {
|
||||
libraryTarget: 'umd',
|
||||
filename: pkg.main,
|
||||
},
|
||||
externals: [
|
||||
/\b(vtk.js)/,
|
||||
/\b(dcmjs)/,
|
||||
/\b(gl-matrix)/,
|
||||
/^@ohif/,
|
||||
/^@cornerstonejs/,
|
||||
],
|
||||
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -10,7 +10,7 @@ module.exports = {
|
||||
modules: 'commonjs',
|
||||
debug: false,
|
||||
},
|
||||
"@babel/preset-typescript",
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
'@babel/preset-react',
|
||||
],
|
||||
@@ -26,7 +26,7 @@ module.exports = {
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
"@babel/preset-typescript",
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
@@ -35,7 +35,7 @@ module.exports = {
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
"@babel/preset-typescript",
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-rt",
|
||||
"version": "3.7.0-beta.54",
|
||||
"version": "3.8.2",
|
||||
"description": "DICOM RT read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -24,6 +24,8 @@
|
||||
"yarn": ">=1.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"dev:dicom-seg": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
@@ -31,10 +33,10 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.7.0-beta.54",
|
||||
"@ohif/extension-cornerstone": "3.7.0-beta.54",
|
||||
"@ohif/extension-default": "3.7.0-beta.54",
|
||||
"@ohif/i18n": "3.7.0-beta.54",
|
||||
"@ohif/core": "3.8.2\n",
|
||||
"@ohif/extension-cornerstone": "3.8.2\n",
|
||||
"@ohif/extension-default": "3.8.2\n",
|
||||
"@ohif/i18n": "3.8.2\n",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
+14
-21
@@ -5,11 +5,11 @@ import loadRTStruct from './loadRTStruct';
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3'];
|
||||
|
||||
let loadPromises = {};
|
||||
const loadPromises = {};
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
@@ -56,10 +56,7 @@ function _getDisplaySetsFromSeries(
|
||||
};
|
||||
|
||||
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
if (
|
||||
instance.ReferencedFrameOfReferenceSequence &&
|
||||
!instance.ReferencedSeriesSequence
|
||||
) {
|
||||
if (instance.ReferencedFrameOfReferenceSequence && !instance.ReferencedSeriesSequence) {
|
||||
instance.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
instance.ReferencedFrameOfReferenceSequence
|
||||
);
|
||||
@@ -72,13 +69,12 @@ function _getDisplaySetsFromSeries(
|
||||
|
||||
const referencedSeries = referencedSeriesSequence[0];
|
||||
|
||||
displaySet.referencedImages =
|
||||
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
const { DisplaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = DisplaySetService.getDisplaySetsForSeries(
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const referencedDisplaySets = displaySetService.getDisplaySetsForSeries(
|
||||
displaySet.referencedSeriesInstanceUID
|
||||
);
|
||||
|
||||
@@ -88,19 +84,17 @@ function _getDisplaySetsFromSeries(
|
||||
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
displaySet.referencedDisplaySetInstanceUID =
|
||||
referencedDisplaySet.displaySetInstanceUID;
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
|
||||
return referencedDisplaySet;
|
||||
};
|
||||
|
||||
displaySet.load = ({ headers }) =>
|
||||
_load(displaySet, servicesManager, extensionManager, headers);
|
||||
displaySet.load = ({ headers }) => _load(displaySet, servicesManager, extensionManager, headers);
|
||||
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(rtDisplaySet, servicesManager, extensionManager, headers) {
|
||||
function _load(rtDisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager, headers) {
|
||||
const { SOPInstanceUID } = rtDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
if (
|
||||
@@ -178,7 +172,10 @@ function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
return ReferencedSeriesSequence;
|
||||
}
|
||||
|
||||
function _segmentationExistsInCache(rtDisplaySet, segmentationService) {
|
||||
function _segmentationExistsInCache(
|
||||
rtDisplaySet,
|
||||
segmentationService: AppTypes.SegmentationService
|
||||
) {
|
||||
// Todo: fix this
|
||||
return false;
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
@@ -194,11 +191,7 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
name: 'dicom-rt',
|
||||
sopClassUids,
|
||||
getDisplaySetsFromSeries: instances => {
|
||||
return _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
);
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -2,12 +2,9 @@ import { id } from './id';
|
||||
import React from 'react';
|
||||
import { Types } from '@ohif/core';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import hydrateRTDisplaySet from './utils/_hydrateRT';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport'
|
||||
);
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneRTViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstoneRTViewport = props => {
|
||||
@@ -62,4 +59,3 @@ const extension: Types.Extensions.Extension = {
|
||||
};
|
||||
|
||||
export default extension;
|
||||
export { hydrateRTDisplaySet };
|
||||
@@ -25,23 +25,13 @@ async function checkAndLoadContourData(instance, datasource) {
|
||||
|
||||
if (Array.isArray(contourData)) {
|
||||
promisesMap.has(referencedROINumber)
|
||||
? promisesMap
|
||||
.get(referencedROINumber)
|
||||
.push(Promise.resolve(contourData))
|
||||
: promisesMap.set(referencedROINumber, [
|
||||
Promise.resolve(contourData),
|
||||
]);
|
||||
? promisesMap.get(referencedROINumber).push(Promise.resolve(contourData))
|
||||
: promisesMap.set(referencedROINumber, [Promise.resolve(contourData)]);
|
||||
} else if (contourData && contourData.BulkDataURI) {
|
||||
const bulkDataURI = contourData.BulkDataURI;
|
||||
|
||||
if (
|
||||
!datasource ||
|
||||
!datasource.retrieve ||
|
||||
!datasource.retrieve.bulkDataURI
|
||||
) {
|
||||
return Promise.reject(
|
||||
'Invalid datasource object or retrieve function'
|
||||
);
|
||||
if (!datasource || !datasource.retrieve || !datasource.retrieve.bulkDataURI) {
|
||||
return Promise.reject('Invalid datasource object or retrieve function');
|
||||
}
|
||||
|
||||
const bulkDataPromise = datasource.retrieve.bulkDataURI({
|
||||
@@ -74,10 +64,7 @@ async function checkAndLoadContourData(instance, datasource) {
|
||||
ROIContour.ContourSequence.forEach((Contour, index) => {
|
||||
const promise = resolvedPromises[index];
|
||||
if (promise.status === 'fulfilled') {
|
||||
if (
|
||||
Array.isArray(promise.value) &&
|
||||
promise.value.every(Number.isFinite)
|
||||
) {
|
||||
if (Array.isArray(promise.value) && promise.value.every(Number.isFinite)) {
|
||||
// If promise.value is already an array of numbers, use it directly
|
||||
Contour.ContourData = promise.value;
|
||||
} else {
|
||||
@@ -85,13 +72,8 @@ async function checkAndLoadContourData(instance, datasource) {
|
||||
const uint8Array = new Uint8Array(promise.value);
|
||||
const textDecoder = new TextDecoder();
|
||||
const dataUint8Array = textDecoder.decode(uint8Array);
|
||||
if (
|
||||
typeof dataUint8Array === 'string' &&
|
||||
dataUint8Array.includes('\\')
|
||||
) {
|
||||
Contour.ContourData = dataUint8Array
|
||||
.split('\\')
|
||||
.map(parseFloat);
|
||||
if (typeof dataUint8Array === 'string' && dataUint8Array.includes('\\')) {
|
||||
Contour.ContourData = dataUint8Array.split('\\').map(parseFloat);
|
||||
} else {
|
||||
Contour.ContourData = [];
|
||||
}
|
||||
@@ -120,9 +102,8 @@ export default async function loadRTStruct(
|
||||
const { bulkDataURI } = dataSource.getConfig?.() || {};
|
||||
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||
referencedDisplaySet
|
||||
);
|
||||
const imageIdSopInstanceUidPairs =
|
||||
_getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet);
|
||||
|
||||
// Set here is loading is asynchronous.
|
||||
// If this function throws its set back to false.
|
||||
@@ -137,20 +118,14 @@ export default async function loadRTStruct(
|
||||
);
|
||||
|
||||
const dicomData = DicomMessage.readFile(segArrayBuffer);
|
||||
const rtStructDataset = DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
const rtStructDataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
||||
rtStructDataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
instance = rtStructDataset;
|
||||
} else {
|
||||
await checkAndLoadContourData(instance, dataSource);
|
||||
}
|
||||
|
||||
const {
|
||||
StructureSetROISequence,
|
||||
ROIContourSequence,
|
||||
RTROIObservationsSequence,
|
||||
} = instance;
|
||||
const { StructureSetROISequence, ROIContourSequence, RTROIObservationsSequence } = instance;
|
||||
|
||||
// Define our structure set entry and add it to the rtstruct module state.
|
||||
const structureSet = {
|
||||
@@ -174,12 +149,8 @@ export default async function loadRTStruct(
|
||||
|
||||
const contourPoints = [];
|
||||
for (let c = 0; c < ContourSequenceArray.length; c++) {
|
||||
const {
|
||||
ContourImageSequence,
|
||||
ContourData,
|
||||
NumberOfContourPoints,
|
||||
ContourGeometricType,
|
||||
} = ContourSequenceArray[c];
|
||||
const { ContourImageSequence, ContourData, NumberOfContourPoints, ContourGeometricType } =
|
||||
ContourSequenceArray[c];
|
||||
|
||||
let isSupported = false;
|
||||
|
||||
@@ -229,9 +200,7 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||
);
|
||||
|
||||
return imageIdSopInstanceUidPairsEntry
|
||||
? imageIdSopInstanceUidPairsEntry.imageId
|
||||
: null;
|
||||
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
|
||||
};
|
||||
|
||||
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
|
||||
@@ -252,8 +221,7 @@ function _setROIContourMetadata(
|
||||
isSupported
|
||||
) {
|
||||
const StructureSetROI = StructureSetROISequence.find(
|
||||
structureSetROI =>
|
||||
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||
structureSetROI => structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||
);
|
||||
|
||||
const ROIContourData = {
|
||||
@@ -293,23 +261,15 @@ function _setROIContourDataColor(ROIContour, ROIContourData) {
|
||||
}
|
||||
}
|
||||
|
||||
function _setROIContourRTROIObservations(
|
||||
ROIContourData,
|
||||
RTROIObservationsSequence,
|
||||
ROINumber
|
||||
) {
|
||||
function _setROIContourRTROIObservations(ROIContourData, RTROIObservationsSequence, ROINumber) {
|
||||
const RTROIObservations = RTROIObservationsSequence.find(
|
||||
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
|
||||
);
|
||||
|
||||
if (RTROIObservations) {
|
||||
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
|
||||
const {
|
||||
ObservationNumber,
|
||||
ROIObservationDescription,
|
||||
RTROIInterpretedType,
|
||||
ROIInterpreter,
|
||||
} = RTROIObservations;
|
||||
const { ObservationNumber, ROIObservationDescription, RTROIInterpretedType, ROIInterpreter } =
|
||||
RTROIObservations;
|
||||
|
||||
ROIContourData.RTROIObservations = {
|
||||
ObservationNumber,
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
async function _hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
}) {
|
||||
const {
|
||||
segmentationService,
|
||||
hangingProtocolService,
|
||||
viewportGridService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const displaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
|
||||
|
||||
let segmentationId = null;
|
||||
|
||||
// We need the hydration to notify panels about the new segmentation added
|
||||
const suppressEvents = false;
|
||||
|
||||
segmentationId = await segmentationService.createSegmentationForRTDisplaySet(
|
||||
rtDisplaySet,
|
||||
segmentationId,
|
||||
suppressEvents
|
||||
);
|
||||
|
||||
segmentationService.hydrateSegmentation(rtDisplaySet.displaySetInstanceUID);
|
||||
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportIndex,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
|
||||
// Todo: fix this after we have a better way for stack viewport segmentations
|
||||
|
||||
// check every viewport in the viewports to see if the displaySetInstanceUID
|
||||
// is being displayed, if so we need to update the viewport to use volume viewport
|
||||
// (if already is not using it) since Cornerstone3D currently only supports
|
||||
// volume viewport for segmentation
|
||||
viewports.forEach((viewport, index) => {
|
||||
if (index === viewportIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
||||
viewport.displaySetInstanceUIDs,
|
||||
rtDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
updatedViewports.push({
|
||||
viewportIndex: index,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Do the entire update at once
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
return true;
|
||||
}
|
||||
|
||||
export default _hydrateRTDisplaySet;
|
||||
@@ -1,12 +1,7 @@
|
||||
function createRTToolGroupAndAddTools(
|
||||
ToolGroupService,
|
||||
customizationService,
|
||||
toolGroupId
|
||||
) {
|
||||
const { tools } =
|
||||
customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
function createRTToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
|
||||
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
}
|
||||
|
||||
export default createRTToolGroupAndAddTools;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ButtonEnums } from '@ohif/ui';
|
||||
import hydrateRTDisplaySet from './_hydrateRT';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
@@ -10,17 +9,15 @@ const RESPONSE = {
|
||||
function promptHydrateRT({
|
||||
servicesManager,
|
||||
rtDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
toolGroupId = 'default',
|
||||
preHydrateCallbacks,
|
||||
}) {
|
||||
hydrateRTDisplaySet,
|
||||
}: withAppTypes) {
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
|
||||
return new Promise(async function(resolve, reject) {
|
||||
const promptResult = await _askHydrate(
|
||||
uiViewportDialogService,
|
||||
viewportIndex
|
||||
);
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
|
||||
|
||||
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
||||
preHydrateCallbacks?.forEach(callback => {
|
||||
@@ -29,7 +26,7 @@ function promptHydrateRT({
|
||||
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
});
|
||||
@@ -39,8 +36,8 @@ function promptHydrateRT({
|
||||
});
|
||||
}
|
||||
|
||||
function _askHydrate(uiViewportDialogService, viewportIndex) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService, viewportId) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const message = 'Do you want to open this Segmentation?';
|
||||
const actions = [
|
||||
{
|
||||
@@ -60,7 +57,8 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
viewportIndex,
|
||||
id: 'promptHydrateRT',
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
@@ -69,6 +67,11 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import {
|
||||
ViewportActionBar,
|
||||
useViewportGrid,
|
||||
LoadingIndicatorTotalPercent,
|
||||
} from '@ohif/ui';
|
||||
import { useViewportGrid, LoadingIndicatorTotalPercent, ViewportActionArrows } from '@ohif/ui';
|
||||
|
||||
import _hydrateRTdisplaySet from '../utils/_hydrateRT';
|
||||
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
import createRTToolGroupAndAddTools from '../utils/initRTToolGroup';
|
||||
import _hydrateRTDisplaySet from '../utils/_hydrateRT';
|
||||
|
||||
const { formatDate } = utils;
|
||||
const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup';
|
||||
|
||||
function OHIFCornerstoneRTViewport(props) {
|
||||
function OHIFCornerstoneRTViewport(props: withAppTypes) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
viewportIndex,
|
||||
viewportLabel,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
@@ -34,9 +24,12 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
segmentationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const toolGroupId = `${RT_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
const toolGroupId = `${RT_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
||||
|
||||
// RT viewport will always have a single display set
|
||||
if (displaySets.length > 1) {
|
||||
@@ -67,12 +60,10 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
// refs
|
||||
const referencedDisplaySetRef = useRef(null);
|
||||
|
||||
const { viewports, activeViewportIndex } = viewportGrid;
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const referencedDisplaySet = rtDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||
referencedDisplaySet
|
||||
);
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(referencedDisplaySet);
|
||||
|
||||
referencedDisplaySetRef.current = {
|
||||
displaySet: referencedDisplaySet,
|
||||
@@ -93,21 +84,26 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
};
|
||||
|
||||
const storePresentationState = useCallback(() => {
|
||||
viewportGrid?.viewports.forEach(({ viewportIndex }) => {
|
||||
viewportGrid?.viewports.forEach(({ viewportId }) => {
|
||||
commandsManager.runCommand('storePresentation', {
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
});
|
||||
});
|
||||
}, [viewportGrid]);
|
||||
|
||||
const hydrateRTDisplaySet = ({ rtDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
||||
displaySets: [rtDisplaySet],
|
||||
viewportId,
|
||||
});
|
||||
};
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
const {
|
||||
displaySet: referencedDisplaySet,
|
||||
} = referencedDisplaySetRef.current;
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
|
||||
// Todo: jump to the center of the first segment
|
||||
return (
|
||||
@@ -115,20 +111,22 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
{...props}
|
||||
displaySets={[referencedDisplaySet, rtDisplaySet]}
|
||||
viewportOptions={{
|
||||
viewportType: 'volume',
|
||||
viewportType: 'stack',
|
||||
toolGroupId: toolGroupId,
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
></Component>
|
||||
);
|
||||
}, [viewportIndex, rtDisplaySet, toolGroupId]);
|
||||
}, [viewportId, rtDisplaySet, toolGroupId]);
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
direction = direction === 'left' ? -1 : 1;
|
||||
const segmentationId = rtDisplaySet.displaySetInstanceUID;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
@@ -145,11 +143,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
segmentationService.jumpToSegmentCenter(
|
||||
segmentationId,
|
||||
newSelectedSegmentIndex,
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment]
|
||||
@@ -162,32 +156,31 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
promptHydrateRT({
|
||||
servicesManager,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
rtDisplaySet,
|
||||
preHydrateCallbacks: [storePresentationState],
|
||||
hydrateRTDisplaySet,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
});
|
||||
}, [servicesManager, viewportIndex, rtDisplaySet, rtIsLoading]);
|
||||
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
// I'm not sure what is this, since in RT we support Overlapping segments
|
||||
// via contours
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
if (
|
||||
evt.rtDisplaySet.displaySetInstanceUID ===
|
||||
rtDisplaySet.displaySetInstanceUID
|
||||
) {
|
||||
if (evt.rtDisplaySet.displaySetInstanceUID === rtDisplaySet.displaySetInstanceUID) {
|
||||
setRtIsLoading(false);
|
||||
}
|
||||
|
||||
if (evt.overlappingSegments) {
|
||||
uiNotificationService.show({
|
||||
title: 'Overlapping Segments',
|
||||
message:
|
||||
'Overlapping segments detected which is not currently supported',
|
||||
message: 'Overlapping segments detected which is not currently supported',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
@@ -222,12 +215,10 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||
({ displaySetInstanceUIDs }) => {
|
||||
const activeViewport = viewports[activeViewportIndex];
|
||||
if (
|
||||
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
|
||||
) {
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: activeViewportIndex,
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUIDs: [],
|
||||
});
|
||||
}
|
||||
@@ -246,19 +237,13 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
return;
|
||||
}
|
||||
|
||||
toolGroup = createRTToolGroupAndAddTools(
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
toolGroupId
|
||||
);
|
||||
toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
||||
|
||||
setToolGroupCreated(true);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
|
||||
toolGroupService.destroyToolGroup(toolGroupId);
|
||||
};
|
||||
@@ -269,9 +254,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
referencedDisplaySetRef.current = null;
|
||||
};
|
||||
}, [rtDisplaySet]);
|
||||
@@ -292,27 +275,14 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
PatientSex,
|
||||
PatientAge,
|
||||
SliceThickness,
|
||||
ManufacturerModelName,
|
||||
StudyDate,
|
||||
SeriesDescription,
|
||||
SpacingBetweenSlices,
|
||||
SeriesNumber,
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
const onStatusClick = async () => {
|
||||
const onStatusClick = useCallback(async () => {
|
||||
// Before hydrating a RT and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the RT. This is
|
||||
@@ -320,56 +290,57 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
// presentation state (w/l and invert) and then opens the RT. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
const isHydrated = await _hydrateRTDisplaySet({
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
};
|
||||
}, [hydrateRTDisplaySet, rtDisplaySet, storePresentationState, viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.setComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
activeViewportId,
|
||||
isHydrated,
|
||||
onSegmentChange,
|
||||
onStatusClick,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
onDoubleClick={evt => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}}
|
||||
onArrowsClick={onSegmentChange}
|
||||
getStatusComponent={() => {
|
||||
return _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
});
|
||||
}}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: `RT Viewport ${SeriesDescription}`,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
: '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined
|
||||
? `${SpacingBetweenSlices.toFixed(2)}mm`
|
||||
: '',
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{rtIsLoading && (
|
||||
<LoadingIndicatorTotalPercent
|
||||
className="w-full h-full"
|
||||
className="h-full w-full"
|
||||
totalNumbers={processingProgress.totalSegments}
|
||||
percentComplete={processingProgress.percentComplete}
|
||||
loadingText="Loading RTSTRUCT..."
|
||||
@@ -384,7 +355,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
OHIFCornerstoneRTViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object,
|
||||
|
||||
@@ -6,45 +6,53 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
switch (isHydrated) {
|
||||
case true:
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>This Segmentation is loaded in the segmentation panel</div>
|
||||
);
|
||||
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
|
||||
break;
|
||||
case false:
|
||||
StatusIcon = () => <Icon name="status-untracked" />;
|
||||
StatusIcon = () => (
|
||||
<Icon
|
||||
className="text-aqua-pale"
|
||||
name="status-untracked"
|
||||
/>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => <div>Click LOAD to load RTSTRUCT.</div>;
|
||||
}
|
||||
|
||||
const StatusArea = () => (
|
||||
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
|
||||
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">RTSTRUCT</span>
|
||||
</div>
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
const StatusArea = () => {
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
return (
|
||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">RTSTRUCT</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<Tooltip
|
||||
content={<ToolTipMessage />}
|
||||
position="bottom-left"
|
||||
>
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -40,21 +40,15 @@ module.exports = (env, argv) => {
|
||||
libraryTarget: 'umd',
|
||||
filename: pkg.main,
|
||||
},
|
||||
externals: [
|
||||
/\b(vtk.js)/,
|
||||
/\b(dcmjs)/,
|
||||
/\b(gl-matrix)/,
|
||||
/^@ohif/,
|
||||
/^@cornerstonejs/,
|
||||
],
|
||||
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
// new MiniCssExtractPlugin({
|
||||
// filename: `./dist/${outputName}.css`,
|
||||
// chunkFilename: `./dist/${outputName}.css`,
|
||||
// }),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `./dist/${outputName}.css`,
|
||||
chunkFilename: `./dist/${outputName}.css`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -10,7 +10,7 @@ module.exports = {
|
||||
modules: 'commonjs',
|
||||
debug: false,
|
||||
},
|
||||
"@babel/preset-typescript",
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
'@babel/preset-react',
|
||||
],
|
||||
@@ -26,7 +26,7 @@ module.exports = {
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
"@babel/preset-typescript",
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
},
|
||||
@@ -35,7 +35,7 @@ module.exports = {
|
||||
// WebPack handles ES6 --> Target Syntax
|
||||
['@babel/preset-env', { modules: false }],
|
||||
'@babel/preset-react',
|
||||
"@babel/preset-typescript",
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: ['react-hot-loader/babel'],
|
||||
ignore: ['**/*.test.jsx', '**/*.test.js', '__snapshots__', '__tests__'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-seg",
|
||||
"version": "3.7.0-beta.54",
|
||||
"version": "3.8.2",
|
||||
"description": "DICOM SEG read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -24,6 +24,8 @@
|
||||
"yarn": ">=1.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"dev:dicom-seg": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
@@ -31,10 +33,10 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.7.0-beta.54",
|
||||
"@ohif/extension-cornerstone": "3.7.0-beta.54",
|
||||
"@ohif/extension-default": "3.7.0-beta.54",
|
||||
"@ohif/i18n": "3.7.0-beta.54",
|
||||
"@ohif/core": "3.8.2\n",
|
||||
"@ohif/extension-cornerstone": "3.8.2\n",
|
||||
"@ohif/extension-default": "3.8.2\n",
|
||||
"@ohif/i18n": "3.8.2\n",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
@@ -44,6 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.77.6",
|
||||
"@cornerstonejs/core": "^1.77.6",
|
||||
"@kitware/vtk.js": "30.4.1",
|
||||
"react-color": "^2.19.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import { createReportDialogPrompt } from '@ohif/extension-default';
|
||||
import { Types } from '@ohif/core';
|
||||
import { cache, metaData } from '@cornerstonejs/core';
|
||||
import {
|
||||
segmentation as cornerstoneToolsSegmentation,
|
||||
Enums as cornerstoneToolsEnums,
|
||||
utilities,
|
||||
} from '@cornerstonejs/tools';
|
||||
import { adaptersRT, helpers, adaptersSEG } from '@cornerstonejs/adapters';
|
||||
import { classes, DicomMetadataStore } from '@ohif/core';
|
||||
|
||||
import vtkImageMarchingSquares from '@kitware/vtk.js/Filters/General/ImageMarchingSquares';
|
||||
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||
|
||||
import {
|
||||
updateViewportsForSegmentationRendering,
|
||||
getUpdatedViewportsForSegmentation,
|
||||
getTargetViewport,
|
||||
} from './utils/hydrationUtils';
|
||||
const { segmentation: segmentationUtils } = utilities;
|
||||
|
||||
const { datasetToBlob } = dcmjs.data;
|
||||
|
||||
const {
|
||||
Cornerstone3D: {
|
||||
Segmentation: { generateLabelMaps2DFrom3D, generateSegmentation },
|
||||
},
|
||||
} = adaptersSEG;
|
||||
|
||||
const {
|
||||
Cornerstone3D: {
|
||||
RTSS: { generateRTSSFromSegmentations },
|
||||
},
|
||||
} = adaptersRT;
|
||||
|
||||
const { downloadDICOMData } = helpers;
|
||||
|
||||
const commandsModule = ({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}: Types.Extensions.ExtensionParams): Types.Extensions.CommandsModule => {
|
||||
const {
|
||||
uiNotificationService,
|
||||
segmentationService,
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
toolGroupService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
|
||||
* This function evaluates viewports based on their compatibility with the provided segmentation's
|
||||
* frame of reference UID and appends them to the updated list if they should render the segmentation.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - the ID of the viewport to be updated.
|
||||
* @param params.servicesManager - The services manager
|
||||
* @param params.referencedDisplaySetInstanceUID - Optional UID for the referenced display set instance.
|
||||
*
|
||||
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
|
||||
*/
|
||||
getUpdatedViewportsForSegmentation,
|
||||
/**
|
||||
* Creates an empty segmentation for a specified viewport.
|
||||
* It first checks if the display set associated with the viewport is reconstructable.
|
||||
* If not, it raises a notification error. Otherwise, it creates a new segmentation
|
||||
* for the display set after handling the necessary steps for making the viewport
|
||||
* a volume viewport first
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - the target viewport ID.
|
||||
*
|
||||
*/
|
||||
createEmptySegmentationForViewport: async ({ viewportId }) => {
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
// Todo: add support for multiple display sets
|
||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
if (!displaySet.isReconstructable) {
|
||||
uiNotificationService.show({
|
||||
title: 'Segmentation',
|
||||
message: 'Segmentation is not supported for non-reconstructible displaysets yet',
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
loadFn: async () => {
|
||||
const currentSegmentations = segmentationService.getSegmentations();
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
displaySetInstanceUID,
|
||||
{ label: `Segmentation ${currentSegmentations.length + 1}` }
|
||||
);
|
||||
|
||||
const toolGroupId = viewport.viewportOptions.toolGroupId;
|
||||
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId
|
||||
);
|
||||
|
||||
// Add only one segment for now
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
toolGroupId,
|
||||
segmentIndex: 1,
|
||||
properties: {
|
||||
label: 'Segment 1',
|
||||
},
|
||||
});
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Loads segmentations for a specified viewport.
|
||||
* The function prepares the viewport for rendering, then loads the segmentation details.
|
||||
* Additionally, if the segmentation has scalar data, it is set for the corresponding label map volume.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.segmentations - Array of segmentations to be loaded.
|
||||
* @param params.viewportId - the target viewport ID.
|
||||
*
|
||||
*/
|
||||
loadSegmentationsForViewport: async ({ segmentations, viewportId }) => {
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
loadFn: async () => {
|
||||
// Todo: handle adding more than one segmentation
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const displaySetInstanceUID = viewport.displaySetInstanceUIDs[0];
|
||||
|
||||
const segmentation = segmentations[0];
|
||||
const segmentationId = segmentation.id;
|
||||
const label = segmentation.label;
|
||||
const segments = segmentation.segments;
|
||||
|
||||
delete segmentation.segments;
|
||||
|
||||
await segmentationService.createSegmentationForDisplaySet(displaySetInstanceUID, {
|
||||
segmentationId,
|
||||
label,
|
||||
});
|
||||
|
||||
if (segmentation.scalarData) {
|
||||
const labelmapVolume = segmentationService.getLabelmapVolume(segmentationId);
|
||||
labelmapVolume.scalarData.set(segmentation.scalarData);
|
||||
}
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(segmentation);
|
||||
|
||||
const toolGroupId = viewport.viewportOptions.toolGroupId;
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId
|
||||
);
|
||||
|
||||
segments.forEach(segment => {
|
||||
if (segment === null) {
|
||||
return;
|
||||
}
|
||||
segmentationService.addSegment(segmentationId, {
|
||||
segmentIndex: segment.segmentIndex,
|
||||
toolGroupId,
|
||||
properties: {
|
||||
color: segment.color,
|
||||
label: segment.label,
|
||||
opacity: segment.opacity,
|
||||
isLocked: segment.isLocked,
|
||||
visibility: segment.isVisible,
|
||||
active: segmentation.activeSegmentIndex === segment.segmentIndex,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (segmentation.centroidsIJK) {
|
||||
segmentationService.setCentroids(segmentation.id, segmentation.centroidsIJK);
|
||||
}
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Loads segmentation display sets for a specified viewport.
|
||||
* Depending on the modality of the display set (SEG or RTSTRUCT),
|
||||
* it chooses the appropriate service function to create
|
||||
* the segmentation for the display set.
|
||||
* The function then prepares the viewport for rendering segmentation.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - ID of the viewport where the segmentation display sets should be loaded.
|
||||
* @param params.displaySets - Array of display sets to be loaded for segmentation.
|
||||
*
|
||||
*/
|
||||
loadSegmentationDisplaySetsForViewport: async ({ viewportId, displaySets }) => {
|
||||
// Todo: handle adding more than one segmentation
|
||||
const displaySet = displaySets[0];
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySet.referencedDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
loadFn: async () => {
|
||||
const segDisplaySet = displaySet;
|
||||
const suppressEvents = false;
|
||||
const serviceFunction =
|
||||
segDisplaySet.Modality === 'SEG'
|
||||
? 'createSegmentationForSEGDisplaySet'
|
||||
: 'createSegmentationForRTDisplaySet';
|
||||
|
||||
const boundFn = segmentationService[serviceFunction].bind(segmentationService);
|
||||
const segmentationId = await boundFn(segDisplaySet, null, suppressEvents);
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
segmentation.description = `S${referencedDisplaySet.SeriesNumber}: ${referencedDisplaySet.SeriesDescription}`;
|
||||
return segmentationId;
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Generates a segmentation from a given segmentation ID.
|
||||
* This function retrieves the associated segmentation and
|
||||
* its referenced volume, extracts label maps from the
|
||||
* segmentation volume, and produces segmentation data
|
||||
* alongside associated metadata.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.segmentationId - ID of the segmentation to be generated.
|
||||
* @param params.options - Optional configuration for the generation process.
|
||||
*
|
||||
* @returns Returns the generated segmentation data.
|
||||
*/
|
||||
generateSegmentation: ({ segmentationId, options = {} }) => {
|
||||
const segmentation = cornerstoneToolsSegmentation.state.getSegmentation(segmentationId);
|
||||
|
||||
const { referencedVolumeId } = segmentation.representationData.LABELMAP;
|
||||
|
||||
const segmentationVolume = cache.getVolume(segmentationId);
|
||||
const referencedVolume = cache.getVolume(referencedVolumeId);
|
||||
const referencedImages = referencedVolume.getCornerstoneImages();
|
||||
|
||||
const labelmapObj = generateLabelMaps2DFrom3D(segmentationVolume);
|
||||
|
||||
// Generate fake metadata as an example
|
||||
labelmapObj.metadata = [];
|
||||
|
||||
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
|
||||
labelmapObj.segmentsOnLabelmap.forEach(segmentIndex => {
|
||||
// segmentation service already has a color for each segment
|
||||
const segment = segmentationInOHIF?.segments[segmentIndex];
|
||||
const { label, color } = segment;
|
||||
|
||||
const RecommendedDisplayCIELabValue = dcmjs.data.Colors.rgb2DICOMLAB(
|
||||
color.slice(0, 3).map(value => value / 255)
|
||||
).map(value => Math.round(value));
|
||||
|
||||
const segmentMetadata = {
|
||||
SegmentNumber: segmentIndex.toString(),
|
||||
SegmentLabel: label,
|
||||
SegmentAlgorithmType: 'MANUAL',
|
||||
SegmentAlgorithmName: 'OHIF Brush',
|
||||
RecommendedDisplayCIELabValue,
|
||||
SegmentedPropertyCategoryCodeSequence: {
|
||||
CodeValue: 'T-D0050',
|
||||
CodingSchemeDesignator: 'SRT',
|
||||
CodeMeaning: 'Tissue',
|
||||
},
|
||||
SegmentedPropertyTypeCodeSequence: {
|
||||
CodeValue: 'T-D0050',
|
||||
CodingSchemeDesignator: 'SRT',
|
||||
CodeMeaning: 'Tissue',
|
||||
},
|
||||
};
|
||||
labelmapObj.metadata[segmentIndex] = segmentMetadata;
|
||||
});
|
||||
|
||||
const generatedSegmentation = generateSegmentation(
|
||||
referencedImages,
|
||||
labelmapObj,
|
||||
metaData,
|
||||
options
|
||||
);
|
||||
|
||||
return generatedSegmentation;
|
||||
},
|
||||
/**
|
||||
* Downloads a segmentation based on the provided segmentation ID.
|
||||
* This function retrieves the associated segmentation and
|
||||
* uses it to generate the corresponding DICOM dataset, which
|
||||
* is then downloaded with an appropriate filename.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.segmentationId - ID of the segmentation to be downloaded.
|
||||
*
|
||||
*/
|
||||
downloadSegmentation: ({ segmentationId }) => {
|
||||
const segmentationInOHIF = segmentationService.getSegmentation(segmentationId);
|
||||
const generatedSegmentation = actions.generateSegmentation({
|
||||
segmentationId,
|
||||
});
|
||||
|
||||
downloadDICOMData(generatedSegmentation.dataset, `${segmentationInOHIF.label}`);
|
||||
},
|
||||
/**
|
||||
* Stores a segmentation based on the provided segmentationId into a specified data source.
|
||||
* The SeriesDescription is derived from user input or defaults to the segmentation label,
|
||||
* and in its absence, defaults to 'Research Derived Series'.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.segmentationId - ID of the segmentation to be stored.
|
||||
* @param params.dataSource - Data source where the generated segmentation will be stored.
|
||||
*
|
||||
* @returns {Object|void} Returns the naturalized report if successfully stored,
|
||||
* otherwise throws an error.
|
||||
*/
|
||||
storeSegmentation: async ({ segmentationId, dataSource }) => {
|
||||
const promptResult = await createReportDialogPrompt(uiDialogService, {
|
||||
extensionManager,
|
||||
});
|
||||
|
||||
if (promptResult.action !== 1 && promptResult.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
if (!segmentation) {
|
||||
throw new Error('No segmentation found');
|
||||
}
|
||||
|
||||
const { label } = segmentation;
|
||||
const SeriesDescription = promptResult.value || label || 'Research Derived Series';
|
||||
|
||||
const generatedData = actions.generateSegmentation({
|
||||
segmentationId,
|
||||
options: {
|
||||
SeriesDescription,
|
||||
},
|
||||
});
|
||||
|
||||
if (!generatedData || !generatedData.dataset) {
|
||||
throw new Error('Error during segmentation generation');
|
||||
}
|
||||
|
||||
const { dataset: naturalizedReport } = generatedData;
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport);
|
||||
|
||||
// The "Mode" route listens for DicomMetadataStore changes
|
||||
// When a new instance is added, it listens and
|
||||
// automatically calls makeDisplaySets
|
||||
|
||||
// add the information for where we stored it to the instance as well
|
||||
naturalizedReport.wadoRoot = dataSource.getConfig().wadoRoot;
|
||||
|
||||
DicomMetadataStore.addInstances([naturalizedReport], true);
|
||||
|
||||
return naturalizedReport;
|
||||
},
|
||||
/**
|
||||
* Converts segmentations into RTSS for download.
|
||||
* This sample function retrieves all segentations and passes to
|
||||
* cornerstone tool adapter to convert to DICOM RTSS format. It then
|
||||
* converts dataset to downloadable blob.
|
||||
*
|
||||
*/
|
||||
downloadRTSS: ({ segmentationId }) => {
|
||||
const segmentations = segmentationService.getSegmentation(segmentationId);
|
||||
const vtkUtils = {
|
||||
vtkImageMarchingSquares,
|
||||
vtkDataArray,
|
||||
vtkImageData,
|
||||
};
|
||||
|
||||
const RTSS = generateRTSSFromSegmentations(
|
||||
segmentations,
|
||||
classes.MetadataProvider,
|
||||
DicomMetadataStore,
|
||||
cache,
|
||||
cornerstoneToolsEnums,
|
||||
vtkUtils
|
||||
);
|
||||
|
||||
try {
|
||||
const reportBlob = datasetToBlob(RTSS);
|
||||
|
||||
//Create a URL for the binary.
|
||||
const objectUrl = URL.createObjectURL(reportBlob);
|
||||
window.location.assign(objectUrl);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
},
|
||||
setBrushSize: ({ value, toolNames }) => {
|
||||
const brushSize = Number(value);
|
||||
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
if (toolNames?.length === 0) {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize);
|
||||
} else {
|
||||
toolNames?.forEach(toolName => {
|
||||
segmentationUtils.setBrushSizeForToolGroup(toolGroupId, brushSize, toolName);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
setThresholdRange: ({
|
||||
value,
|
||||
toolNames = ['ThresholdCircularBrush', 'ThresholdSphereBrush'],
|
||||
}) => {
|
||||
toolGroupService.getToolGroupIds()?.forEach(toolGroupId => {
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
toolNames?.forEach(toolName => {
|
||||
toolGroup.setToolConfiguration(toolName, {
|
||||
strategySpecificConfiguration: {
|
||||
THRESHOLD: {
|
||||
threshold: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
getUpdatedViewportsForSegmentation: {
|
||||
commandFn: actions.getUpdatedViewportsForSegmentation,
|
||||
},
|
||||
loadSegmentationDisplaySetsForViewport: {
|
||||
commandFn: actions.loadSegmentationDisplaySetsForViewport,
|
||||
},
|
||||
loadSegmentationsForViewport: {
|
||||
commandFn: actions.loadSegmentationsForViewport,
|
||||
},
|
||||
createEmptySegmentationForViewport: {
|
||||
commandFn: actions.createEmptySegmentationForViewport,
|
||||
},
|
||||
generateSegmentation: {
|
||||
commandFn: actions.generateSegmentation,
|
||||
},
|
||||
downloadSegmentation: {
|
||||
commandFn: actions.downloadSegmentation,
|
||||
},
|
||||
storeSegmentation: {
|
||||
commandFn: actions.storeSegmentation,
|
||||
},
|
||||
downloadRTSS: {
|
||||
commandFn: actions.downloadRTSS,
|
||||
},
|
||||
setBrushSize: {
|
||||
commandFn: actions.setBrushSize,
|
||||
},
|
||||
setThresholdRange: {
|
||||
commandFn: actions.setThresholdRange,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'SEGMENTATION',
|
||||
};
|
||||
};
|
||||
|
||||
export default commandsModule;
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useAppConfig } from '@state';
|
||||
import { Toolbox } from '@ohif/ui';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
|
||||
const getPanelModule = ({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
configuration,
|
||||
title,
|
||||
}: withAppTypes) => {
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
const wrappedPanelSegmentation = configuration => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
disableEditing: appConfig.disableEditing,
|
||||
...customizationService.get('segmentation.panel'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedPanelSegmentationWithTools = configuration => {
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="segmentationToolbox"
|
||||
title="Segmentation Tools"
|
||||
configuration={{
|
||||
...configuration,
|
||||
}}
|
||||
/>
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
configuration={{
|
||||
...configuration,
|
||||
disableEditing: appConfig.disableEditing,
|
||||
...customizationService.get('segmentation.panel'),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'panelSegmentation',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentation,
|
||||
},
|
||||
{
|
||||
name: 'panelSegmentationWithTools',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentationWithTools,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default getPanelModule;
|
||||
+55
-45
@@ -1,10 +1,6 @@
|
||||
import { utils } from '@ohif/core';
|
||||
import {
|
||||
metaData,
|
||||
cache,
|
||||
triggerEvent,
|
||||
eventTarget,
|
||||
} from '@cornerstonejs/core';
|
||||
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||
import { CONSTANTS } from '@cornerstonejs/tools';
|
||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
@@ -12,11 +8,11 @@ import { dicomlabToRGB } from './utils/dicomlabToRGB';
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
|
||||
|
||||
let loadPromises = {};
|
||||
const loadPromises = {};
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
const instance = instances[0];
|
||||
@@ -66,13 +62,13 @@ function _getDisplaySetsFromSeries(
|
||||
const referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
|
||||
if (!referencedSeriesSequence) {
|
||||
throw new Error('ReferencedSeriesSequence is missing for the SEG');
|
||||
console.error('ReferencedSeriesSequence is missing for the SEG');
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedSeries = referencedSeriesSequence[0];
|
||||
const referencedSeries = referencedSeriesSequence[0] || referencedSeriesSequence;
|
||||
|
||||
displaySet.referencedImages =
|
||||
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
@@ -87,8 +83,7 @@ function _getDisplaySetsFromSeries(
|
||||
|
||||
const referencedDisplaySet = referencedDisplaySets[0];
|
||||
|
||||
displaySet.referencedDisplaySetInstanceUID =
|
||||
referencedDisplaySet.displaySetInstanceUID;
|
||||
displaySet.referencedDisplaySetInstanceUID = referencedDisplaySet.displaySetInstanceUID;
|
||||
|
||||
// Todo: this needs to be able to work with other reference volumes (other than streaming) such as nifti, etc.
|
||||
displaySet.referencedVolumeURI = referencedDisplaySet.displaySetInstanceUID;
|
||||
@@ -104,7 +99,12 @@ function _getDisplaySetsFromSeries(
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
function _load(
|
||||
segDisplaySet,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager,
|
||||
headers
|
||||
) {
|
||||
const { SOPInstanceUID } = segDisplaySet;
|
||||
const { segmentationService } = servicesManager.services;
|
||||
|
||||
@@ -121,10 +121,7 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
// We don't want to fire multiple loads, so we'll wait for the first to finish
|
||||
// and also return the same promise to any other callers.
|
||||
loadPromises[SOPInstanceUID] = new Promise(async (resolve, reject) => {
|
||||
if (
|
||||
!segDisplaySet.segments ||
|
||||
Object.keys(segDisplaySet.segments).length === 0
|
||||
) {
|
||||
if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) {
|
||||
await _loadSegments({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
@@ -154,23 +151,17 @@ async function _loadSegments({
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
headers,
|
||||
}) {
|
||||
}: withAppTypes) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { segmentationService, uiNotificationService } = servicesManager.services;
|
||||
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(
|
||||
segDisplaySet,
|
||||
null,
|
||||
headers
|
||||
);
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
|
||||
|
||||
const cachedReferencedVolume = cache.getVolume(
|
||||
segDisplaySet.referencedVolumeId
|
||||
);
|
||||
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
|
||||
|
||||
if (!cachedReferencedVolume) {
|
||||
throw new Error(
|
||||
@@ -186,12 +177,9 @@ async function _loadSegments({
|
||||
|
||||
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
|
||||
const { percentComplete } = evt.detail;
|
||||
segmentationService._broadcastEvent(
|
||||
segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE,
|
||||
{
|
||||
percentComplete,
|
||||
}
|
||||
);
|
||||
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
|
||||
percentComplete,
|
||||
});
|
||||
});
|
||||
|
||||
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
|
||||
@@ -201,29 +189,51 @@ async function _loadSegments({
|
||||
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
|
||||
);
|
||||
|
||||
let usedRecommendedDisplayCIELabValue = true;
|
||||
results.segMetadata.data.forEach((data, i) => {
|
||||
if (i > 0) {
|
||||
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
|
||||
data.rgba = data.RecommendedDisplayCIELabValue;
|
||||
|
||||
if (data.rgba) {
|
||||
data.rgba = dicomlabToRGB(data.rgba);
|
||||
} else {
|
||||
usedRecommendedDisplayCIELabValue = false;
|
||||
data.rgba = CONSTANTS.COLOR_LUT[i % CONSTANTS.COLOR_LUT.length];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (results.overlappingSegments) {
|
||||
uiNotificationService.show({
|
||||
title: 'Overlapping Segments',
|
||||
message:
|
||||
'Unsupported overlapping segments detected, segmentation rendering results may be incorrect.',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
if (!usedRecommendedDisplayCIELabValue) {
|
||||
// Display a notification about the non-utilization of RecommendedDisplayCIELabValue
|
||||
uiNotificationService.show({
|
||||
title: 'DICOM SEG import',
|
||||
message:
|
||||
'RecommendedDisplayCIELabValue not found for one or more segments. The default color was used instead.',
|
||||
type: 'warning',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(segDisplaySet, results);
|
||||
}
|
||||
|
||||
function _segmentationExists(segDisplaySet, segmentationService) {
|
||||
function _segmentationExists(segDisplaySet, segmentationService: AppTypes.SegmentationService) {
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
return segmentationService.getSegmentation(
|
||||
segDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
);
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
return [
|
||||
@@ -0,0 +1,57 @@
|
||||
export function getToolbarModule({ servicesManager }: withAppTypes) {
|
||||
const { segmentationService, toolbarService, toolGroupService } = servicesManager.services;
|
||||
return [
|
||||
{
|
||||
name: 'evaluate.cornerstone.segmentation',
|
||||
evaluate: ({ viewportId, button, toolNames, disabledText }) => {
|
||||
// Todo: we need to pass in the button section Id since we are kind of
|
||||
// forcing the button to have black background since initially
|
||||
// it is designed for the toolbox not the toolbar on top
|
||||
// we should then branch the buttonSectionId to have different styles
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
if (!segmentations?.length) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright !bg-black opacity-50',
|
||||
disabledText: disabledText ?? 'No segmentations available',
|
||||
};
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const toolName = toolbarService.getToolNameForButton(button);
|
||||
|
||||
if (!toolGroup.hasTool(toolName) && !toolNames) {
|
||||
return {
|
||||
disabled: true,
|
||||
className: '!text-common-bright ohif-disabled',
|
||||
disabledText: disabledText ?? 'Not available on the current viewport',
|
||||
};
|
||||
}
|
||||
|
||||
const isPrimaryActive = toolNames
|
||||
? toolNames.includes(toolGroup.getActivePrimaryMouseButtonTool())
|
||||
: toolGroup.getActivePrimaryMouseButtonTool() === toolName;
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
className: isPrimaryActive
|
||||
? '!text-black !bg-primary-light hover:bg-primary-light hover-text-black hover:cursor-pointer'
|
||||
: '!text-common-bright !bg-black hover:bg-primary-light hover:cursor-pointer hover:text-black',
|
||||
// Todo: isActive right now is used for nested buttons where the primary
|
||||
// button needs to be fully rounded (vs partial rounded) when active
|
||||
// otherwise it does not have any other use
|
||||
isActive: isPrimaryActive,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,17 +1,14 @@
|
||||
import { id } from './id';
|
||||
import React from 'react';
|
||||
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import hydrateSEGDisplaySet from './utils/_hydrateSEG';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import { getToolbarModule } from './getToolbarModule';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport'
|
||||
);
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstoneSEGViewport = props => {
|
||||
@@ -31,40 +28,16 @@ const extension = {
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
* for Modes to consume and render. Each panel is defined by a {name,
|
||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||
*/
|
||||
getPanelModule: ({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
}): Types.Panel[] => {
|
||||
const wrappedPanelSegmentation = () => {
|
||||
return (
|
||||
<PanelSegmentation
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'panelSegmentation',
|
||||
iconName: 'tab-segmentation',
|
||||
iconLabel: 'Segmentation',
|
||||
label: 'Segmentation',
|
||||
component: wrappedPanelSegmentation,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
getViewportModule({ servicesManager, extensionManager }) {
|
||||
getPanelModule,
|
||||
getCommandsModule,
|
||||
getToolbarModule,
|
||||
getViewportModule({ servicesManager, extensionManager, commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneSEGViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstoneSEGViewport
|
||||
@@ -76,9 +49,7 @@ const extension = {
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport },
|
||||
];
|
||||
return [{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport }];
|
||||
},
|
||||
/**
|
||||
* SopClassHandlerModule should provide a list of sop class handlers that will be
|
||||
@@ -91,4 +62,3 @@ const extension = {
|
||||
};
|
||||
|
||||
export default extension;
|
||||
export { hydrateSEGDisplaySet };
|
||||
@@ -1,50 +1,40 @@
|
||||
import { createReportAsync } from '@ohif/extension-default';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SegmentationGroupTable } from '@ohif/ui';
|
||||
import { SegmentationGroupTable, SegmentationGroupTableExpanded } from '@ohif/ui';
|
||||
import { SegmentationPanelMode } from '../types/segmentation';
|
||||
import callInputDialog from './callInputDialog';
|
||||
import { useAppConfig } from '@state';
|
||||
import callColorPickerDialog from './colorPickerDialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const components = {
|
||||
[SegmentationPanelMode.Expanded]: SegmentationGroupTableExpanded,
|
||||
[SegmentationPanelMode.Dropdown]: SegmentationGroupTable,
|
||||
};
|
||||
|
||||
export default function PanelSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}) {
|
||||
const { segmentationService, uiDialogService } = servicesManager.services;
|
||||
const [appConfig] = useAppConfig();
|
||||
const disableEditing = appConfig?.disableEditing;
|
||||
extensionManager,
|
||||
configuration,
|
||||
}: withAppTypes) {
|
||||
const {
|
||||
segmentationService,
|
||||
viewportGridService,
|
||||
uiDialogService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const { t } = useTranslation('PanelSegmentation');
|
||||
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
const [addSegmentationClassName, setAddSegmentationClassName] = useState('');
|
||||
const [segmentationConfiguration, setSegmentationConfiguration] = useState(
|
||||
segmentationService.getConfiguration()
|
||||
);
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() =>
|
||||
segmentationService.getSegmentations()
|
||||
);
|
||||
|
||||
const [isMinimized, setIsMinimized] = useState({});
|
||||
|
||||
const onToggleMinimizeSegmentation = useCallback(
|
||||
id => {
|
||||
setIsMinimized(prevState => ({
|
||||
...prevState,
|
||||
[id]: !prevState[id],
|
||||
}));
|
||||
},
|
||||
[setIsMinimized]
|
||||
);
|
||||
|
||||
// Only expand the last segmentation added to the list and collapse the rest
|
||||
useEffect(() => {
|
||||
const lastSegmentationId = segmentations[segmentations.length - 1]?.id;
|
||||
if (lastSegmentationId) {
|
||||
setIsMinimized(prevState => ({
|
||||
...prevState,
|
||||
[lastSegmentationId]: false,
|
||||
}));
|
||||
}
|
||||
}, [segmentations, setIsMinimized]);
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
@@ -69,6 +59,76 @@ export default function PanelSegmentation({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// temporary measure to not allow add segmentation when the selected viewport
|
||||
// is stack viewport
|
||||
useEffect(() => {
|
||||
const handleActiveViewportChange = viewportId => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(
|
||||
viewportId || viewportGridService.getActiveViewportId()
|
||||
);
|
||||
|
||||
if (!displaySetUIDs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isReconstructable =
|
||||
displaySetUIDs?.some(displaySetUID => {
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetUID);
|
||||
return displaySet?.isReconstructable;
|
||||
}) || false;
|
||||
|
||||
if (isReconstructable) {
|
||||
setAddSegmentationClassName('');
|
||||
} else {
|
||||
setAddSegmentationClassName('ohif-disabled');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle initial state
|
||||
handleActiveViewportChange();
|
||||
|
||||
const changedGrid = viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED;
|
||||
const ready = viewportGridService.EVENTS.VIEWPORTS_READY;
|
||||
|
||||
const subsGrid = [];
|
||||
[ready, changedGrid].forEach(evt => {
|
||||
const { unsubscribe } = viewportGridService.subscribe(evt, ({ viewportId }) => {
|
||||
handleActiveViewportChange(viewportId);
|
||||
});
|
||||
|
||||
subsGrid.push(unsubscribe);
|
||||
});
|
||||
|
||||
const changedData = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
|
||||
const subsData = [];
|
||||
[changedData].forEach(evt => {
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, () => {
|
||||
handleActiveViewportChange();
|
||||
});
|
||||
|
||||
subsData.push(unsubscribe);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
return () => {
|
||||
subsGrid.forEach(unsub => unsub());
|
||||
subsData.forEach(unsub => unsub());
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getToolGroupIds = segmentationId => {
|
||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
||||
|
||||
return toolGroupIds;
|
||||
};
|
||||
|
||||
const onSegmentationAdd = async () => {
|
||||
commandsManager.runCommand('createEmptySegmentationForViewport', {
|
||||
viewportId: viewportGridService.getActiveViewportId(),
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentationClick = (segmentationId: string) => {
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId);
|
||||
};
|
||||
@@ -77,33 +137,19 @@ export default function PanelSegmentation({
|
||||
segmentationService.remove(segmentationId);
|
||||
};
|
||||
|
||||
const getToolGroupIds = segmentationId => {
|
||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(
|
||||
segmentationId
|
||||
);
|
||||
|
||||
return toolGroupIds;
|
||||
const onSegmentAdd = segmentationId => {
|
||||
segmentationService.addSegment(segmentationId);
|
||||
};
|
||||
|
||||
const onSegmentClick = (segmentationId, segmentIndex) => {
|
||||
segmentationService.setActiveSegmentForSegmentation(
|
||||
segmentationId,
|
||||
segmentIndex
|
||||
);
|
||||
segmentationService.setActiveSegment(segmentationId, segmentIndex);
|
||||
|
||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
// const toolGroupId =
|
||||
segmentationService.setActiveSegmentationForToolGroup(
|
||||
segmentationId,
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.jumpToSegmentCenter(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, segmentIndex, toolGroupId);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -118,11 +164,7 @@ export default function PanelSegmentation({
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.setSegmentLabelForSegmentation(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
label
|
||||
);
|
||||
segmentationService.setSegmentLabel(segmentationId, segmentIndex, label);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -147,18 +189,37 @@ export default function PanelSegmentation({
|
||||
};
|
||||
|
||||
const onSegmentColorClick = (segmentationId, segmentIndex) => {
|
||||
// Todo: Implement color picker later
|
||||
return;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
const segment = segmentation.segments[segmentIndex];
|
||||
const { color, opacity } = segment;
|
||||
|
||||
const rgbaColor = {
|
||||
r: color[0],
|
||||
g: color[1],
|
||||
b: color[2],
|
||||
a: opacity / 255.0,
|
||||
};
|
||||
|
||||
callColorPickerDialog(uiDialogService, rgbaColor, (newRgbaColor, actionId) => {
|
||||
if (actionId === 'cancel') {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.setSegmentRGBAColor(segmentationId, segmentIndex, [
|
||||
newRgbaColor.r,
|
||||
newRgbaColor.g,
|
||||
newRgbaColor.b,
|
||||
newRgbaColor.a * 255.0,
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
const onSegmentDelete = (segmentationId, segmentIndex) => {
|
||||
// segmentationService.removeSegmentFromSegmentation(
|
||||
// segmentationId,
|
||||
// segmentIndex
|
||||
// );
|
||||
console.warn('not implemented yet');
|
||||
segmentationService.removeSegment(segmentationId, segmentIndex);
|
||||
};
|
||||
|
||||
// segment hide
|
||||
const onToggleSegmentVisibility = (segmentationId, segmentIndex) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const segmentInfo = segmentation.segments[segmentIndex];
|
||||
@@ -176,8 +237,28 @@ export default function PanelSegmentation({
|
||||
});
|
||||
};
|
||||
|
||||
const onToggleSegmentLock = (segmentationId, segmentIndex) => {
|
||||
segmentationService.toggleSegmentLocked(segmentationId, segmentIndex);
|
||||
};
|
||||
|
||||
const onToggleSegmentationVisibility = segmentationId => {
|
||||
segmentationService.toggleSegmentationVisibility(segmentationId);
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
const isVisible = segmentation.isVisible;
|
||||
const segments = segmentation.segments;
|
||||
|
||||
const toolGroupIds = getToolGroupIds(segmentationId);
|
||||
|
||||
toolGroupIds.forEach(toolGroupId => {
|
||||
segments.forEach((segment, segmentIndex) => {
|
||||
segmentationService.setSegmentVisibility(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
isVisible,
|
||||
toolGroupId
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const _setSegmentationConfiguration = useCallback(
|
||||
@@ -190,80 +271,100 @@ export default function PanelSegmentation({
|
||||
[segmentationService]
|
||||
);
|
||||
|
||||
const onSegmentationDownload = segmentationId => {
|
||||
commandsManager.runCommand('downloadSegmentation', {
|
||||
segmentationId,
|
||||
});
|
||||
};
|
||||
|
||||
const storeSegmentation = async segmentationId => {
|
||||
const datasources = extensionManager.getActiveDataSource();
|
||||
|
||||
const displaySetInstanceUIDs = await createReportAsync({
|
||||
servicesManager,
|
||||
getReport: () =>
|
||||
commandsManager.runCommand('storeSegmentation', {
|
||||
segmentationId,
|
||||
dataSource: datasources[0],
|
||||
}),
|
||||
reportType: 'Segmentation',
|
||||
});
|
||||
|
||||
// Show the exported report in the active viewport as read only (similar to SR)
|
||||
if (displaySetInstanceUIDs) {
|
||||
// clear the segmentation that we exported, similar to the storeMeasurement
|
||||
// where we remove the measurements and prompt again the user if they would like
|
||||
// to re-read the measurements in a SR read only viewport
|
||||
segmentationService.remove(segmentationId);
|
||||
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportId: viewportGridService.getActiveViewportId(),
|
||||
displaySetInstanceUIDs,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSegmentationDownloadRTSS = segmentationId => {
|
||||
commandsManager.runCommand('downloadRTSS', {
|
||||
segmentationId,
|
||||
});
|
||||
};
|
||||
|
||||
const SegmentationGroupTableComponent =
|
||||
components[configuration?.segmentationPanelMode] || SegmentationGroupTable;
|
||||
const allowAddSegment = configuration?.addSegment;
|
||||
const onSegmentationAddWrapper =
|
||||
configuration?.onSegmentationAdd && typeof configuration?.onSegmentationAdd === 'function'
|
||||
? configuration?.onSegmentationAdd
|
||||
: onSegmentationAdd;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-auto min-h-0 justify-between mt-1">
|
||||
{/* show segmentation table */}
|
||||
{segmentations?.length ? (
|
||||
<SegmentationGroupTable
|
||||
title={t('Segmentations')}
|
||||
showAddSegmentation={false}
|
||||
segmentations={segmentations}
|
||||
isMinimized={isMinimized}
|
||||
activeSegmentationId={selectedSegmentationId || ''}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
disableEditing={disableEditing}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
onSegmentDelete={onSegmentDelete}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
|
||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||
setRenderOutline={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderOutline',
|
||||
value
|
||||
)
|
||||
}
|
||||
setOutlineOpacityActive={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'outlineOpacity',
|
||||
value
|
||||
)
|
||||
}
|
||||
setRenderFill={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderFill',
|
||||
value
|
||||
)
|
||||
}
|
||||
setRenderInactiveSegmentations={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderInactiveSegmentations',
|
||||
value
|
||||
)
|
||||
}
|
||||
setOutlineWidthActive={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'outlineWidthActive',
|
||||
value
|
||||
)
|
||||
}
|
||||
setFillAlpha={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'fillAlpha',
|
||||
value
|
||||
)
|
||||
}
|
||||
setFillAlphaInactive={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'fillAlphaInactive',
|
||||
value
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<SegmentationGroupTableComponent
|
||||
title={t('Segmentations')}
|
||||
segmentations={segmentations}
|
||||
disableEditing={configuration.disableEditing}
|
||||
activeSegmentationId={selectedSegmentationId || ''}
|
||||
onSegmentationAdd={onSegmentationAddWrapper}
|
||||
addSegmentationClassName={addSegmentationClassName}
|
||||
showAddSegment={allowAddSegment}
|
||||
onSegmentationClick={onSegmentationClick}
|
||||
onSegmentationDelete={onSegmentationDelete}
|
||||
onSegmentationDownload={onSegmentationDownload}
|
||||
onSegmentationDownloadRTSS={onSegmentationDownloadRTSS}
|
||||
storeSegmentation={storeSegmentation}
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
onSegmentAdd={onSegmentAdd}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
onSegmentDelete={onSegmentDelete}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
onToggleSegmentLock={onToggleSegmentLock}
|
||||
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
|
||||
showDeleteSegment={true}
|
||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||
setRenderOutline={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
||||
}
|
||||
setOutlineOpacityActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
||||
}
|
||||
setRenderFill={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||
}
|
||||
setRenderInactiveSegmentations={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderInactiveSegmentations', value)
|
||||
}
|
||||
setOutlineWidthActive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||
}
|
||||
setFillAlpha={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
||||
}
|
||||
setFillAlphaInactive={value =>
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -271,7 +372,6 @@ PanelSegmentation.propTypes = {
|
||||
commandsManager: PropTypes.shape({
|
||||
runCommand: PropTypes.func.isRequired,
|
||||
}),
|
||||
appConfig: PropTypes.object.isRequired,
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
segmentationService: PropTypes.shape({
|
||||
|
||||
@@ -39,7 +39,7 @@ function callInputDialog(uiDialogService, label, callback) {
|
||||
label="Enter the segment label"
|
||||
labelClassName="text-white text-[14px] leading-[1.2]"
|
||||
autoFocus
|
||||
className="bg-black border-primary-main"
|
||||
className="border-primary-main bg-black"
|
||||
type="text"
|
||||
value={value.label}
|
||||
onChange={event => {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.chrome-picker {
|
||||
background: #090c29 !important;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { Dialog } from '@ohif/ui';
|
||||
import { ChromePicker } from 'react-color';
|
||||
|
||||
import './colorPickerDialog.css';
|
||||
|
||||
function callColorPickerDialog(uiDialogService, rgbaColor, callback) {
|
||||
const dialogId = 'pick-color';
|
||||
|
||||
const onSubmitHandler = ({ action, value }) => {
|
||||
switch (action.id) {
|
||||
case 'save':
|
||||
callback(value.rgbaColor, action.id);
|
||||
break;
|
||||
case 'cancel':
|
||||
callback('', action.id);
|
||||
break;
|
||||
}
|
||||
uiDialogService.dismiss({ id: dialogId });
|
||||
};
|
||||
|
||||
if (uiDialogService) {
|
||||
uiDialogService.create({
|
||||
id: dialogId,
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
title: 'Segment Color',
|
||||
value: { rgbaColor },
|
||||
noCloseButton: true,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
||||
{ id: 'save', text: 'Save', type: 'secondary' },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
const handleChange = color => {
|
||||
setValue({ rgbaColor: color.rgb });
|
||||
};
|
||||
|
||||
return (
|
||||
<ChromePicker
|
||||
color={value.rgbaColor}
|
||||
onChange={handleChange}
|
||||
presetColors={[]}
|
||||
width={300}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default callColorPickerDialog;
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum SegmentationPanelMode {
|
||||
Expanded = 'expanded',
|
||||
Dropdown = 'dropdown',
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
async function _hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
}) {
|
||||
const {
|
||||
segmentationService,
|
||||
hangingProtocolService,
|
||||
viewportGridService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const displaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
|
||||
|
||||
let segmentationId = null;
|
||||
|
||||
// We need the hydration to notify panels about the new segmentation added
|
||||
const suppressEvents = false;
|
||||
|
||||
segmentationId = await segmentationService.createSegmentationForSEGDisplaySet(
|
||||
segDisplaySet,
|
||||
segmentationId,
|
||||
suppressEvents
|
||||
);
|
||||
|
||||
segmentationService.hydrateSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportIndex,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Todo: fix this after we have a better way for stack viewport segmentations
|
||||
|
||||
// check every viewport in the viewports to see if the displaySetInstanceUID
|
||||
// is being displayed, if so we need to update the viewport to use volume viewport
|
||||
// (if already is not using it) since Cornerstone3D currently only supports
|
||||
// volume viewport for segmentation
|
||||
viewports.forEach((viewport, index) => {
|
||||
if (index === viewportIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
||||
viewport.displaySetInstanceUIDs,
|
||||
segDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
updatedViewports.push({
|
||||
viewportIndex: index,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Do the entire update at once
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default _hydrateSEGDisplaySet;
|
||||
@@ -6,9 +6,7 @@ import dcmjs from 'dcmjs';
|
||||
* @returns The RGB color as an array of three integers between 0 and 255.
|
||||
*/
|
||||
function dicomlabToRGB(cielab: number[]): number[] {
|
||||
const rgb = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
|
||||
Math.round(x * 255)
|
||||
);
|
||||
const rgb = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x => Math.round(x * 255));
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { Enums, cache } from '@cornerstonejs/core';
|
||||
|
||||
/**
|
||||
* Updates the viewports in preparation for rendering segmentations.
|
||||
* Evaluates each viewport to determine which need modifications,
|
||||
* then for those viewports, changes them to a volume type and ensures
|
||||
* they are ready for segmentation rendering.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - ID of the viewport to be updated.
|
||||
* @param params.loadFn - Function to load the segmentation data.
|
||||
* @param params.servicesManager - The services manager.
|
||||
* @param params.displaySet - the display set.
|
||||
*
|
||||
* @returns Returns true upon successful update of viewports for segmentation rendering.
|
||||
*/
|
||||
async function updateViewportsForSegmentationRendering({
|
||||
viewportId,
|
||||
loadFn,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
}: {
|
||||
viewportId: string;
|
||||
loadFn: () => Promise<string>;
|
||||
servicesManager: AppTypes.ServicesManager;
|
||||
displaySet?: any;
|
||||
}) {
|
||||
const { cornerstoneViewportService, segmentationService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
||||
|
||||
const referencedDisplaySetInstanceUID =
|
||||
displaySet?.referencedDisplaySetInstanceUID || viewport?.displaySetInstanceUIDs[0];
|
||||
|
||||
const updatedViewports = getUpdatedViewportsForSegmentation({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
});
|
||||
|
||||
// create Segmentation callback which needs to be waited until
|
||||
// the volume is created (if coming from stack)
|
||||
const createSegmentationForVolume = async () => {
|
||||
const segmentationId = await loadFn();
|
||||
segmentationService.hydrateSegmentation(segmentationId);
|
||||
};
|
||||
|
||||
// the reference volume that is used to draw the segmentation. so check if the
|
||||
// volume exists in the cache (the target Viewport is already a volume viewport)
|
||||
const volumeExists = Array.from(cache._volumeCache.keys()).some(volumeId =>
|
||||
volumeId.includes(referencedDisplaySetInstanceUID)
|
||||
);
|
||||
|
||||
updatedViewports.forEach(async viewport => {
|
||||
viewport.viewportOptions = {
|
||||
...viewport.viewportOptions,
|
||||
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
|
||||
needsRerendering: true,
|
||||
};
|
||||
const viewportId = viewport.viewportId;
|
||||
|
||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
const prevCamera = csViewport.getCamera();
|
||||
|
||||
// only run the createSegmentationForVolume for the targetViewportId
|
||||
// since the rest will get handled by cornerstoneViewportService
|
||||
if ((volumeExists || displaySet.Modality === 'RTSTRUCT') && viewportId === targetViewportId) {
|
||||
await createSegmentationForVolume();
|
||||
return;
|
||||
}
|
||||
|
||||
const createNewSegmentationWhenVolumeMounts = async evt => {
|
||||
const isTheActiveViewportVolumeMounted = evt.detail.volumeActors?.find(ac =>
|
||||
ac.uid.includes(referencedDisplaySetInstanceUID)
|
||||
);
|
||||
|
||||
// Note: make sure to re-grab the viewport since it might have changed
|
||||
// during the time it took for the volume to be mounted, for instance
|
||||
// the stack viewport has been changed to a volume viewport
|
||||
const volumeViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
volumeViewport.setCamera(prevCamera);
|
||||
|
||||
volumeViewport.element.removeEventListener(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
createNewSegmentationWhenVolumeMounts
|
||||
);
|
||||
|
||||
if (!isTheActiveViewportVolumeMounted) {
|
||||
// it means it is one of those other updated viewports so just update the camera
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewportId === targetViewportId) {
|
||||
await createSegmentationForVolume();
|
||||
}
|
||||
};
|
||||
|
||||
csViewport.element.addEventListener(
|
||||
Enums.Events.VOLUME_VIEWPORT_NEW_VOLUME,
|
||||
createNewSegmentationWhenVolumeMounts
|
||||
);
|
||||
});
|
||||
|
||||
// Set the displaySets for the viewports that require to be updated
|
||||
viewportGridService.setDisplaySetsForViewports(updatedViewports);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const getTargetViewport = ({ viewportId, viewportGridService }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
const targetViewportId = viewportId || activeViewportId;
|
||||
|
||||
const viewport = viewports.get(targetViewportId);
|
||||
|
||||
return viewport;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a list of viewports that require updates in preparation for segmentation rendering.
|
||||
* This function evaluates viewports based on their compatibility with the provided segmentation's
|
||||
* frame of reference UID and appends them to the updated list if they should render the segmentation.
|
||||
*
|
||||
* @param {Object} params - Parameters for the function.
|
||||
* @param params.viewportId - the ID of the viewport to be updated.
|
||||
* @param params.servicesManager - The services manager
|
||||
* @param params.displaySet - the display set.
|
||||
*
|
||||
* @returns {Array} Returns an array of viewports that require updates for segmentation rendering.
|
||||
*/
|
||||
function getUpdatedViewportsForSegmentation({
|
||||
viewportId,
|
||||
servicesManager,
|
||||
displaySet,
|
||||
}: withAppTypes) {
|
||||
const { hangingProtocolService, displaySetService, segmentationService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
const { viewports, isHangingProtocolLayout } = viewportGridService.getState();
|
||||
|
||||
const viewport = getTargetViewport({ viewportId, viewportGridService });
|
||||
const targetViewportId = viewport.viewportOptions.viewportId;
|
||||
|
||||
const displaySetInstanceUIDs = viewports.get(targetViewportId).displaySetInstanceUIDs;
|
||||
|
||||
const referenceDisplaySetInstanceUID =
|
||||
displaySet?.referencedDisplaySetInstanceUID || displaySetInstanceUIDs[0];
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(referenceDisplaySetInstanceUID);
|
||||
const segmentationFrameOfReferenceUID = referencedDisplaySet.instances[0].FrameOfReferenceUID;
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
targetViewportId,
|
||||
referenceDisplaySetInstanceUID,
|
||||
isHangingProtocolLayout
|
||||
);
|
||||
|
||||
viewports.forEach((viewport, viewportId) => {
|
||||
if (
|
||||
targetViewportId === viewportId ||
|
||||
updatedViewports.find(v => v.viewportId === viewportId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldDisplaySeg = segmentationService.shouldRenderSegmentation(
|
||||
viewport.displaySetInstanceUIDs,
|
||||
segmentationFrameOfReferenceUID
|
||||
);
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
updatedViewports.push({
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
viewportType: displaySet?.Modality === 'RTSTRUCT' ? 'stack' : 'volume',
|
||||
needsRerendering: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return updatedViewports.filter(v => v.viewportOptions?.viewportType !== 'volume3d');
|
||||
}
|
||||
|
||||
export {
|
||||
updateViewportsForSegmentationRendering,
|
||||
getUpdatedViewportsForSegmentation,
|
||||
getTargetViewport,
|
||||
};
|
||||
@@ -1,12 +1,7 @@
|
||||
function createSEGToolGroupAndAddTools(
|
||||
ToolGroupService,
|
||||
customizationService,
|
||||
toolGroupId
|
||||
) {
|
||||
const { tools } =
|
||||
customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
function createSEGToolGroupAndAddTools(ToolGroupService, customizationService, toolGroupId) {
|
||||
const { tools } = customizationService.get('cornerstone.overlayViewportTools') ?? {};
|
||||
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools, {});
|
||||
return ToolGroupService.createToolGroupAndAddTools(toolGroupId, tools);
|
||||
}
|
||||
|
||||
export default createSEGToolGroupAndAddTools;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ButtonEnums } from '@ohif/ui';
|
||||
import hydrateSEGDisplaySet from './_hydrateSEG';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
@@ -10,16 +9,14 @@ const RESPONSE = {
|
||||
function promptHydrateSEG({
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
preHydrateCallbacks,
|
||||
}) {
|
||||
hydrateSEGDisplaySet,
|
||||
}: withAppTypes) {
|
||||
const { uiViewportDialogService } = servicesManager.services;
|
||||
|
||||
return new Promise(async function(resolve, reject) {
|
||||
const promptResult = await _askHydrate(
|
||||
uiViewportDialogService,
|
||||
viewportIndex
|
||||
);
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const promptResult = await _askHydrate(uiViewportDialogService, viewportId);
|
||||
|
||||
if (promptResult === RESPONSE.HYDRATE_SEG) {
|
||||
preHydrateCallbacks?.forEach(callback => {
|
||||
@@ -28,8 +25,7 @@ function promptHydrateSEG({
|
||||
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
resolve(isHydrated);
|
||||
@@ -37,8 +33,8 @@ function promptHydrateSEG({
|
||||
});
|
||||
}
|
||||
|
||||
function _askHydrate(uiViewportDialogService, viewportIndex) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
function _askHydrate(uiViewportDialogService, viewportId) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const message = 'Do you want to open this Segmentation?';
|
||||
const actions = [
|
||||
{
|
||||
@@ -58,7 +54,7 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
@@ -67,6 +63,11 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
onSubmit(RESPONSE.HYDRATE_SEG);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,43 +1,35 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils } from '@ohif/core';
|
||||
import {
|
||||
LoadingIndicatorTotalPercent,
|
||||
useViewportGrid,
|
||||
ViewportActionBar,
|
||||
} from '@ohif/ui';
|
||||
import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
import _getStatusComponent from './_getStatusComponent';
|
||||
|
||||
const { formatDate } = utils;
|
||||
const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup';
|
||||
|
||||
function OHIFCornerstoneSEGViewport(props) {
|
||||
function OHIFCornerstoneSEGViewport(props: withAppTypes) {
|
||||
const {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
viewportIndex,
|
||||
viewportLabel,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('SEGViewport');
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
toolGroupService,
|
||||
segmentationService,
|
||||
uiNotificationService,
|
||||
customizationService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportIndex}`;
|
||||
const toolGroupId = `${SEG_TOOLGROUP_BASE_NAME}-${viewportId}`;
|
||||
|
||||
// SEG viewport will always have a single display set
|
||||
if (displaySets.length > 1) {
|
||||
@@ -67,7 +59,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
// refs
|
||||
const referencedDisplaySetRef = useRef(null);
|
||||
|
||||
const { viewports, activeViewportIndex } = viewportGrid;
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||
@@ -94,9 +86,9 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
};
|
||||
|
||||
const storePresentationState = useCallback(() => {
|
||||
viewportGrid?.viewports.forEach(({ viewportIndex }) => {
|
||||
viewportGrid?.viewports.forEach(({ viewportId }) => {
|
||||
commandsManager.runCommand('storePresentation', {
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
});
|
||||
});
|
||||
}, [viewportGrid]);
|
||||
@@ -106,9 +98,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
const {
|
||||
displaySet: referencedDisplaySet,
|
||||
} = referencedDisplaySetRef.current;
|
||||
const { displaySet: referencedDisplaySet } = referencedDisplaySetRef.current;
|
||||
|
||||
// Todo: jump to the center of the first segment
|
||||
return (
|
||||
@@ -121,16 +111,17 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
orientation: viewportOptions.orientation,
|
||||
viewportId: viewportOptions.viewportId,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
onElementDisabled={onElementDisabled}
|
||||
// initialImageIndex={initialImageIndex}
|
||||
></Component>
|
||||
);
|
||||
}, [viewportIndex, segDisplaySet, toolGroupId]);
|
||||
}, [viewportId, segDisplaySet, toolGroupId]);
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
direction = direction === 'left' ? -1 : 1;
|
||||
const segmentationId = segDisplaySet.displaySetInstanceUID;
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
@@ -148,11 +139,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
segmentationService.jumpToSegmentCenter(
|
||||
segmentationId,
|
||||
newSelectedSegmentIndex,
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment]
|
||||
@@ -165,35 +152,24 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
promptHydrateSEG({
|
||||
servicesManager,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
segDisplaySet,
|
||||
preHydrateCallbacks: [storePresentationState],
|
||||
hydrateSEGDisplaySet,
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
});
|
||||
}, [servicesManager, viewportIndex, segDisplaySet, segIsLoading]);
|
||||
}, [servicesManager, viewportId, segDisplaySet, segIsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = segmentationService.subscribe(
|
||||
segmentationService.EVENTS.SEGMENTATION_LOADING_COMPLETE,
|
||||
evt => {
|
||||
if (
|
||||
evt.segDisplaySet.displaySetInstanceUID ===
|
||||
segDisplaySet.displaySetInstanceUID
|
||||
) {
|
||||
if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) {
|
||||
setSegIsLoading(false);
|
||||
}
|
||||
|
||||
if (evt.overlappingSegments) {
|
||||
uiNotificationService.show({
|
||||
title: 'Overlapping Segments',
|
||||
message:
|
||||
'Overlapping segments detected which is not currently supported',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -225,12 +201,10 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||
({ displaySetInstanceUIDs }) => {
|
||||
const activeViewport = viewports[activeViewportIndex];
|
||||
if (
|
||||
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
|
||||
) {
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: activeViewportIndex,
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUIDs: [],
|
||||
});
|
||||
}
|
||||
@@ -251,17 +225,11 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
// This creates a custom tool group which has the lifetime of this view
|
||||
// only, and does NOT interfere with currently displayed segmentations.
|
||||
toolGroup = createSEGToolGroupAndAddTools(
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
toolGroupId
|
||||
);
|
||||
toolGroup = createSEGToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId);
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
|
||||
// Only destroy the viewport specific implementation
|
||||
toolGroupService.destroyToolGroup(toolGroupId);
|
||||
@@ -273,13 +241,74 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
referencedDisplaySetRef.current = null;
|
||||
};
|
||||
}, [segDisplaySet]);
|
||||
|
||||
const hydrateSEGDisplaySet = useCallback(
|
||||
({ segDisplaySet, viewportId }) => {
|
||||
commandsManager.runCommand('loadSegmentationDisplaySetsForViewport', {
|
||||
displaySets: [segDisplaySet],
|
||||
viewportId,
|
||||
});
|
||||
},
|
||||
[commandsManager]
|
||||
);
|
||||
|
||||
const onStatusClick = useCallback(async () => {
|
||||
// Before hydrating a SEG and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the SEG. This is
|
||||
// required if the user has changed the viewport (other viewport than SEG viewport)
|
||||
// presentation state (w/l and invert) and then opens the SEG. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
}, [hydrateSEGDisplaySet, segDisplaySet, storePresentationState, viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
viewportActionCornersService.setComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onSegmentChange}
|
||||
className={
|
||||
viewportId === activeViewportId ? 'visible' : 'invisible group-hover:visible'
|
||||
}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
activeViewportId,
|
||||
isHydrated,
|
||||
onSegmentChange,
|
||||
onStatusClick,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
|
||||
@@ -296,7 +325,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
@@ -315,65 +344,12 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
SpacingBetweenSlices,
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
const onStatusClick = async () => {
|
||||
// Before hydrating a SEG and make it added to all viewports in the grid
|
||||
// that share the same frameOfReferenceUID, we need to store the viewport grid
|
||||
// presentation state, so that we can restore it after hydrating the SEG. This is
|
||||
// required if the user has changed the viewport (other viewport than SEG viewport)
|
||||
// presentation state (w/l and invert) and then opens the SEG. If we don't store
|
||||
// the presentation state, the viewport will be reset to the default presentation
|
||||
storePresentationState();
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
onDoubleClick={evt => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}}
|
||||
onArrowsClick={onSegmentChange}
|
||||
getStatusComponent={() => {
|
||||
return _getStatusComponent({
|
||||
isHydrated,
|
||||
onStatusClick,
|
||||
});
|
||||
}}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
seriesDescription: `SEG Viewport ${SeriesDescription}`,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
: '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness
|
||||
? utils.roundNumber(SliceThickness, 2)
|
||||
: '',
|
||||
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined
|
||||
? utils.roundNumber(SpacingBetweenSlices, 2)
|
||||
: '',
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{segIsLoading && (
|
||||
<LoadingIndicatorTotalPercent
|
||||
className="w-full h-full"
|
||||
className="h-full w-full"
|
||||
totalNumbers={processingProgress.totalSegments}
|
||||
percentComplete={processingProgress.percentComplete}
|
||||
loadingText="Loading SEG..."
|
||||
@@ -388,7 +364,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
OHIFCornerstoneSEGViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object,
|
||||
@@ -399,12 +375,18 @@ OHIFCornerstoneSEGViewport.defaultProps = {
|
||||
};
|
||||
|
||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
|
||||
const {
|
||||
SharedFunctionalGroupsSequence: [SharedFunctionalGroup],
|
||||
} = segDisplaySet.instance;
|
||||
const {
|
||||
PixelMeasuresSequence: [PixelMeasures],
|
||||
} = SharedFunctionalGroup;
|
||||
const { SharedFunctionalGroupsSequence } = segDisplaySet.instance;
|
||||
|
||||
const SharedFunctionalGroup = Array.isArray(SharedFunctionalGroupsSequence)
|
||||
? SharedFunctionalGroupsSequence[0]
|
||||
: SharedFunctionalGroupsSequence;
|
||||
|
||||
const { PixelMeasuresSequence } = SharedFunctionalGroup;
|
||||
|
||||
const PixelMeasures = Array.isArray(PixelMeasuresSequence)
|
||||
? PixelMeasuresSequence[0]
|
||||
: PixelMeasuresSequence;
|
||||
|
||||
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
|
||||
|
||||
const image0 = referencedDisplaySet.images[0];
|
||||
|
||||
@@ -6,45 +6,53 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
switch (isHydrated) {
|
||||
case true:
|
||||
StatusIcon = () => <Icon name="status-alert" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>This Segmentation is loaded in the segmentation panel</div>
|
||||
);
|
||||
ToolTipMessage = () => <div>This Segmentation is loaded in the segmentation panel</div>;
|
||||
break;
|
||||
case false:
|
||||
StatusIcon = () => <Icon name="status-untracked" />;
|
||||
StatusIcon = () => (
|
||||
<Icon
|
||||
className="text-aqua-pale"
|
||||
name="status-untracked"
|
||||
/>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => <div>Click LOAD to load segmentation.</div>;
|
||||
}
|
||||
|
||||
const StatusArea = () => (
|
||||
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
|
||||
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SEG</span>
|
||||
</div>
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
const StatusArea = () => {
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
return (
|
||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SEG</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{!isHydrated && (
|
||||
<div
|
||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={onStatusClick}
|
||||
>
|
||||
{loadStr}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<Tooltip
|
||||
content={<ToolTipMessage />}
|
||||
position="bottom-left"
|
||||
>
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -41,13 +41,7 @@ module.exports = (env, argv) => {
|
||||
libraryTarget: 'umd',
|
||||
filename: pkg.main,
|
||||
},
|
||||
externals: [
|
||||
/\b(vtk.js)/,
|
||||
/\b(dcmjs)/,
|
||||
/\b(gl-matrix)/,
|
||||
/^@ohif/,
|
||||
/^@cornerstonejs/,
|
||||
],
|
||||
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||
"version": "3.7.0-beta.54",
|
||||
"version": "3.8.2",
|
||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -23,6 +23,8 @@
|
||||
"ohif-extension"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"dev:cornerstone": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
@@ -32,11 +34,11 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.7.0-beta.54",
|
||||
"@ohif/extension-cornerstone": "3.7.0-beta.54",
|
||||
"@ohif/extension-measurement-tracking": "3.7.0-beta.54",
|
||||
"@ohif/ui": "3.7.0-beta.54",
|
||||
"dcmjs": "^0.29.5",
|
||||
"@ohif/core": "3.8.2\n",
|
||||
"@ohif/extension-cornerstone": "3.8.2\n",
|
||||
"@ohif/extension-measurement-tracking": "3.8.2\n",
|
||||
"@ohif/ui": "3.8.2\n",
|
||||
"dcmjs": "^0.29.12",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -44,9 +46,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.9.3",
|
||||
"@cornerstonejs/core": "^1.9.3",
|
||||
"@cornerstonejs/tools": "^1.9.3",
|
||||
"@cornerstonejs/adapters": "^1.77.6",
|
||||
"@cornerstonejs/core": "^1.77.6",
|
||||
"@cornerstonejs/tools": "^1.77.6",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
+21
-40
@@ -17,11 +17,7 @@ const { log } = OHIF;
|
||||
* @param options Naturalized DICOM JSON headers to merge into the displaySet.
|
||||
*
|
||||
*/
|
||||
const _generateReport = (
|
||||
measurementData,
|
||||
additionalFindingTypes,
|
||||
options = {}
|
||||
) => {
|
||||
const _generateReport = (measurementData, additionalFindingTypes, options = {}) => {
|
||||
const filteredToolState = getFilteredCornerstoneToolState(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
@@ -44,7 +40,9 @@ const _generateReport = (
|
||||
return dataset;
|
||||
};
|
||||
|
||||
const commandsModule = ({}) => {
|
||||
const commandsModule = (props: withAppTypes) => {
|
||||
const { servicesManager } = props;
|
||||
const { customizationService } = servicesManager.services;
|
||||
const actions = {
|
||||
/**
|
||||
*
|
||||
@@ -54,20 +52,12 @@ const commandsModule = ({}) => {
|
||||
* as opposed to Finding Sites.
|
||||
* that you wish to serialize.
|
||||
*/
|
||||
downloadReport: ({
|
||||
measurementData,
|
||||
additionalFindingTypes,
|
||||
options = {},
|
||||
}) => {
|
||||
const srDataset = actions.generateReport(
|
||||
measurementData,
|
||||
additionalFindingTypes,
|
||||
options
|
||||
);
|
||||
downloadReport: ({ measurementData, additionalFindingTypes, options = {} }) => {
|
||||
const srDataset = actions.generateReport(measurementData, additionalFindingTypes, options);
|
||||
const reportBlob = dcmjs.data.datasetToBlob(srDataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
var objectUrl = URL.createObjectURL(reportBlob);
|
||||
const objectUrl = URL.createObjectURL(reportBlob);
|
||||
window.location.assign(objectUrl);
|
||||
},
|
||||
|
||||
@@ -91,32 +81,31 @@ const commandsModule = ({}) => {
|
||||
log.info('[DICOMSR] storeMeasurements');
|
||||
|
||||
if (!dataSource || !dataSource.store || !dataSource.store.dicom) {
|
||||
log.error(
|
||||
'[DICOMSR] datasource has no dataSource.store.dicom endpoint!'
|
||||
);
|
||||
log.error('[DICOMSR] datasource has no dataSource.store.dicom endpoint!');
|
||||
return Promise.reject({});
|
||||
}
|
||||
|
||||
try {
|
||||
const naturalizedReport = _generateReport(
|
||||
measurementData,
|
||||
additionalFindingTypes,
|
||||
options
|
||||
);
|
||||
const naturalizedReport = _generateReport(measurementData, additionalFindingTypes, options);
|
||||
|
||||
const { StudyInstanceUID, ContentSequence } = naturalizedReport;
|
||||
// The content sequence has 5 or more elements, of which
|
||||
// the `[4]` element contains the annotation data, so this is
|
||||
// checking that there is some annotation data present.
|
||||
if (!ContentSequence?.[4].ContentSequence?.length) {
|
||||
console.log(
|
||||
'naturalizedReport missing imaging content',
|
||||
naturalizedReport
|
||||
);
|
||||
console.log('naturalizedReport missing imaging content', naturalizedReport);
|
||||
throw new Error('Invalid report, no content');
|
||||
}
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport);
|
||||
const onBeforeDicomStore =
|
||||
customizationService.getModeCustomization('onBeforeDicomStore')?.value;
|
||||
|
||||
let dicomDict;
|
||||
if (typeof onBeforeDicomStore === 'function') {
|
||||
dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport });
|
||||
}
|
||||
|
||||
await dataSource.store.dicom(naturalizedReport, null, dicomDict);
|
||||
|
||||
if (StudyInstanceUID) {
|
||||
dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||
@@ -130,12 +119,8 @@ const commandsModule = ({}) => {
|
||||
return naturalizedReport;
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
log.error(
|
||||
`[DICOMSR] Error while saving the measurements: ${error.message}`
|
||||
);
|
||||
throw new Error(
|
||||
error.message || 'Error while saving the measurements.'
|
||||
);
|
||||
log.error(`[DICOMSR] Error while saving the measurements: ${error.message}`);
|
||||
throw new Error(error.message || 'Error while saving the measurements.');
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -143,13 +128,9 @@ const commandsModule = ({}) => {
|
||||
const definitions = {
|
||||
downloadReport: {
|
||||
commandFn: actions.downloadReport,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
storeMeasurements: {
|
||||
commandFn: actions.storeMeasurements,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
||||
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
|
||||
import addMeasurement from './utils/addMeasurement';
|
||||
import addDICOMSRDisplayAnnotation from './utils/addDICOMSRDisplayAnnotation';
|
||||
import isRehydratable from './utils/isRehydratable';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
@@ -29,9 +29,7 @@ const validateSameStudyUID = (uid: string, instances): void => {
|
||||
instances.forEach(it => {
|
||||
if (it.StudyInstanceUID !== uid) {
|
||||
console.warn('Not all instances have the same UID', uid, it);
|
||||
throw new Error(
|
||||
`Instances ${it.SOPInstanceUID} does not belong to ${uid}`
|
||||
);
|
||||
throw new Error(`Instances ${it.SOPInstanceUID} does not belong to ${uid}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -51,10 +49,7 @@ const CodeNameCodeSequenceValues = {
|
||||
|
||||
const CodingSchemeDesignators = {
|
||||
SRT: 'SRT',
|
||||
CornerstoneCodeSchemes: [
|
||||
Cornerstone3DCodeScheme.CodingSchemeDesignator,
|
||||
'CST4',
|
||||
],
|
||||
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
|
||||
};
|
||||
|
||||
const RELATIONSHIP_TYPE = {
|
||||
@@ -71,10 +66,7 @@ const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
||||
* @param instances is a list of instances from THIS series that are not
|
||||
* in this DICOM SR Display Set already.
|
||||
*/
|
||||
function addInstances(
|
||||
instances: InstanceMetadata[],
|
||||
displaySetService: DisplaySetService
|
||||
) {
|
||||
function addInstances(instances: InstanceMetadata[], displaySetService: DisplaySetService) {
|
||||
this.instances.push(...instances);
|
||||
utils.sortStudyInstances(this.instances);
|
||||
// The last instance is the newest one, so is the one most interesting.
|
||||
@@ -95,7 +87,7 @@ function addInstances(
|
||||
*/
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
servicesManager: AppTypes.ServicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
// If the series has no instances, stop here
|
||||
@@ -123,8 +115,7 @@ function _getDisplaySetsFromSeries(
|
||||
|
||||
if (
|
||||
!ConceptNameCodeSequence ||
|
||||
ConceptNameCodeSequence.CodeValue !==
|
||||
CodeNameCodeSequenceValues.ImagingMeasurementReport
|
||||
ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.ImagingMeasurementReport
|
||||
) {
|
||||
servicesManager.services.uiNotificationService.show({
|
||||
title: 'DICOM SR',
|
||||
@@ -163,13 +154,37 @@ function _getDisplaySetsFromSeries(
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
function _load(displaySet, servicesManager, extensionManager) {
|
||||
async function _load(displaySet, servicesManager: AppTypes.ServicesManager, extensionManager) {
|
||||
const { displaySetService, measurementService } = servicesManager.services;
|
||||
const dataSources = extensionManager.getDataSources();
|
||||
const dataSource = dataSources[0];
|
||||
|
||||
const { ContentSequence } = displaySet.instance;
|
||||
|
||||
async function retrieveBulkData(obj, parentObj = null, key = null) {
|
||||
for (const prop in obj) {
|
||||
if (typeof obj[prop] === 'object' && obj[prop] !== null) {
|
||||
await retrieveBulkData(obj[prop], obj, prop);
|
||||
} else if (Array.isArray(obj[prop])) {
|
||||
await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop)));
|
||||
} else if (prop === 'BulkDataURI') {
|
||||
const value = await dataSource.retrieve.bulkDataURI({
|
||||
BulkDataURI: obj[prop],
|
||||
StudyInstanceUID: displaySet.instance.StudyInstanceUID,
|
||||
SeriesInstanceUID: displaySet.instance.SeriesInstanceUID,
|
||||
SOPInstanceUID: displaySet.instance.SOPInstanceUID,
|
||||
});
|
||||
if (parentObj && key) {
|
||||
parentObj[key] = new Float32Array(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (displaySet.isLoaded !== true) {
|
||||
await retrieveBulkData(ContentSequence);
|
||||
}
|
||||
|
||||
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
|
||||
displaySet.measurements = _getMeasurements(ContentSequence);
|
||||
|
||||
@@ -187,33 +202,34 @@ function _load(displaySet, servicesManager, extensionManager) {
|
||||
_checkIfCanAddMeasurementsToDisplaySet(
|
||||
displaySet,
|
||||
activeDisplaySet,
|
||||
dataSource
|
||||
dataSource,
|
||||
servicesManager
|
||||
);
|
||||
});
|
||||
|
||||
// Subscribe to new displaySets as the source may come in after.
|
||||
displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
data => {
|
||||
const { displaySetsAdded } = data;
|
||||
// If there are still some measurements that have not yet been loaded into cornerstone,
|
||||
// See if we can load them onto any of the new displaySets.
|
||||
displaySetsAdded.forEach(newDisplaySet => {
|
||||
_checkIfCanAddMeasurementsToDisplaySet(
|
||||
displaySet,
|
||||
newDisplaySet,
|
||||
dataSource
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => {
|
||||
const { displaySetsAdded } = data;
|
||||
// If there are still some measurements that have not yet been loaded into cornerstone,
|
||||
// See if we can load them onto any of the new displaySets.
|
||||
displaySetsAdded.forEach(newDisplaySet => {
|
||||
_checkIfCanAddMeasurementsToDisplaySet(
|
||||
displaySet,
|
||||
newDisplaySet,
|
||||
dataSource,
|
||||
servicesManager
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
srDisplaySet,
|
||||
newDisplaySet,
|
||||
dataSource
|
||||
dataSource,
|
||||
servicesManager: AppTypes.ServicesManager
|
||||
) {
|
||||
const { customizationService } = servicesManager.services;
|
||||
let unloadedMeasurements = srDisplaySet.measurements.filter(
|
||||
measurement => measurement.loaded === false
|
||||
);
|
||||
@@ -223,12 +239,16 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newDisplaySet instanceof ImageSet) {
|
||||
if ((!newDisplaySet) instanceof ImageSet) {
|
||||
// This also filters out _this_ displaySet, as it is not an ImageSet.
|
||||
return;
|
||||
}
|
||||
|
||||
const { sopClassUids, images } = newDisplaySet;
|
||||
if (newDisplaySet.unsupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sopClassUids } = newDisplaySet;
|
||||
|
||||
// Check if any have the newDisplaySet is the correct SOPClass.
|
||||
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
|
||||
@@ -248,8 +268,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
const { coords } = measurement;
|
||||
|
||||
coords.forEach(coord => {
|
||||
const SOPInstanceUID =
|
||||
coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
|
||||
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
||||
SOPInstanceUIDs.push(SOPInstanceUID);
|
||||
@@ -257,9 +276,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
});
|
||||
});
|
||||
|
||||
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(
|
||||
newDisplaySet
|
||||
);
|
||||
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
|
||||
|
||||
for (const imageId of imageIdsForDisplaySet) {
|
||||
if (!unloadedMeasurements.length) {
|
||||
@@ -267,25 +284,41 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
return;
|
||||
}
|
||||
|
||||
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(
|
||||
imageId
|
||||
);
|
||||
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
|
||||
|
||||
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
||||
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||
const measurement = unloadedMeasurements[j];
|
||||
if (
|
||||
_measurementReferencesSOPInstanceUID(
|
||||
let measurement = unloadedMeasurements[j];
|
||||
|
||||
const onBeforeSRAddMeasurement = customizationService.getModeCustomization(
|
||||
'onBeforeSRAddMeasurement'
|
||||
)?.value;
|
||||
|
||||
if (typeof onBeforeSRAddMeasurement === 'function') {
|
||||
measurement = onBeforeSRAddMeasurement({
|
||||
measurement,
|
||||
SOPInstanceUID,
|
||||
frameNumber
|
||||
)
|
||||
) {
|
||||
addMeasurement(
|
||||
measurement,
|
||||
imageId,
|
||||
newDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
StudyInstanceUID: srDisplaySet.StudyInstanceUID,
|
||||
SeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
|
||||
});
|
||||
}
|
||||
|
||||
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
|
||||
const frame =
|
||||
(measurement.coords[0].ReferencedSOPSequence &&
|
||||
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
/** Add DICOMSRDisplay annotation for the SR viewport (only) */
|
||||
addDICOMSRDisplayAnnotation(measurement, imageId, frame);
|
||||
|
||||
/** Update measurement properties */
|
||||
measurement.loaded = true;
|
||||
measurement.imageId = imageId;
|
||||
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
||||
measurement.ReferencedSOPInstanceUID =
|
||||
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
measurement.frameNumber = frame;
|
||||
delete measurement.coords;
|
||||
|
||||
unloadedMeasurements.splice(j, 1);
|
||||
}
|
||||
@@ -294,18 +327,14 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
}
|
||||
}
|
||||
|
||||
function _measurementReferencesSOPInstanceUID(
|
||||
measurement,
|
||||
SOPInstanceUID,
|
||||
frameNumber
|
||||
) {
|
||||
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
|
||||
const { coords } = measurement;
|
||||
|
||||
// NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM
|
||||
// Standard. But for now, we will support only one ReferenceFrameNumber.
|
||||
const ReferencedFrameNumber =
|
||||
(measurement.coords[0].ReferencedSOPSequence &&
|
||||
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
||||
measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
|
||||
@@ -324,11 +353,7 @@ function _measurementReferencesSOPInstanceUID(
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
);
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
return [
|
||||
@@ -343,30 +368,22 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
||||
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.ImagingMeasurements
|
||||
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
|
||||
);
|
||||
|
||||
const MeasurementGroups = _getSequenceAsArray(
|
||||
ImagingMeasurements.ContentSequence
|
||||
).filter(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.MeasurementGroup
|
||||
const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
|
||||
);
|
||||
|
||||
const mergedContentSequencesByTrackingUniqueIdentifiers = _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
||||
MeasurementGroups
|
||||
);
|
||||
const mergedContentSequencesByTrackingUniqueIdentifiers =
|
||||
_getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups);
|
||||
|
||||
const measurements = [];
|
||||
|
||||
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
|
||||
trackingUniqueIdentifier => {
|
||||
const mergedContentSequence =
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
];
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier];
|
||||
|
||||
const measurement = _processMeasurement(mergedContentSequence);
|
||||
|
||||
@@ -379,15 +396,11 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
||||
return measurements;
|
||||
}
|
||||
|
||||
function _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
||||
MeasurementGroups
|
||||
) {
|
||||
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
|
||||
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
|
||||
|
||||
MeasurementGroups.forEach(MeasurementGroup => {
|
||||
const ContentSequence = _getSequenceAsArray(
|
||||
MeasurementGroup.ContentSequence
|
||||
);
|
||||
const ContentSequence = _getSequenceAsArray(MeasurementGroup.ContentSequence);
|
||||
|
||||
const TrackingUniqueIdentifierItem = ContentSequence.find(
|
||||
item =>
|
||||
@@ -396,22 +409,16 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
||||
);
|
||||
|
||||
if (!TrackingUniqueIdentifierItem) {
|
||||
console.warn(
|
||||
'No Tracking Unique Identifier, skipping ambiguous measurement.'
|
||||
);
|
||||
console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.');
|
||||
}
|
||||
|
||||
const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID;
|
||||
|
||||
if (
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
] === undefined
|
||||
) {
|
||||
if (mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] === undefined) {
|
||||
// Add the full ContentSequence
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
] = [...ContentSequence];
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] = [
|
||||
...ContentSequence,
|
||||
];
|
||||
} else {
|
||||
// Add the ContentSequence minus the tracking identifier, as we have this
|
||||
// Information in the merged ContentSequence anyway.
|
||||
@@ -420,9 +427,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
||||
item.ConceptNameCodeSequence.CodeValue !==
|
||||
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
|
||||
) {
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
].push(item);
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier].push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -447,18 +452,12 @@ function _processTID1410Measurement(mergedContentSequence) {
|
||||
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
|
||||
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
|
||||
|
||||
const graphicItem = mergedContentSequence.find(
|
||||
group => group.ValueType === 'SCOORD'
|
||||
);
|
||||
const graphicItem = mergedContentSequence.find(group => group.ValueType === 'SCOORD');
|
||||
|
||||
const UIDREFContentItem = mergedContentSequence.find(
|
||||
group => group.ValueType === 'UIDREF'
|
||||
);
|
||||
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
|
||||
|
||||
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
|
||||
);
|
||||
|
||||
if (!graphicItem) {
|
||||
@@ -468,9 +467,7 @@ function _processTID1410Measurement(mergedContentSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
const NUMContentItems = mergedContentSequence.filter(
|
||||
group => group.ValueType === 'NUM'
|
||||
);
|
||||
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
|
||||
|
||||
const measurement = {
|
||||
loaded: false,
|
||||
@@ -485,10 +482,7 @@ function _processTID1410Measurement(mergedContentSequence) {
|
||||
|
||||
if (MeasuredValueSequence) {
|
||||
measurement.labels.push(
|
||||
_getLabelFromMeasuredValueSequence(
|
||||
ConceptNameCodeSequence,
|
||||
MeasuredValueSequence
|
||||
)
|
||||
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -497,32 +491,22 @@ function _processTID1410Measurement(mergedContentSequence) {
|
||||
}
|
||||
|
||||
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
const NUMContentItems = mergedContentSequence.filter(
|
||||
group => group.ValueType === 'NUM'
|
||||
);
|
||||
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
|
||||
|
||||
const UIDREFContentItem = mergedContentSequence.find(
|
||||
group => group.ValueType === 'UIDREF'
|
||||
);
|
||||
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
|
||||
|
||||
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
|
||||
);
|
||||
|
||||
const finding = mergedContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.Finding
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.Finding
|
||||
);
|
||||
|
||||
const findingSites = mergedContentSequence.filter(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
|
||||
CodingSchemeDesignators.SRT &&
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.FindingSite
|
||||
item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SRT &&
|
||||
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite
|
||||
);
|
||||
|
||||
const measurement = {
|
||||
@@ -538,8 +522,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
finding.ConceptCodeSequence.CodingSchemeDesignator
|
||||
) &&
|
||||
finding.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
) {
|
||||
measurement.labels.push({
|
||||
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||
@@ -554,8 +537,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
||||
) &&
|
||||
FindingSite.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
);
|
||||
|
||||
if (cornerstoneFreeTextFindingSite) {
|
||||
@@ -567,18 +549,12 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
}
|
||||
|
||||
NUMContentItems.forEach(item => {
|
||||
const {
|
||||
ConceptNameCodeSequence,
|
||||
ContentSequence,
|
||||
MeasuredValueSequence,
|
||||
} = item;
|
||||
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
|
||||
|
||||
const { ValueType } = ContentSequence;
|
||||
|
||||
if (!ValueType === 'SCOORD') {
|
||||
console.warn(
|
||||
`Graphic ${ValueType} not currently supported, skipping annotation.`
|
||||
);
|
||||
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -591,10 +567,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
|
||||
if (MeasuredValueSequence) {
|
||||
measurement.labels.push(
|
||||
_getLabelFromMeasuredValueSequence(
|
||||
ConceptNameCodeSequence,
|
||||
MeasuredValueSequence
|
||||
)
|
||||
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -634,17 +607,12 @@ function _getCoordsFromSCOORDOrSCOORD3D(item) {
|
||||
return coords;
|
||||
}
|
||||
|
||||
function _getLabelFromMeasuredValueSequence(
|
||||
ConceptNameCodeSequence,
|
||||
MeasuredValueSequence
|
||||
) {
|
||||
function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) {
|
||||
const { CodeMeaning } = ConceptNameCodeSequence;
|
||||
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
|
||||
const { CodeValue } = MeasurementUnitsCodeSequence;
|
||||
|
||||
const formatedNumericValue = NumericValue
|
||||
? Number(NumericValue).toFixed(2)
|
||||
: '';
|
||||
const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : '';
|
||||
|
||||
return {
|
||||
label: CodeMeaning,
|
||||
@@ -654,17 +622,11 @@ function _getLabelFromMeasuredValueSequence(
|
||||
|
||||
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
||||
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.ImageLibrary
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
|
||||
);
|
||||
|
||||
const ImageLibraryGroup = _getSequenceAsArray(
|
||||
ImageLibrary.ContentSequence
|
||||
).find(
|
||||
item =>
|
||||
item.ConceptNameCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.ImageLibraryGroup
|
||||
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
|
||||
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
|
||||
);
|
||||
|
||||
const referencedImages = [];
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getHangingProtocolModule, {
|
||||
srProtocol,
|
||||
} from './getHangingProtocolModule';
|
||||
import { srProtocol } from './getHangingProtocolModule';
|
||||
import onModeEnter from './onModeEnter';
|
||||
import getCommandsModule from './commandsModule';
|
||||
import preRegistration from './init';
|
||||
@@ -12,9 +10,7 @@ import hydrateStructuredReport from './utils/hydrateStructuredReport';
|
||||
import createReferencedImageDisplaySet from './utils/createReferencedImageDisplaySet';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport'
|
||||
);
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSRViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstoneSRViewport = props => {
|
||||
@@ -58,7 +54,7 @@ const dicomSRExtension = {
|
||||
},
|
||||
getCommandsModule,
|
||||
getSopClassHandlerModule,
|
||||
// Include dynmically computed values such as toolNames not known till instantiation
|
||||
// Include dynamically computed values such as toolNames not known till instantiation
|
||||
getUtilityModule({ servicesManager }) {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
addTool,
|
||||
AngleTool,
|
||||
annotation,
|
||||
ArrowAnnotateTool,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
CircleROITool,
|
||||
LengthTool,
|
||||
PlanarFreehandROITool,
|
||||
RectangleROITool,
|
||||
} from '@cornerstonejs/tools';
|
||||
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||
import addToolInstance from './utils/addToolInstance';
|
||||
@@ -18,27 +18,26 @@ import toolNames from './tools/toolNames';
|
||||
/**
|
||||
* @param {object} configuration
|
||||
*/
|
||||
export default function init({
|
||||
configuration = {},
|
||||
}: Types.Extensions.ExtensionParams): void {
|
||||
addTool(DICOMSRDisplayTool);
|
||||
addToolInstance(toolNames.SRLength, LengthTool, {});
|
||||
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
|
||||
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
|
||||
addToolInstance(toolNames.SRLength, LengthTool);
|
||||
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
||||
addToolInstance(toolNames.SREllipticalROI, EllipticalROITool);
|
||||
addToolInstance(toolNames.SRCircleROI, CircleROITool);
|
||||
addToolInstance(toolNames.SRArrowAnnotate, ArrowAnnotateTool);
|
||||
addToolInstance(toolNames.SRAngle, AngleTool);
|
||||
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
|
||||
addToolInstance(toolNames.SRRectangleROI, RectangleROITool);
|
||||
|
||||
// TODO - fix the SR display of Cobb Angle, as it joins the two lines
|
||||
addToolInstance(toolNames.SRCobbAngle, CobbAngleTool);
|
||||
// TODO - fix the rehydration of Freehand, as it throws an exception
|
||||
// on a missing polyline. The fix is probably in CS3D
|
||||
addToolInstance(toolNames.SRPlanarFreehandROI, PlanarFreehandROITool);
|
||||
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const dashedLine = {
|
||||
lineDash: '4,4',
|
||||
};
|
||||
annotation.config.style.setToolGroupToolStyles('SRToolGroup', {
|
||||
[toolNames.DICOMSRDisplay]: dashedLine,
|
||||
SRLength: dashedLine,
|
||||
SRBidirectional: dashedLine,
|
||||
SREllipticalROI: dashedLine,
|
||||
@@ -47,6 +46,7 @@ export default function init({
|
||||
SRCobbAngle: dashedLine,
|
||||
SRAngle: dashedLine,
|
||||
SRPlanarFreehandROI: dashedLine,
|
||||
SRRectangleROI: dashedLine,
|
||||
global: {},
|
||||
});
|
||||
}
|
||||
@@ -22,14 +22,14 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
}
|
||||
|
||||
_getTextBoxLinesFromLabels(labels) {
|
||||
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||
// TODO -> max 5 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||
|
||||
const labelLength = Math.min(labels.length, 3);
|
||||
const labelLength = Math.min(labels.length, 5);
|
||||
const lines = [];
|
||||
|
||||
for (let i = 0; i < labelLength; i++) {
|
||||
const labelEntry = labels[i];
|
||||
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
|
||||
lines.push(`${_labelToShorthand(labelEntry.label)}: ${labelEntry.value}`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
@@ -40,49 +40,32 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
isPointNearTool = () => null;
|
||||
getHandleNearImagePoint = () => null;
|
||||
|
||||
renderAnnotation = (
|
||||
enabledElement: Types.IEnabledElement,
|
||||
svgDrawingHelper: any
|
||||
): void => {
|
||||
renderAnnotation = (enabledElement: Types.IEnabledElement, svgDrawingHelper: any): void => {
|
||||
const { viewport } = enabledElement;
|
||||
const { element } = viewport;
|
||||
|
||||
let annotations = annotation.state.getAnnotations(
|
||||
this.getToolName(),
|
||||
element
|
||||
);
|
||||
let annotations = annotation.state.getAnnotations(this.getToolName(), element);
|
||||
|
||||
// Todo: We don't need this anymore, filtering happens in triggerAnnotationRender
|
||||
if (!annotations?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
annotations = this.filterInteractableAnnotationsForElement(
|
||||
element,
|
||||
annotations
|
||||
);
|
||||
annotations = this.filterInteractableAnnotationsForElement(element, annotations);
|
||||
|
||||
if (!annotations?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement(
|
||||
element
|
||||
);
|
||||
const trackingUniqueIdentifiersForElement = getTrackingUniqueIdentifiersForElement(element);
|
||||
|
||||
const {
|
||||
activeIndex,
|
||||
trackingUniqueIdentifiers,
|
||||
} = trackingUniqueIdentifiersForElement;
|
||||
const { activeIndex, trackingUniqueIdentifiers } = trackingUniqueIdentifiersForElement;
|
||||
|
||||
const activeTrackingUniqueIdentifier =
|
||||
trackingUniqueIdentifiers[activeIndex];
|
||||
const activeTrackingUniqueIdentifier = trackingUniqueIdentifiers[activeIndex];
|
||||
|
||||
// Filter toolData to only render the data for the active SR.
|
||||
const filteredAnnotations = annotations.filter(annotation =>
|
||||
trackingUniqueIdentifiers.includes(
|
||||
annotation.data?.cachedStats?.TrackingUniqueIdentifier
|
||||
)
|
||||
trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier)
|
||||
);
|
||||
|
||||
if (!viewport._actors?.size) {
|
||||
@@ -94,20 +77,24 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
toolName: this.getToolName(),
|
||||
viewportId: enabledElement.viewport.id,
|
||||
};
|
||||
const { style: annotationStyle } = annotation.config;
|
||||
|
||||
for (let i = 0; i < filteredAnnotations.length; i++) {
|
||||
const annotation = filteredAnnotations[i];
|
||||
const annotationUID = annotation.annotationUID;
|
||||
const { renderableData } = annotation.data.cachedStats;
|
||||
const { cachedStats } = annotation.data;
|
||||
const { renderableData, TrackingUniqueIdentifier } = annotation.data;
|
||||
const { referencedImageId } = annotation.metadata;
|
||||
|
||||
styleSpecifier.annotationUID = annotationUID;
|
||||
|
||||
const groupStyle = annotationStyle.getToolGroupToolStyles(this.toolGroupId)[
|
||||
this.getToolName()
|
||||
];
|
||||
|
||||
const lineWidth = this.getStyle('lineWidth', styleSpecifier, annotation);
|
||||
const lineDash = this.getStyle('lineDash', styleSpecifier, annotation);
|
||||
const color =
|
||||
cachedStats.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||
TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||
? 'rgb(0, 255, 0)'
|
||||
: this.getStyle('color', styleSpecifier, annotation);
|
||||
|
||||
@@ -115,6 +102,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
color,
|
||||
lineDash,
|
||||
lineWidth,
|
||||
...groupStyle,
|
||||
};
|
||||
|
||||
Object.keys(renderableData).forEach(GraphicType => {
|
||||
@@ -138,8 +126,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
break;
|
||||
case SCOORD_TYPES.ELLIPSE:
|
||||
renderMethod = this.renderEllipse;
|
||||
canvasCoordinatesAdapter =
|
||||
utilities.math.ellipse.getCanvasEllipseCorners;
|
||||
canvasCoordinatesAdapter = utilities.math.ellipse.getCanvasEllipseCorners;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported GraphicType: ${GraphicType}`);
|
||||
@@ -178,6 +165,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
const drawingOptions = {
|
||||
color: options.color,
|
||||
width: options.lineWidth,
|
||||
lineDash: options.lineDash,
|
||||
};
|
||||
let allCanvasCoordinates = [];
|
||||
renderableData.map((data, index) => {
|
||||
@@ -221,15 +209,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
renderableData.map((data, index) => {
|
||||
canvasCoordinates = data.map(p => viewport.worldToCanvas(p));
|
||||
const handleGroupUID = '0';
|
||||
drawing.drawHandles(
|
||||
svgDrawingHelper,
|
||||
annotationUID,
|
||||
handleGroupUID,
|
||||
canvasCoordinates,
|
||||
{
|
||||
color: options.color,
|
||||
}
|
||||
);
|
||||
drawing.drawHandles(svgDrawingHelper, annotationUID, handleGroupUID, canvasCoordinates, {
|
||||
color: options.color,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -248,10 +230,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
canvasCoordinates.push(viewport.worldToCanvas(point));
|
||||
|
||||
// We get the other point for the arrow by using the image size
|
||||
const imagePixelModule = metaData.get(
|
||||
'imagePixelModule',
|
||||
referencedImageId
|
||||
);
|
||||
const imagePixelModule = metaData.get('imagePixelModule', referencedImageId);
|
||||
|
||||
let xOffset = 10;
|
||||
let yOffset = 10;
|
||||
@@ -309,9 +288,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
|
||||
const rotation = viewport.getRotation();
|
||||
|
||||
canvasCoordinates = ellipsePointsWorld.map(p =>
|
||||
viewport.worldToCanvas(p)
|
||||
);
|
||||
canvasCoordinates = ellipsePointsWorld.map(p => viewport.worldToCanvas(p));
|
||||
let canvasCorners;
|
||||
if (rotation == 90 || rotation == 270) {
|
||||
canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners([
|
||||
@@ -336,6 +313,7 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
{
|
||||
color: options.color,
|
||||
width: options.lineWidth,
|
||||
lineDash: options.lineDash,
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -366,23 +344,16 @@ export default class DICOMSRDisplayTool extends AnnotationTool {
|
||||
adaptedCanvasCoordinates = canvasCoordinatesAdapter(canvasCoordinates);
|
||||
}
|
||||
const textLines = this._getTextBoxLinesFromLabels(label);
|
||||
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(
|
||||
adaptedCanvasCoordinates
|
||||
);
|
||||
const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates);
|
||||
|
||||
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(
|
||||
canvasTextBoxCoords
|
||||
);
|
||||
if (!annotation.data?.handles?.textBox?.worldPosition) {
|
||||
annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords);
|
||||
}
|
||||
|
||||
const textBoxPosition = viewport.worldToCanvas(
|
||||
annotation.data.handles.textBox.worldPosition
|
||||
);
|
||||
const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition);
|
||||
|
||||
const textBoxUID = '1';
|
||||
const textBoxOptions = this.getLinkedTextBoxStyle(
|
||||
styleSpecifier,
|
||||
annotation
|
||||
);
|
||||
const textBoxOptions = this.getLinkedTextBoxStyle(styleSpecifier, annotation);
|
||||
|
||||
const boundingBox = drawing.drawLinkedTextBox(
|
||||
svgDrawingHelper,
|
||||
|
||||
@@ -27,15 +27,11 @@ function setTrackingUniqueIdentifiersForElement(
|
||||
};
|
||||
}
|
||||
|
||||
function setActiveTrackingUniqueIdentifierForElement(
|
||||
element,
|
||||
TrackingUniqueIdentifier
|
||||
) {
|
||||
function setActiveTrackingUniqueIdentifierForElement(element, TrackingUniqueIdentifier) {
|
||||
const enabledElement = getEnabledElement(element);
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
const trackingIdentifiersForElement =
|
||||
state.trackingIdentifiersByViewportId[viewport.id];
|
||||
const trackingIdentifiersForElement = state.trackingIdentifiersByViewportId[viewport.id];
|
||||
|
||||
if (trackingIdentifiersForElement) {
|
||||
const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex(
|
||||
|
||||
+24
-72
@@ -1,19 +1,13 @@
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import { Types, annotation } from '@cornerstonejs/tools';
|
||||
import { metaData, utilities, Types as csTypes } from '@cornerstonejs/core';
|
||||
|
||||
import toolNames from '../tools/toolNames';
|
||||
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
|
||||
export default function addMeasurement(
|
||||
measurement,
|
||||
imageId,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
// TODO -> Render rotated ellipse .
|
||||
export default function addDICOMSRDisplayAnnotation(measurement, imageId, frameNumber) {
|
||||
const toolName = toolNames.DICOMSRDisplay;
|
||||
|
||||
const measurementData = {
|
||||
@@ -31,69 +25,43 @@ export default function addMeasurement(
|
||||
}
|
||||
|
||||
measurementData.renderableData[GraphicType].push(
|
||||
_getRenderableData(
|
||||
GraphicType,
|
||||
GraphicData,
|
||||
imageId,
|
||||
measurement.TrackingIdentifier
|
||||
)
|
||||
_getRenderableData(GraphicType, GraphicData, imageId)
|
||||
);
|
||||
});
|
||||
|
||||
// Use the metadata provider to grab its imagePlaneModule metadata
|
||||
const imagePlaneModule = metaData.get('imagePlaneModule', imageId);
|
||||
|
||||
const annotationManager = annotation.state.getAnnotationManager();
|
||||
|
||||
// Create Cornerstone3D Annotation from measurement
|
||||
const frameNumber =
|
||||
(measurement.coords[0].ReferencedSOPSequence &&
|
||||
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
/**
|
||||
* This annotation (DICOMSRDisplay) is only used by the SR viewport.
|
||||
* This is used before the annotation is hydrated. If hydrated the measurement will be added
|
||||
* to the measurement service and will be available for the other viewports.
|
||||
*/
|
||||
const SRAnnotation: Types.Annotation = {
|
||||
annotationUID: measurement.TrackingUniqueIdentifier,
|
||||
highlighted: false,
|
||||
isLocked: false,
|
||||
invalidated: false,
|
||||
metadata: {
|
||||
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
||||
toolName: toolName,
|
||||
FrameOfReferenceUID: imagePlaneModule.frameOfReferenceUID,
|
||||
referencedImageId: imageId,
|
||||
},
|
||||
data: {
|
||||
label: measurement.labels,
|
||||
handles: {
|
||||
textBox: {},
|
||||
textBox: measurement.textBox ?? {},
|
||||
},
|
||||
cachedStats: {
|
||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||
renderableData: measurementData.renderableData,
|
||||
},
|
||||
frameNumber: frameNumber,
|
||||
cachedStats: {},
|
||||
TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier,
|
||||
renderableData: measurementData.renderableData,
|
||||
frameNumber,
|
||||
},
|
||||
};
|
||||
|
||||
const annotationManager = annotation.state.getAnnotationManager();
|
||||
annotationManager.addAnnotation(SRAnnotation);
|
||||
|
||||
measurement.loaded = true;
|
||||
measurement.imageId = imageId;
|
||||
measurement.displaySetInstanceUID = displaySetInstanceUID;
|
||||
|
||||
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
|
||||
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
|
||||
// It'd be super weird if it didn't anyway as a SCOORD.
|
||||
measurement.ReferencedSOPInstanceUID =
|
||||
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||
measurement.frameNumber = frameNumber;
|
||||
delete measurement.coords;
|
||||
}
|
||||
|
||||
function _getRenderableData(
|
||||
GraphicType,
|
||||
GraphicData,
|
||||
imageId,
|
||||
TrackingIdentifier
|
||||
) {
|
||||
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
||||
|
||||
function _getRenderableData(GraphicType, GraphicData, imageId) {
|
||||
let renderableData: csTypes.Point3[];
|
||||
|
||||
switch (GraphicType) {
|
||||
@@ -207,38 +175,22 @@ function _getRenderableData(
|
||||
throw new Error('imageId does not have imagePlaneModule metadata');
|
||||
}
|
||||
|
||||
const {
|
||||
columnCosines,
|
||||
}: { columnCosines: csTypes.Point3 } = imagePlaneModule;
|
||||
const { columnCosines }: { columnCosines: csTypes.Point3 } = imagePlaneModule;
|
||||
|
||||
// find which axis is parallel to the columnCosines
|
||||
const columnCosinesVec = vec3.fromValues(...columnCosines);
|
||||
|
||||
const projectedMajorAxisOnColVec = Math.abs(
|
||||
vec3.dot(columnCosinesVec, majorAxisVec)
|
||||
);
|
||||
const projectedMinorAxisOnColVec = Math.abs(
|
||||
vec3.dot(columnCosinesVec, minorAxisVec)
|
||||
);
|
||||
const projectedMajorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, majorAxisVec));
|
||||
const projectedMinorAxisOnColVec = Math.abs(vec3.dot(columnCosinesVec, minorAxisVec));
|
||||
|
||||
const absoluteOfMajorDotProduct = Math.abs(projectedMajorAxisOnColVec);
|
||||
const absoluteOfMinorDotProduct = Math.abs(projectedMinorAxisOnColVec);
|
||||
|
||||
renderableData = [];
|
||||
if (Math.abs(absoluteOfMajorDotProduct - 1) < EPSILON) {
|
||||
renderableData = [
|
||||
pointsWorld[0],
|
||||
pointsWorld[1],
|
||||
pointsWorld[2],
|
||||
pointsWorld[3],
|
||||
];
|
||||
renderableData = [pointsWorld[0], pointsWorld[1], pointsWorld[2], pointsWorld[3]];
|
||||
} else if (Math.abs(absoluteOfMinorDotProduct - 1) < EPSILON) {
|
||||
renderableData = [
|
||||
pointsWorld[2],
|
||||
pointsWorld[3],
|
||||
pointsWorld[0],
|
||||
pointsWorld[1],
|
||||
];
|
||||
renderableData = [pointsWorld[2], pointsWorld[3], pointsWorld[0], pointsWorld[1]];
|
||||
} else {
|
||||
console.warn('OBLIQUE ELLIPSE NOT YET SUPPORTED');
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
import { addTool } from '@cornerstonejs/tools';
|
||||
|
||||
export default function addToolInstance(
|
||||
name: string,
|
||||
toolClass,
|
||||
configuration?
|
||||
): void {
|
||||
export default function addToolInstance(name: string, toolClass, configuration?): void {
|
||||
class InstanceClass extends toolClass {
|
||||
static toolName = name;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,8 @@ import { DisplaySetService, classes } from '@ohif/core';
|
||||
const ImageSet = classes.ImageSet;
|
||||
|
||||
const findInstance = (measurement, displaySetService: DisplaySetService) => {
|
||||
const {
|
||||
displaySetInstanceUID,
|
||||
ReferencedSOPInstanceUID: sopUid,
|
||||
} = measurement;
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } = measurement;
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!referencedDisplaySet.images) {
|
||||
return;
|
||||
}
|
||||
@@ -20,10 +15,7 @@ const findInstance = (measurement, displaySetService: DisplaySetService) => {
|
||||
* contained within the provided display set.
|
||||
* @return an array of instances referenced.
|
||||
*/
|
||||
const findReferencedInstances = (
|
||||
displaySetService: DisplaySetService,
|
||||
displaySet
|
||||
) => {
|
||||
const findReferencedInstances = (displaySetService: DisplaySetService, displaySet) => {
|
||||
const instances = [];
|
||||
const instanceById = {};
|
||||
for (const measurement of displaySet.measurements) {
|
||||
@@ -58,7 +50,7 @@ const findReferencedInstances = (
|
||||
const createReferencedImageDisplaySet = (displaySetService, displaySet) => {
|
||||
const instances = findReferencedInstances(displaySetService, displaySet);
|
||||
// This will be a member function of the created image set
|
||||
const updateInstances = function() {
|
||||
const updateInstances = function () {
|
||||
this.images.splice(
|
||||
0,
|
||||
this.images.length,
|
||||
|
||||
@@ -14,8 +14,7 @@ const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => {
|
||||
}
|
||||
|
||||
instanceFound = displaySet.images.find(
|
||||
instanceMetadata =>
|
||||
instanceMetadata.getSOPInstanceUID() === SOPInstanceUID
|
||||
instanceMetadata => instanceMetadata.getSOPInstanceUID() === SOPInstanceUID
|
||||
);
|
||||
|
||||
return !!instanceFound;
|
||||
|
||||
@@ -18,10 +18,7 @@ const findMostRecentStructuredReport = studies => {
|
||||
}
|
||||
|
||||
if (isStructuredReportSeries(series)) {
|
||||
if (
|
||||
!mostRecentStructuredReport ||
|
||||
compareSeriesDate(series, mostRecentStructuredReport)
|
||||
) {
|
||||
if (!mostRecentStructuredReport || compareSeriesDate(series, mostRecentStructuredReport)) {
|
||||
mostRecentStructuredReport = series;
|
||||
}
|
||||
}
|
||||
@@ -38,10 +35,7 @@ const findMostRecentStructuredReport = studies => {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isStructuredReportSeries = series => {
|
||||
const supportedSopClassUIDs = [
|
||||
'1.2.840.10008.5.1.4.1.1.88.22',
|
||||
'1.2.840.10008.5.1.4.1.1.11.1',
|
||||
];
|
||||
const supportedSopClassUIDs = ['1.2.840.10008.5.1.4.1.1.88.22', '1.2.840.10008.5.1.4.1.1.11.1'];
|
||||
|
||||
const firstInstance = series.getFirstInstance();
|
||||
const SOPClassUID = firstInstance.getData().metadata.SOPClassUID;
|
||||
@@ -50,7 +44,7 @@ const isStructuredReportSeries = series => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Checkes if series1 is newer than series2
|
||||
* Checks if series1 is newer than series2
|
||||
*
|
||||
* @param {Object} series1 - Series Metadata 1
|
||||
* @param {Object} series2 - Series Metadata 2
|
||||
|
||||
@@ -2,17 +2,12 @@ import OHIF from '@ohif/core';
|
||||
import { annotation } from '@cornerstonejs/tools';
|
||||
const { log } = OHIF;
|
||||
|
||||
function getFilteredCornerstoneToolState(
|
||||
measurementData,
|
||||
additionalFindingTypes
|
||||
) {
|
||||
function getFilteredCornerstoneToolState(measurementData, additionalFindingTypes) {
|
||||
const filteredToolState = {};
|
||||
|
||||
function addToFilteredToolState(annotation, toolType) {
|
||||
if (!annotation.metadata?.referencedImageId) {
|
||||
log.warn(
|
||||
`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`
|
||||
);
|
||||
log.warn(`[DICOMSR] No referencedImageId found for ${toolType} ${annotation.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,9 +25,7 @@ function getFilteredCornerstoneToolState(
|
||||
};
|
||||
}
|
||||
|
||||
const measurementDataI = measurementData.find(
|
||||
md => md.uid === annotation.annotationUID
|
||||
);
|
||||
const measurementDataI = measurementData.find(md => md.uid === annotation.annotationUID);
|
||||
const toolData = imageIdSpecificToolState[toolType].data;
|
||||
|
||||
let { finding } = measurementDataI;
|
||||
@@ -77,9 +70,7 @@ function getFilteredCornerstoneToolState(
|
||||
for (let i = 0; i < framesOfReference.length; i++) {
|
||||
const frameOfReference = framesOfReference[i];
|
||||
|
||||
const frameOfReferenceAnnotations = annotationManager.getAnnotations(
|
||||
frameOfReference
|
||||
);
|
||||
const frameOfReferenceAnnotations = annotationManager.getAnnotations(frameOfReference);
|
||||
|
||||
const toolTypes = Object.keys(frameOfReferenceAnnotations);
|
||||
|
||||
@@ -91,9 +82,7 @@ function getFilteredCornerstoneToolState(
|
||||
if (annotations) {
|
||||
for (let k = 0; k < annotations.length; k++) {
|
||||
const annotation = annotations[k];
|
||||
const uidIndex = uids.findIndex(
|
||||
uid => uid === annotation.annotationUID
|
||||
);
|
||||
const uidIndex = uids.findIndex(uid => uid === annotation.annotationUID);
|
||||
|
||||
if (uidIndex !== -1) {
|
||||
addToFilteredToolState(annotation, toolType);
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
export default function getLabelFromDCMJSImportedToolData(toolData) {
|
||||
const { findingSites = [], finding } = toolData;
|
||||
|
||||
let freeTextLabel = findingSites.find(
|
||||
fs => fs.CodeValue === 'CORNERSTONEFREETEXT'
|
||||
);
|
||||
let freeTextLabel = findingSites.find(fs => fs.CodeValue === 'CORNERSTONEFREETEXT');
|
||||
|
||||
if (freeTextLabel) {
|
||||
return freeTextLabel.CodeMeaning;
|
||||
|
||||
+32
-54
@@ -43,25 +43,16 @@ const convertSites = (codingValues, sites) => {
|
||||
*
|
||||
*/
|
||||
export default function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
{ servicesManager, extensionManager, appConfig }: withAppTypes,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const annotationManager = CsAnnotation.state.getAnnotationManager();
|
||||
const disableEditing = appConfig?.disableEditing;
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
const {
|
||||
measurementService,
|
||||
displaySetService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
const { measurementService, displaySetService, customizationService } = servicesManager.services;
|
||||
|
||||
const codingValues = customizationService.getCustomization(
|
||||
'codingValues',
|
||||
{}
|
||||
);
|
||||
const displaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const codingValues = customizationService.getCustomization('codingValues', {});
|
||||
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
// TODO -> We should define a strict versioning somewhere.
|
||||
const mappings = measurementService.getSourceMappings(
|
||||
@@ -99,7 +90,7 @@ export default function hydrateStructuredReport(
|
||||
const datasetToUse = _mapLegacyDataSet(instance);
|
||||
|
||||
// Use dcmjs to generate toolState.
|
||||
const storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
|
||||
let storedMeasurementByAnnotationType = MeasurementReport.generateToolState(
|
||||
datasetToUse,
|
||||
// NOTE: we need to pass in the imageIds to dcmjs since the we use them
|
||||
// for the imageToWorld transformation. The following assumes that the order
|
||||
@@ -110,6 +101,16 @@ export default function hydrateStructuredReport(
|
||||
metaData
|
||||
);
|
||||
|
||||
const onBeforeSRHydration =
|
||||
customizationService.getModeCustomization('onBeforeSRHydration')?.value;
|
||||
|
||||
if (typeof onBeforeSRHydration === 'function') {
|
||||
storedMeasurementByAnnotationType = onBeforeSRHydration({
|
||||
storedMeasurementByAnnotationType,
|
||||
displaySet,
|
||||
});
|
||||
}
|
||||
|
||||
// Filter what is found by DICOM SR to measurements we support.
|
||||
const mappingDefinitions = mappings.map(m => m.annotationType);
|
||||
const hydratableMeasurementsInSR = {};
|
||||
@@ -125,16 +126,14 @@ export default function hydrateStructuredReport(
|
||||
|
||||
// TODO: notification if no hydratable?
|
||||
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
|
||||
const toolDataForAnnotationType =
|
||||
hydratableMeasurementsInSR[annotationType];
|
||||
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
|
||||
|
||||
toolDataForAnnotationType.forEach(toolData => {
|
||||
// Add the measurement to toolState
|
||||
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
|
||||
// files, and looking up the imageId from sopInstanceUIDToImageId results
|
||||
// in the wrong value.
|
||||
const frameNumber =
|
||||
(toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
|
||||
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
|
||||
const imageId =
|
||||
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
|
||||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
|
||||
@@ -150,10 +149,7 @@ export default function hydrateStructuredReport(
|
||||
|
||||
for (let i = 0; i < imageIds.length; i++) {
|
||||
const imageId = imageIds[i];
|
||||
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get(
|
||||
'instance',
|
||||
imageId
|
||||
);
|
||||
const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId);
|
||||
|
||||
if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) {
|
||||
SeriesInstanceUIDs.push(SeriesInstanceUID);
|
||||
@@ -162,23 +158,19 @@ export default function hydrateStructuredReport(
|
||||
if (!targetStudyInstanceUID) {
|
||||
targetStudyInstanceUID = StudyInstanceUID;
|
||||
} else if (targetStudyInstanceUID !== StudyInstanceUID) {
|
||||
console.warn(
|
||||
'NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'
|
||||
);
|
||||
console.warn('NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.');
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(hydratableMeasurementsInSR).forEach(annotationType => {
|
||||
const toolDataForAnnotationType =
|
||||
hydratableMeasurementsInSR[annotationType];
|
||||
const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType];
|
||||
|
||||
toolDataForAnnotationType.forEach(toolData => {
|
||||
// Add the measurement to toolState
|
||||
// dcmjs and Cornerstone3D has structural defect in supporting multi-frame
|
||||
// files, and looking up the imageId from sopInstanceUIDToImageId results
|
||||
// in the wrong value.
|
||||
const frameNumber =
|
||||
(toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
|
||||
const frameNumber = (toolData.annotation.data && toolData.annotation.data.frameNumber) || 1;
|
||||
const imageId =
|
||||
imageIdsForToolState[toolData.sopInstanceUid][frameNumber] ||
|
||||
sopInstanceUIDToImageId[toolData.sopInstanceUid];
|
||||
@@ -208,19 +200,11 @@ export default function hydrateStructuredReport(
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
||||
);
|
||||
annotation.data.label = getLabelFromDCMJSImportedToolData(toolData);
|
||||
annotation.data.finding = convertCode(
|
||||
codingValues,
|
||||
toolData.finding?.[0]
|
||||
);
|
||||
annotation.data.findingSites = convertSites(
|
||||
codingValues,
|
||||
toolData.findingSites
|
||||
);
|
||||
annotation.data.finding = convertCode(codingValues, toolData.finding?.[0]);
|
||||
annotation.data.findingSites = convertSites(codingValues, toolData.findingSites);
|
||||
annotation.data.site = annotation.data.findingSites?.[0];
|
||||
|
||||
const matchingMapping = mappings.find(
|
||||
m => m.annotationType === annotationType
|
||||
);
|
||||
const matchingMapping = mappings.find(m => m.annotationType === annotationType);
|
||||
|
||||
const newAnnotationUID = measurementService.addRawMeasurement(
|
||||
source,
|
||||
@@ -231,9 +215,7 @@ export default function hydrateStructuredReport(
|
||||
);
|
||||
|
||||
if (disableEditing) {
|
||||
const addedAnnotation = annotationManager.getAnnotation(
|
||||
newAnnotationUID
|
||||
);
|
||||
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
|
||||
locking.setAnnotationLocked(addedAnnotation, true);
|
||||
}
|
||||
|
||||
@@ -262,15 +244,14 @@ function _mapLegacyDataSet(dataset) {
|
||||
);
|
||||
|
||||
// Retrieve the Measurements themselves
|
||||
const measurementGroups = toArray(
|
||||
imagingMeasurementContent.ContentSequence
|
||||
).filter(codeMeaningEquals(GROUP));
|
||||
const measurementGroups = toArray(imagingMeasurementContent.ContentSequence).filter(
|
||||
codeMeaningEquals(GROUP)
|
||||
);
|
||||
|
||||
// For each of the supported measurement types, compute the measurement data
|
||||
const measurementData = {};
|
||||
|
||||
const cornerstoneToolClasses =
|
||||
MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
|
||||
const cornerstoneToolClasses = MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
|
||||
|
||||
const registeredToolClasses = [];
|
||||
|
||||
@@ -280,13 +261,10 @@ function _mapLegacyDataSet(dataset) {
|
||||
});
|
||||
|
||||
measurementGroups.forEach((measurementGroup, index) => {
|
||||
const measurementGroupContentSequence = toArray(
|
||||
measurementGroup.ContentSequence
|
||||
);
|
||||
const measurementGroupContentSequence = toArray(measurementGroup.ContentSequence);
|
||||
|
||||
const TrackingIdentifierGroup = measurementGroupContentSequence.find(
|
||||
contentItem =>
|
||||
contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
|
||||
contentItem => contentItem.ConceptNameCodeSequence.CodeMeaning === TRACKING_IDENTIFIER
|
||||
);
|
||||
|
||||
const TrackingIdentifier = TrackingIdentifierGroup.TextValue;
|
||||
@@ -304,7 +282,7 @@ function _mapLegacyDataSet(dataset) {
|
||||
return dataset;
|
||||
}
|
||||
|
||||
const toArray = function(x) {
|
||||
const toArray = function (x) {
|
||||
return Array.isArray(x) ? x : [x];
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
|
||||
const cornerstoneAdapters =
|
||||
adaptersSR.Cornerstone3D.MeasurementReport
|
||||
.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
|
||||
adaptersSR.Cornerstone3D.MeasurementReport.CORNERSTONE_TOOL_CLASSES_BY_UTILITY_TYPE;
|
||||
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
const CORNERSTONE_3D_TAG = cornerstoneAdapters.CORNERSTONE_3D_TAG;
|
||||
@@ -24,8 +23,7 @@ export default function isRehydratable(displaySet, mappings) {
|
||||
|
||||
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
|
||||
adapterKey =>
|
||||
typeof cornerstoneAdapters[adapterKey]
|
||||
.isValidCornerstoneTrackingIdentifier === 'function'
|
||||
typeof cornerstoneAdapters[adapterKey].isValidCornerstoneTrackingIdentifier === 'function'
|
||||
);
|
||||
|
||||
const adapters = [];
|
||||
@@ -48,19 +46,13 @@ export default function isRehydratable(displaySet, mappings) {
|
||||
|
||||
const mappedTrackingIdentifier = `${cornerstoneTag}:${toolName}`;
|
||||
|
||||
return adapter.isValidCornerstoneTrackingIdentifier(
|
||||
mappedTrackingIdentifier
|
||||
);
|
||||
return adapter.isValidCornerstoneTrackingIdentifier(mappedTrackingIdentifier);
|
||||
});
|
||||
|
||||
if (hydratable) {
|
||||
return true;
|
||||
}
|
||||
console.log(
|
||||
'Measurement is not rehydratable',
|
||||
TrackingIdentifier,
|
||||
measurements[i]
|
||||
);
|
||||
console.log('Measurement is not rehydratable', TrackingIdentifier, measurements[i]);
|
||||
}
|
||||
|
||||
console.log('No measurements found which were rehydratable');
|
||||
|
||||
@@ -1,32 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import OHIF, { utils, ServicesManager, ExtensionManager } from '@ohif/core';
|
||||
import { ExtensionManager } from '@ohif/core';
|
||||
|
||||
import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSRModule';
|
||||
|
||||
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
|
||||
import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
import { useAppConfig } from '@state';
|
||||
import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet';
|
||||
|
||||
const { formatDate } = utils;
|
||||
|
||||
const MEASUREMENT_TRACKING_EXTENSION_ID =
|
||||
'@ohif/extension-measurement-tracking';
|
||||
const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking';
|
||||
|
||||
const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup';
|
||||
|
||||
function OHIFCornerstoneSRViewport(props) {
|
||||
const {
|
||||
children,
|
||||
dataSource,
|
||||
displaySets,
|
||||
viewportIndex,
|
||||
viewportLabel,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
function OHIFCornerstoneSRViewport(props: withAppTypes) {
|
||||
const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } =
|
||||
props;
|
||||
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
@@ -34,8 +24,11 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
measurementService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
// SR viewport will always have a single display set
|
||||
if (displaySets.length > 1) {
|
||||
throw new Error('SR viewport should only have a single display set');
|
||||
@@ -46,15 +39,12 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const [measurementSelected, setMeasurementSelected] = useState(0);
|
||||
const [measurementCount, setMeasurementCount] = useState(1);
|
||||
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(
|
||||
null
|
||||
);
|
||||
const [
|
||||
referencedDisplaySetMetadata,
|
||||
setReferencedDisplaySetMetadata,
|
||||
] = useState(null);
|
||||
const [activeImageDisplaySetData, setActiveImageDisplaySetData] = useState(null);
|
||||
const [referencedDisplaySetMetadata, setReferencedDisplaySetMetadata] = useState(null);
|
||||
const [element, setElement] = useState(null);
|
||||
const { viewports, activeViewportIndex } = viewportGrid;
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
|
||||
// Optional hook into tracking extension, if present.
|
||||
let trackedMeasurements;
|
||||
@@ -82,13 +72,11 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(
|
||||
SeriesInstanceUIDs[0]
|
||||
);
|
||||
const displaySets = displaySetService.getDisplaySetsForSeries(SeriesInstanceUIDs[0]);
|
||||
if (displaySets.length) {
|
||||
viewportGridService.setDisplaySetsForViewports([
|
||||
{
|
||||
viewportIndex: activeViewportIndex,
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUIDs: [displaySets[0].displaySetInstanceUID],
|
||||
},
|
||||
]);
|
||||
@@ -126,11 +114,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
|
||||
const updateViewport = useCallback(
|
||||
newMeasurementSelected => {
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
sopClassUids,
|
||||
} = srDisplaySet;
|
||||
const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet;
|
||||
|
||||
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
||||
return;
|
||||
@@ -139,11 +123,13 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
if (sopClassUids && sopClassUids.length > 1) {
|
||||
// Todo: what happens if there are multiple SOP Classes? Why we are
|
||||
// not throwing an error?
|
||||
console.warn(
|
||||
'More than one SOPClassUID in the same series is not yet supported.'
|
||||
);
|
||||
console.warn('More than one SOPClassUID in the same series is not yet supported.');
|
||||
}
|
||||
|
||||
// if (!srDisplaySet.measurements || !srDisplaySet.measurements.length) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
_getViewportReferencedDisplaySetData(
|
||||
srDisplaySet,
|
||||
newMeasurementSelected,
|
||||
@@ -163,19 +149,11 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
// imageIdIndex will handle it by updating the viewport, but if they
|
||||
// are the same we just need to use measurementService to jump to the
|
||||
// new measurement
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(
|
||||
viewportInfo.getViewportId()
|
||||
);
|
||||
const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
const imageIds = csViewport.getImageIds();
|
||||
|
||||
const imageIdIndex = imageIds.indexOf(
|
||||
measurements[newMeasurementSelected].imageId
|
||||
);
|
||||
const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId);
|
||||
|
||||
if (imageIdIndex !== -1) {
|
||||
csViewport.setImageIdIndex(imageIdIndex);
|
||||
@@ -183,7 +161,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
}
|
||||
});
|
||||
},
|
||||
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportIndex]
|
||||
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportId]
|
||||
);
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
@@ -226,40 +204,31 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
// The positionIds for the viewport aren't meaningful for the child display sets
|
||||
positionIds: null,
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
onElementEnabled(evt);
|
||||
}}
|
||||
initialImageIndex={initialImageIndex}
|
||||
isJumpToMeasurementDisabled={true}
|
||||
></Component>
|
||||
);
|
||||
}, [activeImageDisplaySetData, viewportIndex, measurementSelected]);
|
||||
}, [activeImageDisplaySetData, viewportId, measurementSelected]);
|
||||
|
||||
const onMeasurementChange = useCallback(
|
||||
direction => {
|
||||
let newMeasurementSelected = measurementSelected;
|
||||
|
||||
if (direction === 'right') {
|
||||
newMeasurementSelected++;
|
||||
|
||||
if (newMeasurementSelected >= measurementCount) {
|
||||
newMeasurementSelected = 0;
|
||||
}
|
||||
} else {
|
||||
newMeasurementSelected--;
|
||||
|
||||
if (newMeasurementSelected < 0) {
|
||||
newMeasurementSelected = measurementCount - 1;
|
||||
}
|
||||
newMeasurementSelected += direction;
|
||||
if (newMeasurementSelected >= measurementCount) {
|
||||
newMeasurementSelected = 0;
|
||||
} else if (newMeasurementSelected < 0) {
|
||||
newMeasurementSelected = measurementCount - 1;
|
||||
}
|
||||
|
||||
setTrackingIdentifiers(newMeasurementSelected);
|
||||
updateViewport(newMeasurementSelected);
|
||||
},
|
||||
[
|
||||
measurementSelected,
|
||||
measurementCount,
|
||||
updateViewport,
|
||||
setTrackingIdentifiers,
|
||||
]
|
||||
[measurementSelected, measurementCount, updateViewport, setTrackingIdentifiers]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -269,12 +238,10 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
const onDisplaySetsRemovedSubscription = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SETS_REMOVED,
|
||||
({ displaySetInstanceUIDs }) => {
|
||||
const activeViewport = viewports[activeViewportIndex];
|
||||
if (
|
||||
displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)
|
||||
) {
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: activeViewportIndex,
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUIDs: [],
|
||||
});
|
||||
}
|
||||
@@ -294,12 +261,16 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
* if it is hydrated we don't even use the SR viewport.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!srDisplaySet.isLoaded) {
|
||||
srDisplaySet.load();
|
||||
}
|
||||
const numMeasurements = srDisplaySet.measurements.length;
|
||||
setMeasurementCount(numMeasurements);
|
||||
}, [srDisplaySet]);
|
||||
const loadSR = async () => {
|
||||
if (!srDisplaySet.isLoaded) {
|
||||
await srDisplaySet.load();
|
||||
}
|
||||
const numMeasurements = srDisplaySet.measurements.length;
|
||||
setMeasurementCount(numMeasurements);
|
||||
updateViewport(measurementSelected);
|
||||
};
|
||||
loadSR();
|
||||
}, [dataSource, srDisplaySet]);
|
||||
|
||||
/**
|
||||
* Hook to update the tracking identifiers when the selected measurement changes or
|
||||
@@ -316,18 +287,50 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
* Todo: what is this, not sure what it does regarding the react aspect,
|
||||
* it is updating a local variable? which is not state.
|
||||
*/
|
||||
let isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
|
||||
const [isLocked, setIsLocked] = useState(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
useEffect(() => {
|
||||
isLocked = trackedMeasurements?.context?.trackedSeries?.length > 0;
|
||||
setIsLocked(trackedMeasurements?.context?.trackedSeries?.length > 0);
|
||||
}, [trackedMeasurements]);
|
||||
|
||||
/**
|
||||
* Data fetching for the SR displaySet, which updates the measurements and
|
||||
* also gets the referenced image displaySet that SR is based on.
|
||||
*/
|
||||
useEffect(() => {
|
||||
updateViewport(measurementSelected);
|
||||
}, [dataSource, srDisplaySet]);
|
||||
viewportActionCornersService.setComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: _getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportId,
|
||||
isRehydratable: srDisplaySet.isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
t,
|
||||
}),
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topLeft,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
index: 0,
|
||||
component: (
|
||||
<ViewportActionArrows
|
||||
key="actionArrows"
|
||||
onArrowsClick={onMeasurementChange}
|
||||
></ViewportActionArrows>
|
||||
),
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [
|
||||
isLocked,
|
||||
onMeasurementChange,
|
||||
sendTrackedMeasurementsEvent,
|
||||
srDisplaySet,
|
||||
t,
|
||||
viewportActionCornersService,
|
||||
viewportId,
|
||||
]);
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
let childrenWithProps = null;
|
||||
@@ -341,69 +344,16 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
PatientID,
|
||||
PatientName,
|
||||
PatientSex,
|
||||
PatientAge,
|
||||
SliceThickness,
|
||||
ManufacturerModelName,
|
||||
StudyDate,
|
||||
SeriesDescription,
|
||||
SpacingBetweenSlices,
|
||||
SeriesNumber,
|
||||
} = referencedDisplaySetMetadata;
|
||||
|
||||
// TODO -> disabled double click for now: onDoubleClick={_onDoubleClick}
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
onDoubleClick={evt => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
}}
|
||||
onArrowsClick={onMeasurementChange}
|
||||
getStatusComponent={() =>
|
||||
_getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportIndex,
|
||||
isTracked: false,
|
||||
isRehydratable: srDisplaySet.isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
})
|
||||
}
|
||||
studyData={{
|
||||
label: viewportLabel,
|
||||
useAltStyling: true,
|
||||
studyDate: formatDate(StudyDate),
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: SeriesDescription || '',
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
: '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined
|
||||
? `${SpacingBetweenSlices.toFixed(2)}mm`
|
||||
: '',
|
||||
scanner: ManufacturerModelName || '',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||
<div className="relative flex h-full w-full flex-row overflow-hidden">
|
||||
{getCornerstoneViewport()}
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
@@ -413,14 +363,13 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
|
||||
OHIFCornerstoneSRViewport.propTypes = {
|
||||
displaySets: PropTypes.arrayOf(PropTypes.object),
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.object,
|
||||
children: PropTypes.node,
|
||||
viewportLabel: PropTypes.string,
|
||||
customProps: PropTypes.object,
|
||||
viewportOptions: PropTypes.object,
|
||||
viewportLabel: PropTypes.string,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager).isRequired,
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
extensionManager: PropTypes.instanceOf(ExtensionManager).isRequired,
|
||||
};
|
||||
|
||||
@@ -437,10 +386,15 @@ async function _getViewportReferencedDisplaySetData(
|
||||
const measurement = measurements[measurementSelected];
|
||||
|
||||
const { displaySetInstanceUID } = measurement;
|
||||
if (!displaySet.keyImageDisplaySet) {
|
||||
// Create a new display set, and preserve a reference to it here,
|
||||
// so that it can be re-displayed and shown inside the SR viewport.
|
||||
// This is only for ease of redisplay - the display set is stored in the
|
||||
// usual manner in the display set service.
|
||||
displaySet.keyImageDisplaySet = createReferencedImageDisplaySet(displaySetService, displaySet);
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
const image0 = referencedDisplaySet.images[0];
|
||||
const referencedDisplaySetMetadata = {
|
||||
@@ -462,26 +416,25 @@ async function _getViewportReferencedDisplaySetData(
|
||||
|
||||
function _getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
t,
|
||||
}) {
|
||||
const handleMouseUp = () => {
|
||||
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
||||
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
});
|
||||
};
|
||||
|
||||
const { t } = useTranslation('Common');
|
||||
const loadStr = t('LOAD');
|
||||
|
||||
// 1 - Incompatible
|
||||
// 2 - Locked
|
||||
// 3 - Rehydratable / Open
|
||||
const state =
|
||||
isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
|
||||
const state = isRehydratable && !isLocked ? 3 : isRehydratable && isLocked ? 2 : 1;
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
@@ -511,22 +464,25 @@ function _getStatusComponent({
|
||||
);
|
||||
break;
|
||||
case 3:
|
||||
StatusIcon = () => <Icon name="status-untracked" />;
|
||||
|
||||
ToolTipMessage = () => (
|
||||
<div>{`Click ${loadStr} to restore measurements.`}</div>
|
||||
StatusIcon = () => (
|
||||
<Icon
|
||||
className="text-aqua-pale"
|
||||
name="status-untracked"
|
||||
/>
|
||||
);
|
||||
|
||||
ToolTipMessage = () => <div>{`Click ${loadStr} to restore measurements.`}</div>;
|
||||
}
|
||||
|
||||
const StatusArea = () => (
|
||||
<div className="flex h-6 leading-6 cursor-default text-sm text-white">
|
||||
<div className="min-w-[45px] flex items-center p-1 rounded-l-xl rounded-r bg-customgray-100">
|
||||
<div className="flex h-6 cursor-default text-sm leading-6 text-white">
|
||||
<div className="bg-customgray-100 flex min-w-[45px] items-center rounded-l-xl rounded-r p-1">
|
||||
<StatusIcon />
|
||||
<span className="ml-1">SR</span>
|
||||
</div>
|
||||
{state === 3 && (
|
||||
<div
|
||||
className="ml-1 px-1.5 rounded cursor-pointer hover:text-black bg-primary-main hover:bg-primary-light"
|
||||
className="bg-primary-main hover:bg-primary-light ml-1 cursor-pointer rounded px-1.5 hover:text-black"
|
||||
// Using onMouseUp here because onClick is not working when the viewport is not active and is styled with pointer-events:none
|
||||
onMouseUp={handleMouseUp}
|
||||
>
|
||||
@@ -539,7 +495,10 @@ function _getStatusComponent({
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<Tooltip
|
||||
content={<ToolTipMessage />}
|
||||
position="bottom-left"
|
||||
>
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
const webpack = require('webpack');
|
||||
const { merge } = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.base.js');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, '../');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
const ENTRY = {
|
||||
app: `${SRC_DIR}/index.ts`,
|
||||
};
|
||||
|
||||
const outputName = `ohif-${pkg.name.split('/').pop()}`;
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR, ENTRY });
|
||||
|
||||
return merge(commonConfig, {
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'ohif-extension-cornerstone',
|
||||
libraryTarget: 'umd',
|
||||
filename: pkg.main,
|
||||
},
|
||||
externals: [/\b(vtk.js)/, /\b(dcmjs)/, /\b(gl-matrix)/, /^@ohif/, /^@cornerstonejs/],
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `./dist/${outputName}.css`,
|
||||
chunkFilename: `./dist/${outputName}.css`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,231 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.8.2](https://github.com/OHIF/Viewers/compare/v3.8.1...v3.8.2) (2024-06-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.8.1](https://github.com/OHIF/Viewers/compare/v3.8.0...v3.8.1) (2024-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **crosshairs:** reset angle, position, and slabthickness for crosshairs when reset viewport tool is used ([#4113](https://github.com/OHIF/Viewers/issues/4113)) ([2d4e049](https://github.com/OHIF/Viewers/commit/2d4e049d8a54ca25c9dc02f29f2e53a6968d82e1))
|
||||
* dicom json for orthanc by Update package versions for [@cornerstonejs](https://github.com/cornerstonejs) dependencies ([#4165](https://github.com/OHIF/Viewers/issues/4165)) ([9f5165d](https://github.com/OHIF/Viewers/commit/9f5165de47ed547919683498cfae87909de94786))
|
||||
* **segmentation:** Address issue where segmentation creation failed on layout change ([#4153](https://github.com/OHIF/Viewers/issues/4153)) ([1b290bd](https://github.com/OHIF/Viewers/commit/1b290bdaa1a0ba750a4e5f539a51a636de529240))
|
||||
* **us:** logic for color conversion ([b7ebb0a](https://github.com/OHIF/Viewers/commit/b7ebb0aa8f814427045b7d5e7568dc0298ab0973))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **typings:** Enhance typing support with withAppTypes and custom services throughout OHIF ([#4090](https://github.com/OHIF/Viewers/issues/4090)) ([3539e04](https://github.com/OHIF/Viewers/commit/3539e040193369b8011254d0c67804ef67d83a6c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.93...v3.8.0) (2024-04-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.93](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.92...v3.8.0-beta.93) (2024-04-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **toolbox:** Preserve user-specified tool state and streamline command execution ([#4063](https://github.com/OHIF/Viewers/issues/4063)) ([f1a736d](https://github.com/OHIF/Viewers/commit/f1a736d1934733a434cb87b2c284907a3122403f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.92](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.91...v3.8.0-beta.92) (2024-04-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** fix patient header for doc, track ball rotate resize observer and add segmentation button not being enabled on viewport data change ([#4068](https://github.com/OHIF/Viewers/issues/4068)) ([c09311d](https://github.com/OHIF/Viewers/commit/c09311d3b7df05fcd00a9f36a7233e9d7e5589d0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.91](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.90...v3.8.0-beta.91) (2024-04-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.90](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.89...v3.8.0-beta.90) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.89](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.88...v3.8.0-beta.89) (2024-04-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport-webworker-segmentation:** Resolve issues with viewport detection, webworker termination, and segmentation panel layout change ([#4059](https://github.com/OHIF/Viewers/issues/4059)) ([52a0c59](https://github.com/OHIF/Viewers/commit/52a0c59294a4161fcca0a6708855549034849951))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.88](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.87...v3.8.0-beta.88) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.87](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.86...v3.8.0-beta.87) (2024-04-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **tmtv-mode:** Add Brush tools and move SUV peak calculation to web worker ([#4053](https://github.com/OHIF/Viewers/issues/4053)) ([8192e34](https://github.com/OHIF/Viewers/commit/8192e348eca993fec331d4963efe88f9a730eceb))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.86](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.85...v3.8.0-beta.86) (2024-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **layouts:** and fix thumbnail in touch and update migration guide for 3.8 release ([#4052](https://github.com/OHIF/Viewers/issues/4052)) ([d250d04](https://github.com/OHIF/Viewers/commit/d250d04580883446fcb8d748b2a97c5c198922af))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.85](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.84...v3.8.0-beta.85) (2024-04-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.84](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.83...v3.8.0-beta.84) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** and replace seriesInstanceUID and seriesInstanceUIDs URL with seriesInstanceUIDs ([#4049](https://github.com/OHIF/Viewers/issues/4049)) ([da7c1a5](https://github.com/OHIF/Viewers/commit/da7c1a5d8c54bfa1d3f97bbc500386bf76e7fd9d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.83](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.82...v3.8.0-beta.83) (2024-04-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - final ([#4048](https://github.com/OHIF/Viewers/issues/4048)) ([170bb96](https://github.com/OHIF/Viewers/commit/170bb96983082c39b22b7352e0c54aacf3e73b02))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.82](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.81...v3.8.0-beta.82) (2024-04-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes - more ([#4043](https://github.com/OHIF/Viewers/issues/4043)) ([3754c22](https://github.com/OHIF/Viewers/commit/3754c224b4dab28182adb0a41e37d890942144d8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.81](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.80...v3.8.0-beta.81) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **viewport:** Reset viewport state and fix CINE looping, thumbnail resolution, and dynamic tool settings ([#4037](https://github.com/OHIF/Viewers/issues/4037)) ([f99a0bf](https://github.com/OHIF/Viewers/commit/f99a0bfb31434aa137bbb3ed1f9eef1dfcc09025))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.80](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.79...v3.8.0-beta.80) (2024-04-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bugs:** enhancements and bug fixes ([#4036](https://github.com/OHIF/Viewers/issues/4036)) ([e80fc6f](https://github.com/OHIF/Viewers/commit/e80fc6f47708e1d6b1a1e1de438196a4b74ec637))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.79](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.78...v3.8.0-beta.79) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **SM:** remove SM measurements from measurement panel ([#4022](https://github.com/OHIF/Viewers/issues/4022)) ([df49a65](https://github.com/OHIF/Viewers/commit/df49a653be61a93f6e9fb3663aabe9775c31fd13))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.78](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.77...v3.8.0-beta.78) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.77](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.76...v3.8.0-beta.77) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.76](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.75...v3.8.0-beta.76) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.75](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.74...v3.8.0-beta.75) (2024-04-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dynamic-volume
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.8.0-beta.74](https://github.com/OHIF/Viewers/compare/v3.8.0-beta.73...v3.8.0-beta.74) (2024-04-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **4D:** Add 4D dynamic volume rendering and new pre-clinical 4d pt/ct mode ([#3664](https://github.com/OHIF/Viewers/issues/3664)) ([d57e8bc](https://github.com/OHIF/Viewers/commit/d57e8bc1571c6da4effaa492ee2d162c552365a2))
|
||||
@@ -0,0 +1,20 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 cornerstone-dynamic-volume ()
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,8 @@
|
||||
# cornerstone-dynamic-volume
|
||||
## Description
|
||||
|
||||
## Author
|
||||
OHIF
|
||||
|
||||
## License
|
||||
MIT
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('../../babel.config.js');
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dynamic-volume",
|
||||
"version": "3.8.2",
|
||||
"description": "OHIF extension for 4D volumes data",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/ohif-extension-cornerstone-dynamic-volume.umd.js",
|
||||
"module": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./types": "./src/types/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --output-pathinfo",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"clean": "shx rm -rf dist",
|
||||
"clean:deep": "yarn run clean && shx rm -rf node_modules",
|
||||
"start": "yarn run dev",
|
||||
"test:unit": "jest --watchAll",
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.8.2\n",
|
||||
"@ohif/extension-cornerstone": "3.8.2\n",
|
||||
"@ohif/extension-default": "3.8.2\n",
|
||||
"@ohif/i18n": "3.8.2\n",
|
||||
"@ohif/ui": "3.8.2\n",
|
||||
"dcmjs": "^0.29.5",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/core": "^1.77.6",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.77.6",
|
||||
"@cornerstonejs/tools": "^1.77.6",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import updateSegmentationsChartDisplaySet from './updateSegmentationsChartDisplaySet';
|
||||
|
||||
export { updateSegmentationsChartDisplaySet };
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||
|
||||
import * as cs from '@cornerstonejs/core';
|
||||
import * as csTools from '@cornerstonejs/tools';
|
||||
|
||||
const CHART_MODALITY = 'CHT';
|
||||
const SEG_CHART_INSTANCE_UID = utils.guid();
|
||||
|
||||
// Private SOPClassUid for chart data
|
||||
const ChartDataSOPClassUid = '1.9.451.13215.7.3.2.7.6.1';
|
||||
|
||||
const { utilities: csToolsUtils } = csTools;
|
||||
|
||||
function _getDateTimeStr() {
|
||||
const now = new Date();
|
||||
const date =
|
||||
now.getFullYear() + ('0' + now.getUTCMonth()).slice(-2) + ('0' + now.getUTCDate()).slice(-2);
|
||||
const time =
|
||||
('0' + now.getUTCHours()).slice(-2) +
|
||||
('0' + now.getUTCMinutes()).slice(-2) +
|
||||
('0' + now.getUTCSeconds()).slice(-2);
|
||||
|
||||
return { date, time };
|
||||
}
|
||||
|
||||
function _getTimePointsDataByTagName(volume, timePointsTag) {
|
||||
const uniqueTimePoints = volume.imageIds.reduce((timePoints, imageId) => {
|
||||
const instance = DicomMetadataStore.getInstanceByImageId(imageId);
|
||||
const timePointValue = instance[timePointsTag];
|
||||
|
||||
if (timePointValue !== undefined) {
|
||||
timePoints.add(timePointValue);
|
||||
}
|
||||
|
||||
return timePoints;
|
||||
}, new Set());
|
||||
|
||||
return Array.from(uniqueTimePoints).sort((a: number, b: number) => a - b);
|
||||
}
|
||||
|
||||
function _convertTimePointsUnit(timePoints, timePointsUnit) {
|
||||
const validUnits = ['ms', 's', 'm', 'h'];
|
||||
const divisors = [1000, 60, 60];
|
||||
const currentUnitIndex = validUnits.indexOf(timePointsUnit);
|
||||
let divisor = 1;
|
||||
|
||||
if (currentUnitIndex !== -1) {
|
||||
for (let i = currentUnitIndex; i < validUnits.length - 1; i++) {
|
||||
const newDivisor = divisor * divisors[i];
|
||||
const greaterThanDivisorCount = timePoints.filter(timePoint => timePoint > newDivisor).length;
|
||||
|
||||
// Change the scale only if more than 50% of the time points are
|
||||
// greater than the new divisor.
|
||||
if (greaterThanDivisorCount <= timePoints.length / 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
divisor = newDivisor;
|
||||
timePointsUnit = validUnits[i + 1];
|
||||
}
|
||||
|
||||
if (divisor > 1) {
|
||||
timePoints = timePoints.map(timePoint => timePoint / divisor);
|
||||
}
|
||||
}
|
||||
|
||||
return { timePoints, timePointsUnit };
|
||||
}
|
||||
|
||||
// It currently supports only one tag but a few other will be added soon
|
||||
// Supported 4D Tags
|
||||
// (0018,1060) Trigger Time [NOK]
|
||||
// (0018,0081) Echo Time [NOK]
|
||||
// (0018,0086) Echo Number [NOK]
|
||||
// (0020,0100) Temporal Position Identifier [NOK]
|
||||
// (0054,1300) FrameReferenceTime [OK]
|
||||
function _getTimePointsData(volume) {
|
||||
const timePointsTags = {
|
||||
FrameReferenceTime: {
|
||||
unit: 'ms',
|
||||
},
|
||||
};
|
||||
|
||||
const timePointsTagNames = Object.keys(timePointsTags);
|
||||
let timePoints;
|
||||
let timePointsUnit;
|
||||
|
||||
for (let i = 0; i < timePointsTagNames.length; i++) {
|
||||
const tagName = timePointsTagNames[i];
|
||||
const curTimePoints = _getTimePointsDataByTagName(volume, tagName);
|
||||
|
||||
if (curTimePoints.length) {
|
||||
timePoints = curTimePoints;
|
||||
timePointsUnit = timePointsTags[tagName].unit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!timePoints.length) {
|
||||
const concatTagNames = timePointsTagNames.join(', ');
|
||||
|
||||
throw new Error(`Could not extract time points data for the following tags: ${concatTagNames}`);
|
||||
}
|
||||
|
||||
const convertedTimePoints = _convertTimePointsUnit(timePoints, timePointsUnit);
|
||||
|
||||
timePoints = convertedTimePoints.timePoints;
|
||||
timePointsUnit = convertedTimePoints.timePointsUnit;
|
||||
|
||||
return { timePoints, timePointsUnit };
|
||||
}
|
||||
|
||||
function _getSegmentationData(segmentation, volumesTimePointsCache, displaySetService) {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||
const anInstance = displaySet.instances?.[0];
|
||||
|
||||
if (anInstance) {
|
||||
return (
|
||||
anInstance.FrameReferenceTime !== undefined || anInstance.NumberOfTimeSlices !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// const referencedDynamicVolume = cs.cache.getVolume(dynamic4DDisplaySet.displaySetInstanceUID);
|
||||
let volumeCacheKey: string | undefined;
|
||||
const volumeId = dynamic4DDisplaySet.displaySetInstanceUID;
|
||||
|
||||
for (const [key] of cs.cache._volumeCache) {
|
||||
if (key.includes(volumeId)) {
|
||||
volumeCacheKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let referencedDynamicVolume;
|
||||
if (volumeCacheKey) {
|
||||
referencedDynamicVolume = cs.cache.getVolume(volumeCacheKey);
|
||||
}
|
||||
|
||||
const { StudyInstanceUID, StudyDescription } = DicomMetadataStore.getInstanceByImageId(
|
||||
referencedDynamicVolume.imageIds[0]
|
||||
);
|
||||
|
||||
const [timeData, _] = csToolsUtils.dynamicVolume.getDataInTime(referencedDynamicVolume, {
|
||||
maskVolumeId: segmentation.id,
|
||||
}) as number[][];
|
||||
|
||||
const pixelCount = timeData.length;
|
||||
|
||||
if (pixelCount === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// since we only use one segmentation representation per segmentationId
|
||||
// it is fine to pick the first one
|
||||
const segmentationRepresentations = csTools.segmentation.state.getSegmentationIdRepresentations(
|
||||
segmentation.id
|
||||
);
|
||||
|
||||
const segmentationRepresentationUID =
|
||||
segmentationRepresentations[0].segmentationRepresentationUID;
|
||||
|
||||
const toolGroupId = csTools.segmentation.state.getToolGroupIdFromSegmentationRepresentationUID(
|
||||
segmentationRepresentationUID
|
||||
);
|
||||
|
||||
// Todo: this is useless we should be able to grab color with just segRepUID and segmentIndex
|
||||
const color = csTools.segmentation.config.color.getColorForSegmentIndex(
|
||||
toolGroupId,
|
||||
segmentationRepresentationUID,
|
||||
1 // segmentIndex
|
||||
);
|
||||
|
||||
const hexColor = cs.utilities.color.rgbToHex(...color);
|
||||
let timePointsData = volumesTimePointsCache.get(referencedDynamicVolume);
|
||||
|
||||
if (!timePointsData) {
|
||||
timePointsData = _getTimePointsData(referencedDynamicVolume);
|
||||
volumesTimePointsCache.set(referencedDynamicVolume, timePointsData);
|
||||
}
|
||||
|
||||
const { timePoints, timePointsUnit } = timePointsData;
|
||||
|
||||
if (timePoints.length !== timeData[0].length) {
|
||||
throw new Error('Invalid number of time points returned');
|
||||
}
|
||||
|
||||
const timepointsCount = timePoints.length;
|
||||
const chartSeriesData = new Array(timepointsCount);
|
||||
|
||||
for (let i = 0; i < timepointsCount; i++) {
|
||||
const average = timeData.reduce((acc, cur) => acc + cur[i] / pixelCount, 0);
|
||||
|
||||
chartSeriesData[i] = [timePoints[i], average];
|
||||
}
|
||||
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
StudyDescription,
|
||||
chartData: {
|
||||
series: {
|
||||
label: segmentation.label,
|
||||
points: chartSeriesData,
|
||||
color: hexColor,
|
||||
},
|
||||
axis: {
|
||||
x: {
|
||||
label: `Time (${timePointsUnit})`,
|
||||
},
|
||||
y: {
|
||||
label: `Vl (Bq/ml)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function _getInstanceFromSegmentations(segmentations, displaySetService) {
|
||||
if (!segmentations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const volumesTimePointsCache = new WeakMap();
|
||||
const segmentationsData = segmentations.map(segmentation =>
|
||||
_getSegmentationData(segmentation, volumesTimePointsCache, displaySetService)
|
||||
);
|
||||
|
||||
const { date: seriesDate, time: seriesTime } = _getDateTimeStr();
|
||||
const series = segmentationsData.reduce((allSeries, curSegData) => {
|
||||
return [...allSeries, curSegData.chartData.series];
|
||||
}, []);
|
||||
|
||||
const instance = {
|
||||
SOPClassUID: ChartDataSOPClassUid,
|
||||
Modality: CHART_MODALITY,
|
||||
SOPInstanceUID: utils.guid(),
|
||||
SeriesDate: seriesDate,
|
||||
SeriesTime: seriesTime,
|
||||
SeriesInstanceUID: SEG_CHART_INSTANCE_UID,
|
||||
StudyInstanceUID: segmentationsData[0].StudyInstanceUID,
|
||||
StudyDescription: segmentationsData[0].StudyDescription,
|
||||
SeriesNumber: 100,
|
||||
SeriesDescription: 'Segmentation chart series data',
|
||||
chartData: {
|
||||
series,
|
||||
axis: { ...segmentationsData[0].chartData.axis },
|
||||
},
|
||||
};
|
||||
|
||||
const seriesMetadata = {
|
||||
StudyInstanceUID: instance.StudyInstanceUID,
|
||||
StudyDescription: instance.StudyDescription,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
SeriesDescription: instance.SeriesDescription,
|
||||
SeriesNumber: instance.SeriesNumber,
|
||||
SeriesTime: instance.SeriesTime,
|
||||
SOPClassUID: instance.SOPClassUID,
|
||||
Modality: instance.Modality,
|
||||
};
|
||||
|
||||
return { seriesMetadata, instance };
|
||||
}
|
||||
|
||||
function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void {
|
||||
const { segmentationService, displaySetService } = servicesManager.services;
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const { seriesMetadata, instance } =
|
||||
_getInstanceFromSegmentations(segmentations, displaySetService) ?? {};
|
||||
|
||||
if (seriesMetadata && instance) {
|
||||
// An event is triggered after adding the instance and the displaySet is created
|
||||
DicomMetadataStore.addSeriesMetadata([seriesMetadata], true);
|
||||
DicomMetadataStore.addInstances([instance], true);
|
||||
}
|
||||
}
|
||||
|
||||
export { updateSegmentationsChartDisplaySet as default };
|
||||
@@ -0,0 +1,407 @@
|
||||
import * as importedActions from './actions';
|
||||
import { utilities, Enums } from '@cornerstonejs/tools';
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
|
||||
const LABELMAP = Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
|
||||
const services = servicesManager.services;
|
||||
const { displaySetService, viewportGridService, segmentationService } = services;
|
||||
|
||||
const actions = {
|
||||
...importedActions,
|
||||
getDynamic4DDisplaySet: () => {
|
||||
const displaySets = displaySetService.getActiveDisplaySets();
|
||||
|
||||
const dynamic4DDisplaySet = displaySets.find(displaySet => {
|
||||
const anInstance = displaySet.instances?.[0];
|
||||
|
||||
if (anInstance) {
|
||||
return (
|
||||
anInstance.FrameReferenceTime !== undefined ||
|
||||
anInstance.NumberOfTimeSlices !== undefined ||
|
||||
anInstance.TemporalPositionIdentifier !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return dynamic4DDisplaySet;
|
||||
},
|
||||
getComputedDisplaySets: () => {
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
const cachedDisplaySets = [...displaySetCache.values()];
|
||||
const computedDisplaySets = cachedDisplaySets.filter(displaySet => {
|
||||
return displaySet.isDerived;
|
||||
});
|
||||
return computedDisplaySets;
|
||||
},
|
||||
exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => {
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID;
|
||||
|
||||
// cache._volumeCache is a map that has a key that includes the volumeId
|
||||
// it is not exactly the volumeId, but it is the key that includes the volumeId
|
||||
// so we can't do cache._volumeCache.get(volumeId) we should iterate
|
||||
// over the keys and find the one that includes the volumeId
|
||||
let volumeCacheKey: string | undefined;
|
||||
|
||||
for (const [key] of cache._volumeCache) {
|
||||
if (key.includes(volumeId)) {
|
||||
volumeCacheKey = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let dynamicVolume;
|
||||
if (volumeCacheKey) {
|
||||
dynamicVolume = cache.getVolume(volumeCacheKey);
|
||||
}
|
||||
|
||||
const instance = dynamic4DDisplaySet.instances[0];
|
||||
|
||||
const csv = [];
|
||||
|
||||
// CSV header information with placeholder empty values for the metadata lines
|
||||
csv.push(`Patient ID,${instance.PatientID},`);
|
||||
csv.push(`Study Date,${instance.StudyDate},`);
|
||||
csv.push(`StudyInstanceUID,${instance.StudyInstanceUID},`);
|
||||
csv.push(`StudyDescription,${instance.StudyDescription},`);
|
||||
csv.push(`SeriesInstanceUID,${instance.SeriesInstanceUID},`);
|
||||
|
||||
// empty line
|
||||
csv.push('');
|
||||
csv.push('');
|
||||
|
||||
// Helper function to calculate standard deviation
|
||||
function calculateStandardDeviation(data) {
|
||||
const n = data.length;
|
||||
const mean = data.reduce((acc, value) => acc + value, 0) / n;
|
||||
const squaredDifferences = data.map(value => (value - mean) ** 2);
|
||||
const variance = squaredDifferences.reduce((acc, value) => acc + value, 0) / n;
|
||||
const stdDeviation = Math.sqrt(variance);
|
||||
return stdDeviation;
|
||||
}
|
||||
|
||||
// Iterate through each segmentation to get the timeData and ijkCoords
|
||||
segmentations.forEach((segmentation, segmentationIndex) => {
|
||||
const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
|
||||
maskVolumeId: segmentation.id,
|
||||
}) as number[][];
|
||||
|
||||
if (summaryStats) {
|
||||
// Adding column headers for pixel identifier and segmentation label ids
|
||||
let headers = 'Operation,Segmentation Label ID';
|
||||
const maxLength = dynamicVolume.numTimePoints;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
headers += `,Time Point ${t}`;
|
||||
}
|
||||
csv.push(headers);
|
||||
// // perform summary statistics on the timeData including for each time point, mean, median, min, max, and standard deviation for
|
||||
// // all the voxels in the ROI
|
||||
const mean = [];
|
||||
const min = [];
|
||||
const minIJK = [];
|
||||
const max = [];
|
||||
const maxIJK = [];
|
||||
const std = [];
|
||||
|
||||
const numVoxels = timeData.length;
|
||||
// Helper function to calculate standard deviation
|
||||
for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
|
||||
// for each voxel in the ROI, get the value at the current time point
|
||||
const voxelValues = [];
|
||||
for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
|
||||
voxelValues.push(timeData[voxelIndex][timeIndex]);
|
||||
}
|
||||
|
||||
mean.push(voxelValues.reduce((acc, value) => acc + value, 0) / numVoxels);
|
||||
const minimum = Math.min(...voxelValues);
|
||||
min.push(minimum);
|
||||
minIJK.push(ijkCoords[voxelValues.indexOf(minimum)]);
|
||||
const maximum = Math.max(...voxelValues);
|
||||
max.push(maximum);
|
||||
maxIJK.push(ijkCoords[voxelValues.indexOf(maximum)]);
|
||||
std.push(calculateStandardDeviation(voxelValues));
|
||||
}
|
||||
|
||||
let row = `Mean,${segmentation.label}`;
|
||||
// Generate separate rows for each statistic
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${mean[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
|
||||
row = `Standard Deviation,${segmentation.label}`;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${std[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
|
||||
row = `Min,${segmentation.label}`;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${min[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
|
||||
row = `Max,${segmentation.label}`;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
row += `,${max[t]}`;
|
||||
}
|
||||
|
||||
csv.push(row);
|
||||
} else {
|
||||
// Adding column headers for pixel identifier and segmentation label ids
|
||||
let headers = 'Pixel Identifier (IJK),Segmentation Label ID';
|
||||
const maxLength = dynamicVolume.numTimePoints;
|
||||
for (let t = 0; t < maxLength; t++) {
|
||||
headers += `,Time Point ${t}`;
|
||||
}
|
||||
csv.push(headers);
|
||||
// Assuming timeData and ijkCoords are of the same length
|
||||
for (let i = 0; i < timeData.length; i++) {
|
||||
// Generate the pixel identifier
|
||||
const pixelIdentifier = `${ijkCoords[i][0]}_${ijkCoords[i][1]}_${ijkCoords[i][2]}`;
|
||||
|
||||
// Start a new row for the current pixel
|
||||
let row = `${pixelIdentifier},${segmentation.label}`;
|
||||
|
||||
// Add time data points for this pixel
|
||||
for (let t = 0; t < timeData[i].length; t++) {
|
||||
row += `,${timeData[i][t]}`;
|
||||
}
|
||||
|
||||
// Append the row to the CSV array
|
||||
csv.push(row);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Convert to CSV string
|
||||
const csvContent = csv.join('\n');
|
||||
|
||||
// Generate filename and trigger download
|
||||
const filename = `${instance.PatientID}.csv`;
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
swapDynamicWithComputedDisplaySet: ({ displaySet }) => {
|
||||
const computedDisplaySet = displaySet;
|
||||
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
const cachedDisplaySetKeys = [displaySetCache.keys()];
|
||||
const { displaySetInstanceUID } = computedDisplaySet;
|
||||
// Check to see if computed display set is already in cache
|
||||
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
||||
displaySetCache.set(displaySetInstanceUID, computedDisplaySet);
|
||||
}
|
||||
|
||||
// Get all viewports and their corresponding indices
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
// get the viewports in the grid
|
||||
// iterate over them and find the ones that are showing a dynamic
|
||||
// volume (displaySet), and replace that exact displaySet with the
|
||||
// computed displaySet
|
||||
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const viewportsToUpdate = [];
|
||||
|
||||
for (const [key, value] of viewports) {
|
||||
const viewport = value;
|
||||
const viewportOptions = viewport.viewportOptions;
|
||||
const { displaySetInstanceUIDs } = viewport;
|
||||
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
||||
dynamic4DDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
if (displaySetInstanceUIDIndex !== -1) {
|
||||
const newViewport = {
|
||||
viewportId: viewport.viewportId,
|
||||
// merge the other displaySetInstanceUIDs with the new one
|
||||
displaySetInstanceUIDs: [
|
||||
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
||||
displaySetInstanceUID,
|
||||
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
||||
],
|
||||
viewportOptions: {
|
||||
initialImageOptions: viewportOptions.initialImageOptions,
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
background: viewportOptions.background,
|
||||
},
|
||||
};
|
||||
viewportsToUpdate.push(newViewport);
|
||||
}
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||
},
|
||||
swapComputedWithDynamicDisplaySet: () => {
|
||||
// Todo: this assumes there is only one dynamic display set in the viewer
|
||||
const dynamicDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
|
||||
const displaySetCache = displaySetService.getDisplaySetCache();
|
||||
const cachedDisplaySetKeys = [...displaySetCache.keys()]; // Fix: Spread to get the array
|
||||
const { displaySetInstanceUID } = dynamicDisplaySet;
|
||||
|
||||
// Check to see if dynamic display set is already in cache
|
||||
if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
|
||||
displaySetCache.set(displaySetInstanceUID, dynamicDisplaySet);
|
||||
}
|
||||
|
||||
// Get all viewports and their corresponding indices
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
// Get the computed 4D display set
|
||||
const computed4DDisplaySet = actions.getComputedDisplaySets()[0];
|
||||
|
||||
const viewportsToUpdate = [];
|
||||
|
||||
for (const [key, value] of viewports) {
|
||||
const viewport = value;
|
||||
const viewportOptions = viewport.viewportOptions;
|
||||
const { displaySetInstanceUIDs } = viewport;
|
||||
const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
|
||||
computed4DDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
if (displaySetInstanceUIDIndex !== -1) {
|
||||
const newViewport = {
|
||||
viewportId: viewport.viewportId,
|
||||
// merge the other displaySetInstanceUIDs with the new one
|
||||
displaySetInstanceUIDs: [
|
||||
...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
|
||||
displaySetInstanceUID,
|
||||
...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
|
||||
],
|
||||
viewportOptions: {
|
||||
initialImageOptions: viewportOptions.initialImageOptions,
|
||||
viewportType: 'volume',
|
||||
orientation: viewportOptions.orientation,
|
||||
background: viewportOptions.background,
|
||||
},
|
||||
};
|
||||
viewportsToUpdate.push(newViewport);
|
||||
}
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
|
||||
},
|
||||
createNewLabelMapForDynamicVolume: async ({ label }) => {
|
||||
const { viewports, activeViewportId } = viewportGridService.getState();
|
||||
|
||||
// get the dynamic 4D display set
|
||||
const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
|
||||
const dynamic4DDisplaySetInstanceUID = dynamic4DDisplaySet.displaySetInstanceUID;
|
||||
|
||||
// check if the dynamic 4D display set is in the display, if not we might have
|
||||
// the computed volumes and we should choose them for the segmentation
|
||||
// creation
|
||||
|
||||
let referenceDisplaySet;
|
||||
|
||||
const activeViewport = viewports.get(activeViewportId);
|
||||
const activeDisplaySetInstanceUIDs = activeViewport.displaySetInstanceUIDs;
|
||||
const dynamicIsInActiveViewport = activeDisplaySetInstanceUIDs.includes(
|
||||
dynamic4DDisplaySetInstanceUID
|
||||
);
|
||||
|
||||
if (dynamicIsInActiveViewport) {
|
||||
referenceDisplaySet = dynamic4DDisplaySet;
|
||||
}
|
||||
|
||||
if (!referenceDisplaySet) {
|
||||
// try to see if there is any derived displaySet in the active viewport
|
||||
// which is referencing the dynamic 4D display set
|
||||
|
||||
// Todo: this is wrong but I don't have time to fix it now
|
||||
const cachedDisplaySets = displaySetService.getDisplaySetCache();
|
||||
for (const [key, displaySet] of cachedDisplaySets) {
|
||||
if (displaySet.referenceDisplaySetUID === dynamic4DDisplaySetInstanceUID) {
|
||||
referenceDisplaySet = displaySet;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!referenceDisplaySet) {
|
||||
throw new Error('No reference display set found based on the dynamic data');
|
||||
}
|
||||
|
||||
const segmentationId = await segmentationService.createSegmentationForDisplaySet(
|
||||
referenceDisplaySet.displaySetInstanceUID,
|
||||
{ label }
|
||||
);
|
||||
|
||||
// Add Segmentation to all toolGroupIds in the viewer
|
||||
const toolGroupIds = Array.from(
|
||||
viewports.values(),
|
||||
viewport => viewport.viewportOptions.toolGroupId
|
||||
);
|
||||
|
||||
const representationType = LABELMAP;
|
||||
|
||||
for (const toolGroupId of toolGroupIds) {
|
||||
const hydrateSegmentation = true;
|
||||
await segmentationService.addSegmentationRepresentationToToolGroup(
|
||||
toolGroupId,
|
||||
segmentationId,
|
||||
hydrateSegmentation,
|
||||
representationType
|
||||
);
|
||||
|
||||
segmentationService.setActiveSegmentationForToolGroup(segmentationId, toolGroupId);
|
||||
}
|
||||
|
||||
return segmentationId;
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
updateSegmentationsChartDisplaySet: {
|
||||
commandFn: actions.updateSegmentationsChartDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
exportTimeReportCSV: {
|
||||
commandFn: actions.exportTimeReportCSV,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
swapDynamicWithComputedDisplaySet: {
|
||||
commandFn: actions.swapDynamicWithComputedDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
createNewLabelMapForDynamicVolume: {
|
||||
commandFn: actions.createNewLabelMapForDynamicVolume,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
swapComputedWithDynamicDisplaySet: {
|
||||
commandFn: actions.swapComputedWithDynamicDisplaySet,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
actions,
|
||||
definitions,
|
||||
defaultContext: 'DYNAMIC-VOLUME:CORNERSTONE',
|
||||
};
|
||||
};
|
||||
|
||||
export default commandsModule;
|
||||
@@ -0,0 +1,654 @@
|
||||
const DEFAULT_COLORMAP = '2hot';
|
||||
const toolGroupIds = {
|
||||
pt: 'dynamic4D-pt',
|
||||
fusion: 'dynamic4D-fusion',
|
||||
ct: 'dynamic4D-ct',
|
||||
};
|
||||
|
||||
function getPTOptions({
|
||||
colormap,
|
||||
voiInverted,
|
||||
}: {
|
||||
colormap?: {
|
||||
name: string;
|
||||
opacity:
|
||||
| number
|
||||
| {
|
||||
value: number;
|
||||
opacity: number;
|
||||
}[];
|
||||
};
|
||||
voiInverted?: boolean;
|
||||
} = {}) {
|
||||
return {
|
||||
blendMode: 'MIP',
|
||||
colormap,
|
||||
voi: {
|
||||
windowWidth: 5,
|
||||
windowCenter: 2.5,
|
||||
},
|
||||
voiInverted,
|
||||
};
|
||||
}
|
||||
|
||||
function getPTViewports() {
|
||||
const ptOptionsParams = {
|
||||
colormap: {
|
||||
name: DEFAULT_COLORMAP,
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 1 },
|
||||
{ value: 1, opacity: 1 },
|
||||
],
|
||||
},
|
||||
voiInverted: false,
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ptAxial',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: toolGroupIds.pt,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ptSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: toolGroupIds.pt,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ptCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: toolGroupIds.pt,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getFusionViewports() {
|
||||
const ptOptionsParams = {
|
||||
colormap: {
|
||||
name: DEFAULT_COLORMAP,
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.3 },
|
||||
{ value: 1, opacity: 0.3 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionAxial',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: toolGroupIds.fusion,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: toolGroupIds.fusion,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: toolGroupIds.fusion,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
options: { ...getPTOptions(ptOptionsParams) },
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getSeriesChartViewport() {
|
||||
return {
|
||||
viewportOptions: {
|
||||
viewportId: 'seriesChart',
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'chartDisplaySet',
|
||||
options: {
|
||||
// This dataset does not require the download of any instance since it is pre-computed locally,
|
||||
// but interleaveTopToBottom.ts was not loading any series because it consider that all viewports
|
||||
// are a Cornerstone viewport which is not true in this case and it waits for all viewports to
|
||||
// have called interleaveTopToBottom(...).
|
||||
skipLoading: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getCTViewports() {
|
||||
return [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ctAxial',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: toolGroupIds.ct,
|
||||
initialImageOptions: {
|
||||
preset: 'middle', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'axialSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ctSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: toolGroupIds.ct,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'sagittalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'ctCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: toolGroupIds.ct,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'cameraPosition',
|
||||
id: 'coronalSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const defaultProtocol = {
|
||||
id: 'default4D',
|
||||
locked: true,
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
modifiedDate: '2023-01-01T00:00:00.000Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
imageLoadStrategy: 'default', // "default" , "interleaveTopToBottom", "interleaveCenter"
|
||||
protocolMatchingRules: [
|
||||
{
|
||||
attribute: 'ModalitiesInStudy',
|
||||
constraint: {
|
||||
contains: ['CT', 'PT'],
|
||||
},
|
||||
},
|
||||
],
|
||||
// -1 would be used to indicate active only, whereas other values are
|
||||
// the number of required priors referenced - so 0 means active with
|
||||
// 0 or more priors.
|
||||
numberOfPriorsReferenced: -1,
|
||||
displaySetSelectors: {
|
||||
defaultDisplaySetId: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
// Try to match series with images by default, to prevent weird display
|
||||
// on SEG/SR containing studies
|
||||
{
|
||||
attribute: 'numImageFrames',
|
||||
constraint: {
|
||||
greaterThan: { value: 0 },
|
||||
},
|
||||
},
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
ctDisplaySet: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: 'CT',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
ptDisplaySet: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: 'PT',
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
contains: 'Corrected',
|
||||
},
|
||||
},
|
||||
{
|
||||
weight: 2,
|
||||
attribute: 'SeriesDescription',
|
||||
constraint: {
|
||||
doesNotContain: {
|
||||
value: 'Uncorrected',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Should we check if CorrectedImage contains ATTN?
|
||||
// (0028,0051) (CorrectedImage): NORM\DTIM\ATTN\SCAT\RADL\DECY
|
||||
],
|
||||
// Can be used to select matching studies
|
||||
// studyMatchingRules: [],
|
||||
},
|
||||
chartDisplaySet: {
|
||||
// Unused currently
|
||||
imageMatchingRules: [],
|
||||
// Matches displaysets, NOT series
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
attribute: 'Modality',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: 'CHT',
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
id: 'dataPreparation',
|
||||
name: 'Data Preparation',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
viewports: [...getPTViewports()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'registration',
|
||||
name: 'Registration',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 3,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
viewports: [...getFusionViewports(), ...getCTViewports(), ...getPTViewports()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'roiQuantification',
|
||||
name: 'ROI Quantification',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 3,
|
||||
},
|
||||
},
|
||||
viewports: [...getFusionViewports()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'kineticAnalysis',
|
||||
name: 'Kinetic Analysis',
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 2,
|
||||
columns: 3,
|
||||
layoutOptions: [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1 / 2,
|
||||
},
|
||||
{
|
||||
x: 1 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1 / 2,
|
||||
},
|
||||
{
|
||||
x: 2 / 3,
|
||||
y: 0,
|
||||
width: 1 / 3,
|
||||
height: 1 / 2,
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: 1 / 2,
|
||||
width: 1,
|
||||
height: 1 / 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
viewports: [...getFusionViewports(), getSeriesChartViewport()],
|
||||
createdDate: '2023-01-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* HangingProtocolModule should provide a list of hanging protocols that will be
|
||||
* available in OHIF for Modes to use to decide on the structure of the viewports
|
||||
* and also the series that hung in the viewports. Each hanging protocol is defined by
|
||||
* { name, protocols}. Examples include the default hanging protocol provided by
|
||||
* the default extension that shows 2x2 viewports.
|
||||
*/
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: defaultProtocol.id,
|
||||
protocol: defaultProtocol,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getHangingProtocolModule;
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { DynamicDataPanel } from './panels';
|
||||
import { Toolbox } from '@ohif/ui';
|
||||
import DynamicExport from './panels/DynamicExport';
|
||||
|
||||
function getPanelModule({ commandsManager, extensionManager, servicesManager }) {
|
||||
const wrappedDynamicDataPanel = () => {
|
||||
return (
|
||||
<DynamicDataPanel
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicToolbox = () => {
|
||||
return (
|
||||
<>
|
||||
<Toolbox
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
buttonSectionId="dynamic-toolbox"
|
||||
title="Threshold Tools"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const wrappedDynamicExport = () => {
|
||||
return (
|
||||
<>
|
||||
<DynamicExport
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'dynamic-volume',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: '4D Workflow',
|
||||
component: wrappedDynamicDataPanel,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-toolbox',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: 'Dynamic Toolbox',
|
||||
component: wrappedDynamicToolbox,
|
||||
},
|
||||
{
|
||||
name: 'dynamic-export',
|
||||
iconName: 'tab-4d',
|
||||
iconLabel: '4D Workflow',
|
||||
label: '4D Workflow',
|
||||
component: wrappedDynamicExport,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default getPanelModule;
|
||||
@@ -0,0 +1,6 @@
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const id = packageJson.name;
|
||||
const SOPClassHandlerName = 'dynamic-volume';
|
||||
|
||||
export { id, SOPClassHandlerName };
|
||||
@@ -0,0 +1,57 @@
|
||||
import { id } from './id';
|
||||
import commandsModule from './commandsModule';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import { cache } from '@cornerstonejs/core';
|
||||
|
||||
/**
|
||||
* You can remove any of the following modules if you don't need them.
|
||||
*/
|
||||
const dynamicVolumeExtension = {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
* You ID can be anything you want, but it should be unique.
|
||||
*/
|
||||
id,
|
||||
|
||||
/**
|
||||
* Perform any pre-registration tasks here. This is called before the extension
|
||||
* is registered. Usually we run tasks such as: configuring the libraries
|
||||
* (e.g. cornerstone, cornerstoneTools, ...) or registering any services that
|
||||
* this extension is providing.
|
||||
*/
|
||||
preRegistration: ({ servicesManager, commandsManager, configuration = {} }) => {
|
||||
// TODO: look for the right fix
|
||||
cache.setMaxCacheSize(5 * 1024 * 1024 * 1024);
|
||||
},
|
||||
/**
|
||||
* PanelModule should provide a list of panels that will be available in OHIF
|
||||
* for Modes to consume and render. Each panel is defined by a {name,
|
||||
* iconName, iconLabel, label, component} object. Example of a panel module
|
||||
* is the StudyBrowserPanel that is provided by the default extension in OHIF.
|
||||
*/
|
||||
getPanelModule,
|
||||
/**
|
||||
* ViewportModule should provide a list of viewports that will be available in OHIF
|
||||
* for Modes to consume and use in the viewports. Each viewport is defined by
|
||||
* {name, component} object. Example of a viewport module is the CornerstoneViewport
|
||||
* that is provided by the Cornerstone extension in OHIF.
|
||||
*/
|
||||
getHangingProtocolModule,
|
||||
/**
|
||||
* CommandsModule should provide a list of commands that will be available in OHIF
|
||||
* for Modes to consume and use in the viewports. Each command is defined by
|
||||
* an object of { actions, definitions, defaultContext } where actions is an
|
||||
* object of functions, definitions is an object of available commands, their
|
||||
* options, and defaultContext is the default context for the command to run against.
|
||||
*/
|
||||
getCommandsModule: ({ servicesManager, commandsManager, extensionManager }) => {
|
||||
return commandsModule({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
extensionManager,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export { dynamicVolumeExtension as default };
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import PanelGenerateImage from './PanelGenerateImage';
|
||||
|
||||
function DynamicDataPanel({ servicesManager, commandsManager }) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col text-white"
|
||||
data-cy={'dynamic-volume-panel'}
|
||||
>
|
||||
<PanelGenerateImage
|
||||
commandsManager={commandsManager}
|
||||
servicesManager={servicesManager}
|
||||
></PanelGenerateImage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicDataPanel;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ActionButtons } from '@ohif/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function DynamicExport({ commandsManager, servicesManager, extensionManager }: withAppTypes) {
|
||||
const { segmentationService } = servicesManager.services;
|
||||
const { t } = useTranslation('dynamicExport');
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
const actions = [
|
||||
{
|
||||
label: 'Export Time Data',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
options: {
|
||||
filename: 'TimeData.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
{
|
||||
label: 'Export ROI Stats',
|
||||
onClick: () => {
|
||||
commandsManager.runCommand('exportTimeReportCSV', {
|
||||
segmentations,
|
||||
summaryStats: true,
|
||||
options: {
|
||||
filename: 'ROIStats.csv',
|
||||
},
|
||||
});
|
||||
},
|
||||
disabled: !segmentations?.length,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Update UI based on segmentation changes (added, removed, updated)
|
||||
*/
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const added = segmentationService.EVENTS.SEGMENTATION_ADDED;
|
||||
const updated = segmentationService.EVENTS.SEGMENTATION_UPDATED;
|
||||
const removed = segmentationService.EVENTS.SEGMENTATION_REMOVED;
|
||||
const subscriptions = [];
|
||||
|
||||
[added, updated, removed].forEach(evt => {
|
||||
const { unsubscribe } = segmentationService.subscribe(evt, () => {
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
setSegmentations(segmentations);
|
||||
});
|
||||
subscriptions.push(unsubscribe);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-3 flex justify-center px-2">
|
||||
<ActionButtons
|
||||
actions={actions}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicExport;
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
InputDoubleRange,
|
||||
Button,
|
||||
PanelSection,
|
||||
ButtonGroup,
|
||||
IconButton,
|
||||
InputNumber,
|
||||
Icon,
|
||||
Tooltip,
|
||||
} from '@ohif/ui';
|
||||
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
|
||||
const controlClassNames = {
|
||||
sizeClassName: 'w-[58px] h-[28px]',
|
||||
arrowsDirection: 'horizontal',
|
||||
labelPosition: 'bottom',
|
||||
};
|
||||
|
||||
const Header = ({ title, tooltip }) => (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Tooltip
|
||||
content={<div className="text-white">{tooltip}</div>}
|
||||
position="bottom-left"
|
||||
tight={true}
|
||||
tooltipBoxClassName="max-w-xs p-2"
|
||||
>
|
||||
<Icon
|
||||
name="info-link"
|
||||
className="text-primary-active h-[14px] w-[14px]"
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className="text-aqua-pale text-[11px] uppercase">{title}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const DynamicVolumeControls = ({
|
||||
isPlaying,
|
||||
onPlayPauseChange,
|
||||
// fps
|
||||
fps,
|
||||
onFpsChange,
|
||||
minFps,
|
||||
maxFps,
|
||||
// Frames
|
||||
currentFrameIndex,
|
||||
onFrameChange,
|
||||
framesLength,
|
||||
onGenerate,
|
||||
onDoubleRangeChange,
|
||||
onDynamicClick,
|
||||
}) => {
|
||||
const [computedView, setComputedView] = useState(false);
|
||||
|
||||
const [computeViewMode, setComputeViewMode] = useState(Enums.DynamicOperatorType.SUM);
|
||||
|
||||
const [sliderRangeValues, setSliderRangeValues] = useState([framesLength / 4, framesLength / 2]);
|
||||
|
||||
useEffect(() => {
|
||||
setSliderRangeValues([framesLength / 4, framesLength / 2]);
|
||||
}, [framesLength]);
|
||||
|
||||
const handleSliderChange = newValues => {
|
||||
onDoubleRangeChange(newValues);
|
||||
|
||||
if (newValues[0] === sliderRangeValues[0] && newValues[1] === sliderRangeValues[1]) {
|
||||
return;
|
||||
}
|
||||
setSliderRangeValues(newValues);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex select-none flex-col">
|
||||
<PanelSection
|
||||
title="Controls"
|
||||
childrenClassName="space-y-4 pb-5 px-5"
|
||||
>
|
||||
<div className="mt-2">
|
||||
<Header
|
||||
title="View"
|
||||
tooltip={
|
||||
'Select the view mode, 4D to view the dynamic volume or Computed to view the computed volume'
|
||||
}
|
||||
/>
|
||||
<ButtonGroup className="mt-2 w-full">
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(false);
|
||||
onDynamicClick?.();
|
||||
}}
|
||||
>
|
||||
4D
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => {
|
||||
setComputedView(true);
|
||||
}}
|
||||
>
|
||||
Computed
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div>
|
||||
<FrameControls
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
isPlaying={isPlaying}
|
||||
computedView={computedView}
|
||||
// fps
|
||||
fps={fps}
|
||||
onFpsChange={onFpsChange}
|
||||
minFps={minFps}
|
||||
maxFps={maxFps}
|
||||
//
|
||||
framesLength={framesLength}
|
||||
onFrameChange={onFrameChange}
|
||||
currentFrameIndex={currentFrameIndex}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-6 flex flex-col ${computedView ? '' : 'ohif-disabled'}`}>
|
||||
<Header
|
||||
title="Computed Operation"
|
||||
tooltip={
|
||||
<div>
|
||||
Operation Buttons (SUM, AVERAGE, SUBTRACT): Select the mathematical operation to be
|
||||
applied to the data set.
|
||||
<br></br> Range Slider: Choose the numeric range within which the operation will be
|
||||
performed.
|
||||
<br></br>Generate Button: Execute the chosen operation on the specified range of
|
||||
data.{' '}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ButtonGroup
|
||||
className={`mt-2 w-full `}
|
||||
separated={true}
|
||||
>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUM)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUM.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.AVERAGE)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.AVERAGE.toString().toUpperCase()}
|
||||
</button>
|
||||
<button
|
||||
className="w-1/2"
|
||||
onClick={() => setComputeViewMode(Enums.DynamicOperatorType.SUBTRACT)}
|
||||
>
|
||||
{Enums.DynamicOperatorType.SUBTRACT.toString().toUpperCase()}
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
<div className="w-ful mt-2">
|
||||
<InputDoubleRange
|
||||
values={sliderRangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={0}
|
||||
showLabel={true}
|
||||
allowNumberEdit={true}
|
||||
maxValue={framesLength}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2 !h-[26px] !w-[115px] self-start !p-0"
|
||||
onClick={() => {
|
||||
onGenerate(computeViewMode);
|
||||
}}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</PanelSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DynamicVolumeControls;
|
||||
|
||||
function FrameControls({
|
||||
isPlaying,
|
||||
onPlayPauseChange,
|
||||
fps,
|
||||
minFps,
|
||||
maxFps,
|
||||
onFpsChange,
|
||||
framesLength,
|
||||
onFrameChange,
|
||||
currentFrameIndex,
|
||||
computedView,
|
||||
}) {
|
||||
const getPlayPauseIconName = () => (isPlaying ? 'icon-pause' : 'icon-play');
|
||||
|
||||
return (
|
||||
<div className={computedView && 'ohif-disabled'}>
|
||||
<Header
|
||||
title="4D Controls"
|
||||
tooltip={
|
||||
<div>
|
||||
Play/Pause Button: Begin or pause the animation of the 4D visualization. <br></br> Frame
|
||||
Selector: Navigate through individual frames of the 4D data. <br></br> FPS (Frames Per
|
||||
Second) Selector: Adjust the playback speed of the animation.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="mt-3 flex justify-between">
|
||||
<IconButton
|
||||
className="bg-customblue-30 h-[26px] w-[58px] rounded-[4px]"
|
||||
onClick={() => onPlayPauseChange(!isPlaying)}
|
||||
>
|
||||
<Icon
|
||||
name={getPlayPauseIconName()}
|
||||
className=" active:text-primary-light hover:bg-customblue-300 h-[24px] w-[24px] cursor-pointer text-white"
|
||||
/>
|
||||
</IconButton>
|
||||
<InputNumber
|
||||
value={currentFrameIndex}
|
||||
onChange={onFrameChange}
|
||||
minValue={0}
|
||||
maxValue={framesLength}
|
||||
label="Frame"
|
||||
{...controlClassNames}
|
||||
/>
|
||||
<InputNumber
|
||||
value={fps}
|
||||
onChange={onFpsChange}
|
||||
minValue={minFps}
|
||||
maxValue={maxFps}
|
||||
{...controlClassNames}
|
||||
label="FPS"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { InputDoubleRange } from '@ohif/ui';
|
||||
import { Select } from '@ohif/ui';
|
||||
import { Button } from '@ohif/ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const GenerateVolume = ({
|
||||
rangeValues,
|
||||
handleSliderChange,
|
||||
operationsUI,
|
||||
options,
|
||||
handleGenerateOptionsChange,
|
||||
onGenerateImage,
|
||||
returnTo4D,
|
||||
displayingComputedVolume,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="mb-2 text-white">Computed Image</div>
|
||||
<Select
|
||||
closeMenuOnSelect={true}
|
||||
className="border-primary-main mr-2 bg-black text-white "
|
||||
options={operationsUI}
|
||||
placeholder={operationsUI.find(option => option.value === options.Operation).placeHolder}
|
||||
value={options.Operation}
|
||||
onChange={({ value }) => {
|
||||
handleGenerateOptionsChange({
|
||||
Operation: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<InputDoubleRange
|
||||
values={rangeValues}
|
||||
onChange={handleSliderChange}
|
||||
minValue={rangeValues[0] || 1}
|
||||
maxValue={rangeValues[1] || 2}
|
||||
showLabel={true}
|
||||
step={1}
|
||||
/>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
onClick={onGenerateImage}
|
||||
className="w-1/2"
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
<Button
|
||||
onClick={returnTo4D}
|
||||
disabled={!displayingComputedVolume}
|
||||
className="w-1/2"
|
||||
>
|
||||
Return To 4D
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
GenerateVolume.propTypes = {
|
||||
rangeValues: PropTypes.array.isRequired,
|
||||
handleSliderChange: PropTypes.func.isRequired,
|
||||
operationsUI: PropTypes.array.isRequired,
|
||||
options: PropTypes.object.isRequired,
|
||||
handleGenerateOptionsChange: PropTypes.func.isRequired,
|
||||
onGenerateImage: PropTypes.func.isRequired,
|
||||
returnTo4D: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default GenerateVolume;
|
||||
@@ -0,0 +1,275 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||
import { cache, utilities as csUtils, volumeLoader, eventTarget } from '@cornerstonejs/core';
|
||||
import { Enums } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||
import DynamicVolumeControls from './DynamicVolumeControls';
|
||||
|
||||
const SOPClassHandlerId = '@ohif/extension-default.sopClassHandlerModule.stack';
|
||||
|
||||
export default function PanelGenerateImage({ servicesManager, commandsManager }: withAppTypes) {
|
||||
const { cornerstoneViewportService, viewportGridService, displaySetService } =
|
||||
servicesManager.services;
|
||||
|
||||
const [{ isCineEnabled }, cineService] = useCine();
|
||||
const [{ activeViewportId }] = useViewportGrid();
|
||||
|
||||
//
|
||||
const [timePointsRange, setTimePointsRange] = useState([]);
|
||||
const [timePointsRangeToUseForGenerate, setTimePointsRangeToUseForGenerate] = useState([]);
|
||||
const [computedDisplaySet, setComputedDisplaySet] = useState(null);
|
||||
const [dynamicVolume, setDynamicVolume] = useState(null);
|
||||
const [frameRate, setFrameRate] = useState(20);
|
||||
const [isPlaying, setIsPlaying] = useState(isCineEnabled);
|
||||
const [timePointRendered, setTimePointRendered] = useState(null);
|
||||
const [displayingComputed, setDisplayingComputed] = useState(false);
|
||||
|
||||
//
|
||||
const uuidComputedVolume = useRef(csUtils.uuidv4());
|
||||
const uuidDynamicVolume = useRef(null);
|
||||
const computedVolumeId = `cornerstoneStreamingImageVolume:${uuidComputedVolume.current}`;
|
||||
|
||||
useEffect(() => {
|
||||
const evt = cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED;
|
||||
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(evt, evtDetails => {
|
||||
evtDetails.viewportData.data.forEach(volumeData => {
|
||||
if (volumeData.volume.isDynamicVolume()) {
|
||||
setDynamicVolume(volumeData.volume);
|
||||
uuidDynamicVolume.current = volumeData.displaySetInstanceUID;
|
||||
setTimePointsRange([1, volumeData.volume.numTimePoints]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = servicesManager.services.cineService.subscribe(
|
||||
servicesManager.services.cineService.EVENTS.CINE_STATE_CHANGED,
|
||||
evt => {
|
||||
setIsPlaying(evt.isPlaying);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [cineService]);
|
||||
|
||||
useEffect(() => {
|
||||
const displaySetUIDs = viewportGridService.getDisplaySetsUIDsForViewport(activeViewportId);
|
||||
|
||||
if (!displaySetUIDs || displaySetUIDs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySets = displaySetUIDs.map(displaySetUID =>
|
||||
displaySetService.getDisplaySetByUID(displaySetUID)
|
||||
);
|
||||
|
||||
const dynamicVolumeDisplaySet = displaySets.find(displaySet => displaySet.isDynamicVolume);
|
||||
|
||||
if (!dynamicVolumeDisplaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dynamicVolume = cache
|
||||
.getVolumes()
|
||||
.find(volume => volume.volumeId.includes(dynamicVolumeDisplaySet.displaySetInstanceUID));
|
||||
|
||||
if (!dynamicVolume) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDynamicVolume(dynamicVolume);
|
||||
uuidDynamicVolume.current = dynamicVolumeDisplaySet.displaySetInstanceUID;
|
||||
setTimePointsRange([1, dynamicVolume.numTimePoints]);
|
||||
}, [activeViewportId, cornerstoneViewportService]);
|
||||
|
||||
useEffect(() => {
|
||||
// ~~ Subscription
|
||||
const evt = Enums.Events.DYNAMIC_VOLUME_TIME_POINT_INDEX_CHANGED;
|
||||
|
||||
const callback = evt => {
|
||||
setTimePointRendered(evt.detail.timePointIndex);
|
||||
};
|
||||
|
||||
eventTarget.addEventListener(evt, callback);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener(evt, callback);
|
||||
};
|
||||
}, [cornerstoneViewportService]);
|
||||
|
||||
function renderGeneratedImage(displaySet) {
|
||||
commandsManager.runCommand('swapDynamicWithComputedDisplaySet', {
|
||||
displaySet,
|
||||
});
|
||||
|
||||
setDisplayingComputed(true);
|
||||
}
|
||||
|
||||
function renderDynamicImage(displaySet) {
|
||||
commandsManager.runCommand('swapComputedWithDynamicDisplaySet');
|
||||
}
|
||||
|
||||
// Get computed volume from cache, calculate the data across the time frames,
|
||||
// set the scalar data to the computedVolume, and create displaySet
|
||||
async function onGenerateImage(operationName) {
|
||||
const dynamicVolumeId = dynamicVolume.volumeId;
|
||||
|
||||
if (!dynamicVolumeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let computedVolume = cache.getVolume(computedVolumeId);
|
||||
|
||||
if (!computedVolume) {
|
||||
await createComputedVolume(dynamicVolumeId, computedVolumeId);
|
||||
computedVolume = cache.getVolume(computedVolumeId);
|
||||
}
|
||||
|
||||
const vals = timePointsRangeToUseForGenerate;
|
||||
|
||||
const targets = Array.from({ length: vals[1] - vals[0] + 1 }, (_, i) => i + vals[0]);
|
||||
|
||||
const dataInTime = cstUtils.dynamicVolume.generateImageFromTimeData(
|
||||
dynamicVolume,
|
||||
operationName,
|
||||
operationName === 'SUBTRACT' ? vals : targets
|
||||
);
|
||||
|
||||
// Add loadStatus.loaded to computed volume and set to true
|
||||
computedVolume.loadStatus = {};
|
||||
computedVolume.loadStatus.loaded = true;
|
||||
// Set computed scalar data to volume
|
||||
const scalarData = computedVolume.getScalarData();
|
||||
scalarData.set(dataInTime);
|
||||
|
||||
// If computed display set does not exist, create an object to be used as
|
||||
// the displaySet. If it does exist, update the image data and vtkTexture
|
||||
if (!computedDisplaySet) {
|
||||
const displaySet = {
|
||||
volumeLoaderSchema: computedVolume.volumeId.split(':')[0],
|
||||
displaySetInstanceUID: uuidComputedVolume.current,
|
||||
SOPClassHandlerId: SOPClassHandlerId,
|
||||
Modality: dynamicVolume.metadata.Modality,
|
||||
isMultiFrame: false,
|
||||
numImageFrames: 1,
|
||||
uid: uuidComputedVolume.current,
|
||||
referenceDisplaySetUID: dynamicVolume.volumeId.split(':')[1],
|
||||
madeInClient: true,
|
||||
FrameOfReferenceUID: dynamicVolume.metadata.FrameOfReferenceUID,
|
||||
isDerived: true,
|
||||
};
|
||||
setComputedDisplaySet(displaySet);
|
||||
renderGeneratedImage(displaySet);
|
||||
} else {
|
||||
commandsManager.runCommand('updateVolumeData', {
|
||||
volume: computedVolume,
|
||||
});
|
||||
// Check if viewport is currently displaying the computed volume, if so,
|
||||
// call render on the viewports to update the image, if not, call
|
||||
// renderGeneratedImage
|
||||
// if (!cache.getVolume(dynamicVolumeId)) {
|
||||
// for (const viewportId of viewports.keys()) {
|
||||
// const viewportForRendering =
|
||||
// cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
// viewportForRendering.render();
|
||||
// }
|
||||
// } else {
|
||||
cornerstoneViewportService.getRenderingEngine().render();
|
||||
renderGeneratedImage(computedDisplaySet);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
const onPlayPauseChange = isPlaying => {
|
||||
isPlaying ? handlePlay() : handleStop();
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
setIsPlaying(true);
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = viewportInfo;
|
||||
cineService.playClip(element, { framesPerSecond: frameRate, viewportId: activeViewportId });
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
setIsPlaying(false);
|
||||
const { element } = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
cineService.stopClip(element);
|
||||
};
|
||||
|
||||
const handleSetFrameRate = newFrameRate => {
|
||||
setFrameRate(newFrameRate);
|
||||
handleStop();
|
||||
handlePlay();
|
||||
};
|
||||
|
||||
function handleSliderChange(newValues) {
|
||||
if (
|
||||
newValues[0] === timePointsRangeToUseForGenerate[0] &&
|
||||
newValues[1] === timePointsRangeToUseForGenerate[1]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimePointsRangeToUseForGenerate(newValues);
|
||||
}
|
||||
|
||||
if (!dynamicVolume || timePointsRange.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DynamicVolumeControls
|
||||
fps={frameRate}
|
||||
isPlaying={isPlaying}
|
||||
onPlayPauseChange={onPlayPauseChange}
|
||||
minFps={1}
|
||||
maxFps={50}
|
||||
currentFrameIndex={timePointRendered}
|
||||
onFpsChange={handleSetFrameRate}
|
||||
framesLength={timePointsRange[1]}
|
||||
onFrameChange={timePointIndex => {
|
||||
dynamicVolume.timePointIndex = timePointIndex;
|
||||
}}
|
||||
onGenerate={onGenerateImage}
|
||||
onDynamicClick={displayingComputed ? () => renderDynamicImage(computedDisplaySet) : null}
|
||||
onDoubleRangeChange={handleSliderChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function createComputedVolume(dynamicVolumeId, computedVolumeId) {
|
||||
if (!cache.getVolume(computedVolumeId)) {
|
||||
const computedVolume = await volumeLoader.createAndCacheDerivedVolume(dynamicVolumeId, {
|
||||
volumeId: computedVolumeId,
|
||||
});
|
||||
return computedVolume;
|
||||
}
|
||||
}
|
||||
|
||||
PanelGenerateImage.propTypes = {
|
||||
servicesManager: PropTypes.shape({
|
||||
services: PropTypes.shape({
|
||||
measurementService: PropTypes.shape({
|
||||
getMeasurements: PropTypes.func.isRequired,
|
||||
subscribe: PropTypes.func.isRequired,
|
||||
EVENTS: PropTypes.object.isRequired,
|
||||
VALUE_TYPES: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
Loaded 100 of 1447 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user