Compare commits
243
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3fe676638b | ||
|
|
d69dab70e1 | ||
|
|
f845f87716 | ||
|
|
de6976df3b | ||
|
|
ea9903e92b | ||
|
|
ed0b01cd39 | ||
|
|
2c891dae60 | ||
|
|
bb4297af35 | ||
|
|
3c4795d8cb | ||
|
|
c6d5f719e7 | ||
|
|
a5bfae6e27 | ||
|
|
53c16b928f | ||
|
|
ecbe7c56e0 | ||
|
|
c4007d4d0d | ||
|
|
19408c987f | ||
|
|
9c51f17c60 | ||
|
|
e664fffbc0 | ||
|
|
884577a23d | ||
|
|
799d0793fd | ||
|
|
7074c6d806 | ||
|
|
41dc424d4a | ||
|
|
88ef7b733f | ||
|
|
2c38f6ba8c | ||
|
|
5302e5b62b | ||
|
|
aedb9d1576 | ||
|
|
daf734e09e | ||
|
|
b55518e08e | ||
|
|
ca1287d99e | ||
|
|
86e6d28ade | ||
|
|
5d435424e2 | ||
|
|
9ae3d9f769 | ||
|
|
348acb9091 | ||
|
|
8adf663248 | ||
|
|
4f037ae55e | ||
|
|
55a50c9d38 | ||
|
|
156c1ba60b | ||
|
|
ec76ea24dd | ||
|
|
431b307fab | ||
|
|
0493a0d587 | ||
|
|
58d38495f0 | ||
|
|
862f255be2 | ||
|
|
72207e6c78 | ||
|
|
6d3b3d191f | ||
|
|
d8867d3804 | ||
|
|
019dedce34 | ||
|
|
4d104add05 | ||
|
|
924a5478f3 | ||
|
|
d8f628b125 | ||
|
|
cab8d322c2 | ||
|
|
3ef26cc670 | ||
|
|
1efe2db662 | ||
|
|
72933af984 | ||
|
|
2645e3c7aa | ||
|
|
81ab52784a | ||
|
|
09a302250c | ||
|
|
783bab7c9d | ||
|
|
7bbc213f58 | ||
|
|
6d88978710 | ||
|
|
32bb553b05 | ||
|
|
d5cbbb037d | ||
|
|
b904ea0f23 | ||
|
|
839923e578 | ||
|
|
030e1e4104 | ||
|
|
265c1fbb10 | ||
|
|
f3aa8a8b61 | ||
|
|
18156bb7b1 | ||
|
|
93a009f1a6 | ||
|
|
11471e6fc1 | ||
|
|
7d286a07f2 | ||
|
|
19418df8b5 | ||
|
|
c9eb0250dd | ||
|
|
c3cd265430 | ||
|
|
913264d289 | ||
|
|
74acc02c37 | ||
|
|
453ff0be4f | ||
|
|
2e040d550d | ||
|
|
bfa1b1c7c0 | ||
|
|
a3e7ff479a | ||
|
|
87f361516c | ||
|
|
5eaf6168f6 | ||
|
|
5ab74f5020 | ||
|
|
92273b25b3 | ||
|
|
3f296bdeb5 | ||
|
|
0600858055 | ||
|
|
4a184d0be7 | ||
|
|
f3a4f861b4 | ||
|
|
fc19912162 | ||
|
|
61595ce7d6 | ||
|
|
ffd53eff85 | ||
|
|
52a43c771a | ||
|
|
c3c132d268 | ||
|
|
e742328ac1 | ||
|
|
90647b63a0 | ||
|
|
ed44ad7f4b | ||
|
|
b0a456a46c | ||
|
|
8b7db45641 | ||
|
|
b5599dde4e | ||
|
|
f7b7131d70 | ||
|
|
d646c15c27 | ||
|
|
64f8bb580a | ||
|
|
ce29bbe795 | ||
|
|
0bbcb4269d | ||
|
|
3efa793e60 | ||
|
|
3b50bd8cc4 | ||
|
|
cd553e29b2 | ||
|
|
12dd5a2619 | ||
|
|
f7a435c2f3 | ||
|
|
19f1da702f | ||
|
|
14974b6973 | ||
|
|
b4dbe5656b | ||
|
|
c6f7349fd7 | ||
|
|
abe939bd20 | ||
|
|
4d4dd45c23 | ||
|
|
7671b9faf4 | ||
|
|
e653961858 | ||
|
|
aeb61f40da | ||
|
|
b5fb2461dd | ||
|
|
15ad8f29cd | ||
|
|
fa37593696 | ||
|
|
08eed4cdf9 | ||
|
|
b5b7d323c8 | ||
|
|
8a8ae237d2 | ||
|
|
cf1e00fee0 | ||
|
|
ef4f28690a | ||
|
|
c98211ccb5 | ||
|
|
71342fac8f | ||
|
|
52f419d188 | ||
|
|
ca3a0b56f7 | ||
|
|
1a91f210ab | ||
|
|
3503e6e9f4 | ||
|
|
0c51444066 | ||
|
|
83c453d1f3 | ||
|
|
fa73a458bd | ||
|
|
5670a1699d | ||
|
|
c529c0da9c | ||
|
|
d51211f6b9 | ||
|
|
2d8400f9d8 | ||
|
|
e12242a15c | ||
|
|
8baf9deaf0 | ||
|
|
a34a0b1a3a | ||
|
|
e1545b67da | ||
|
|
d75305e70d | ||
|
|
242beb7b46 | ||
|
|
c5d4e97115 | ||
|
|
59cdc4ba9a | ||
|
|
971c594791 | ||
|
|
c55136f55e | ||
|
|
63e12dd651 | ||
|
|
c75c6af22f | ||
|
|
3c777ba58b | ||
|
|
e6f73a2025 | ||
|
|
652e61a417 | ||
|
|
48bcdd6b50 | ||
|
|
cc73f15c8c | ||
|
|
3dce70e624 | ||
|
|
1d00893c49 | ||
|
|
2890635675 | ||
|
|
a6d967a37e | ||
|
|
f23a4500f5 | ||
|
|
cadb559619 | ||
|
|
ad3ef3e8ef | ||
|
|
18c6a47dca | ||
|
|
93f30f06c2 | ||
|
|
50d2640876 | ||
|
|
10e399d7b8 | ||
|
|
e4937e7473 | ||
|
|
62dd2e78bd | ||
|
|
c3654b7ddf | ||
|
|
de60586b71 | ||
|
|
f670678cf0 | ||
|
|
56633a67d9 | ||
|
|
8e2535a308 | ||
|
|
6da1734b6e | ||
|
|
5ae6e9caf4 | ||
|
|
5e4043d99c | ||
|
|
0e991d6bde | ||
|
|
2f63a9db80 | ||
|
|
bedc379e97 | ||
|
|
5b7dc9e9c3 | ||
|
|
00099c0136 | ||
|
|
799d648a90 | ||
|
|
d5c347cecd |
No files matched your search
@@ -269,8 +269,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: |
|
||||
@@ -289,8 +288,7 @@ jobs:
|
||||
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 +335,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)
|
||||
|
||||
@@ -417,8 +414,8 @@ workflows:
|
||||
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
|
||||
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" }}'
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
server {
|
||||
listen ${PORT};
|
||||
listen ${PORT} default_server;
|
||||
listen [::]:${PORT} default_server;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
server {
|
||||
listen ${SSL_PORT} ssl http2 default_server;
|
||||
listen [::]:${SSL_PORT} ssl http2 default_server;
|
||||
ssl_certificate /etc/ssl/certs/ssl-certificate.crt;
|
||||
ssl_certificate_key /etc/ssl/private/ssl-private-key.key;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cross-Origin-Opener-Policy same-origin;
|
||||
add_header Cross-Origin-Embedder-Policy require-corp;
|
||||
add_header Cross-Origin-Resource-Policy same-origin;
|
||||
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-Proto $http_x_forwarded_proto;
|
||||
}
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
if [ -n "$SSL_PORT" ]
|
||||
then
|
||||
envsubst '${SSL_PORT}:${PORT}' < /usr/src/default.ssl.conf.template > /etc/nginx/conf.d/default.conf
|
||||
else
|
||||
envsubst '${PORT}' < /usr/src/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
fi
|
||||
|
||||
if [ -n "$APP_CONFIG" ]
|
||||
then
|
||||
echo "$APP_CONFIG" > /usr/share/nginx/html/app-config.js
|
||||
fi
|
||||
|
||||
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
|
||||
then
|
||||
|
||||
@@ -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/
|
||||
+5
-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",
|
||||
@@ -21,6 +16,10 @@
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
// Enforce consistent brace style for all control statements for readability
|
||||
"curly": "error"
|
||||
},
|
||||
"globals": {
|
||||
"cy": true,
|
||||
"before": true,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+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
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Codespell
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codespell:
|
||||
name: Check for spelling errors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Codespell
|
||||
uses: codespell-project/actions-codespell@v2
|
||||
@@ -42,3 +42,4 @@ platform/app/src/pluginImports.js
|
||||
/Viewers.iml
|
||||
platform/app/.recipes/Nginx-Dcm4Che/dcm4che/dcm4che-arc/*
|
||||
platform/app/.recipes/OpenResty-Orthanc/logs/*
|
||||
.vercel
|
||||
@@ -1,3 +1,4 @@
|
||||
[submodule "testdata"]
|
||||
path = testdata
|
||||
url = https://github.com/OHIF/viewer-testdata-dicomweb.git
|
||||
branch = main
|
||||
+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>
|
||||
+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"
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -33,7 +33,11 @@ function transpileJavaScript(mode) {
|
||||
rootMode: 'upward',
|
||||
envName: mode,
|
||||
cacheCompression: false,
|
||||
cacheDirectory: true,
|
||||
// Note: This was causing a lot of issues with yarn link of the cornerstone
|
||||
// only set this to true if you don't have a yarn link to external libs
|
||||
// otherwise expect the lib changes not to be reflected in the dev server
|
||||
// as it will be cached
|
||||
cacheDirectory: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+21
-29
@@ -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
|
||||
@@ -26,11 +25,9 @@ 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();
|
||||
@@ -73,21 +70,17 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
devServer: {
|
||||
open: true,
|
||||
port: 3000,
|
||||
historyApiFallback: true,
|
||||
headers: {
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
},
|
||||
module: {
|
||||
noParse: [/(codec)/, /(dicomicc)/],
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
enforce: 'pre',
|
||||
use: 'source-map-loader',
|
||||
},
|
||||
transpileJavaScriptRule(mode),
|
||||
loadWebWorkersRule,
|
||||
// loadShadersRule,
|
||||
@@ -109,10 +102,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'),
|
||||
@@ -135,7 +125,12 @@ 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({
|
||||
@@ -149,15 +144,12 @@ module.exports = (env, argv, { SRC_DIR, ENTRY }) => {
|
||||
'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 || ''
|
||||
),
|
||||
'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.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
// Uncomment to generate bundle analyzer
|
||||
// new BundleAnalyzerPlugin(),
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **health imaging:** studies not loading from healthimaging if imagepositionpatient is missing ([#3646](https://github.com/OHIF/Viewers/issues/3646)) ([74e62a1](https://github.com/OHIF/Viewers/commit/74e62a176374f720080d4e777972f70e7f2d8b2b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package ohif-monorepo-root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **suv:** import calculate-suv library version that prevents SUV calculation for a zero PatientWeight ([#3638](https://github.com/OHIF/Viewers/issues/3638)) ([0d10f46](https://github.com/OHIF/Viewers/commit/0d10f46b885fe54ec3dae1848134da658eb6280a))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package ohif-monorepo-root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package ohif-monorepo-root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package ohif-monorepo-root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hotkeys:** preserve hotkeys if changed, and reduce re-rendering ([#3635](https://github.com/OHIF/Viewers/issues/3635)) ([94f7cfb](https://github.com/OHIF/Viewers/commit/94f7cfb08e3490488394efc42ef089ebe55e86be))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package ohif-monorepo-root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nginx archive recipe:** Fixes to various configuration files. ([#3624](https://github.com/OHIF/Viewers/issues/3624)) ([3ce7225](https://github.com/OHIF/Viewers/commit/3ce72254b390f32c9aa207a0589e688805e2659d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **OpenIdConnectRoutes:** fix handleUnauthenticated ([#3617](https://github.com/OHIF/Viewers/issues/3617)) ([35fc30c](https://github.com/OHIF/Viewers/commit/35fc30c5359d8199cc38ffa670c08687d2672f11))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **PT Metadata:** Allow for PatientWeight to be missing from the metadata ([#3621](https://github.com/OHIF/Viewers/issues/3621)) ([44f101d](https://github.com/OHIF/Viewers/commit/44f101d3f2b3204b67e31f4e4939062e65a246ee))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package ohif-monorepo-root
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))
|
||||
+7
-1
@@ -38,6 +38,7 @@ COPY platform /usr/src/app/platform
|
||||
|
||||
# Copy Files
|
||||
FROM node:16.15.0-slim as builder
|
||||
RUN apt-get update && apt-get install -y build-essential python3
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -61,7 +62,7 @@ RUN yarn run build
|
||||
|
||||
# Stage 3: Bundle the built application into a Docker container
|
||||
# which runs Nginx using Alpine Linux
|
||||
FROM nginxinc/nginx-unprivileged:1.23.1-alpine as final
|
||||
FROM nginxinc/nginx-unprivileged:1.25-alpine as final
|
||||
#RUN apk add --no-cache bash
|
||||
ENV PORT=80
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
@@ -69,5 +70,10 @@ USER nginx
|
||||
COPY --chown=nginx:nginx .docker/Viewer-v3.x /usr/src
|
||||
RUN chmod 777 /usr/src/entrypoint.sh
|
||||
COPY --from=builder /usr/src/app/platform/app/dist /usr/share/nginx/html
|
||||
# In entrypoint.sh, app-config.js might be overwritten, so chmod it to be writeable.
|
||||
# The nginx user cannot chmod it, so change to root.
|
||||
USER root
|
||||
RUN chmod 666 /usr/share/nginx/html/app-config.js
|
||||
USER nginx
|
||||
ENTRYPOINT ["/usr/src/entrypoint.sh"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -3,7 +3,7 @@
|
||||
<div align="center">
|
||||
<h1>OHIF Medical Imaging Viewer</h1>
|
||||
<p><strong>The OHIF Viewer</strong> is a zero-footprint medical image viewer
|
||||
provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/dicomweb/">DICOMweb</a>.</p>
|
||||
provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF)</a>. It is a configurable and extensible progressive web application with out-of-the-box support for image archives which support <a href="https://www.dicomstandard.org/using/dicomweb/">DICOMweb</a>.</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,13 +34,15 @@ provided by the <a href="https://ohif.org/">Open Health Imaging Foundation (OHIF
|
||||
<!-- [](#contributors) -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
| | |
|
||||
| :-: | :--- |
|
||||
| <img src="platform/docs/docs/assets/img/demo-measurements.jpg" alt="Measurement tracking" width="350"/> | Measurement Tracking |
|
||||
| <img src="platform/docs/docs/assets/img/demo-segmentation.png" alt="Segmentations" width="350"/> | Labelmap Segmentations |
|
||||
| <img src="platform/docs/docs/assets/img/demo-ptct.png" alt="Hanging Protocols" width="350"/> | Fusion and Custom Hanging protocols |
|
||||
| <img src="platform/docs/docs/assets/img/demo-microscopy.png" alt="Microscopy" width="350"/> | Slide Microscopy |
|
||||
| <img src="platform/docs/docs/assets/img/demo-volumeRendering.png" alt="Volume Rendering" width="350"/> | Volume Rendering |
|
||||
|
||||
| | | |
|
||||
| :-: | :--- | :--- |
|
||||
| <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) |
|
||||
|
||||
|
||||
|
||||
## About
|
||||
@@ -161,7 +163,7 @@ yarn install
|
||||
|
||||
These commands are available from the root directory. Each project directory
|
||||
also supports a number of commands that can be found in their respective
|
||||
`README.md` and `project.json` files.
|
||||
`README.md` and `package.json` files.
|
||||
|
||||
| Yarn Commands | Description |
|
||||
| ---------------------------- | ------------------------------------------------------------- |
|
||||
@@ -247,7 +249,7 @@ or, for v1, please cite:
|
||||
> [10.1158/0008-5472.CAN-17-0334](https://www.doi.org/10.1158/0008-5472.CAN-17-0334)
|
||||
|
||||
**Note:** If you use or find this repository helpful, please take the time to
|
||||
star this repository on Github. This is an easy way for us to assess adoption
|
||||
star this repository on GitHub. This is an easy way for us to assess adoption
|
||||
and it can help us obtain future funding for the project.
|
||||
|
||||
This work is supported primarily by the National Institutes of Health, National
|
||||
@@ -255,7 +257,11 @@ Cancer Institute, Informatics Technology for Cancer Research (ITCR) program,
|
||||
under a
|
||||
[grant to Dr. Gordon Harris at Massachusetts General Hospital (U24 CA199460)](https://projectreporter.nih.gov/project_info_description.cfm?aid=8971104).
|
||||
|
||||
This project is tested with BrowserStack. Thank you for supporting open source
|
||||
[NCI Imaging Data Commons (IDC) project](https://imaging.datacommons.cancer.gov/) supported the development of new features and bug fixes marked with ["IDC:priority"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Apriority),
|
||||
["IDC:candidate"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acandidate) or ["IDC:collaboration"](https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3AIDC%3Acollaboration). NCI Imaging Data Commons is supported by contract number 19X037Q from
|
||||
Leidos Biomedical Research under Task Order HHSN26100071 from NCI. [IDC Viewer](https://learn.canceridc.dev/portal/visualization) is a customized version of the OHIF Viewer.
|
||||
|
||||
This project is tested with BrowserStack. Thank you for supporting open-source!
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+9
-9
@@ -3,22 +3,22 @@ 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',
|
||||
{
|
||||
svgo: {
|
||||
plugins: extendDefaultPlugins([
|
||||
plugins: [
|
||||
{
|
||||
name: 'removeViewBox',
|
||||
active: false,
|
||||
name: 'preset-default',
|
||||
params: {
|
||||
overrides: {
|
||||
removeViewBox: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
14369414cf0193f00243ae5c9f8415cf7079a753
|
||||
74e62a176374f720080d4e777972f70e7f2d8b2b
|
||||
@@ -54,11 +54,7 @@ const getCommandsModule = () => ({
|
||||
const ExampleContext = React.createContext();
|
||||
|
||||
function ExampleContextProvider({ children }) {
|
||||
return (
|
||||
<ExampleContext.Provider value={{ example: 'value' }}>
|
||||
{children}
|
||||
</ExampleContext.Provider>
|
||||
);
|
||||
return <ExampleContext.Provider value={{ example: 'value' }}>{children}</ExampleContext.Provider>;
|
||||
}
|
||||
|
||||
const getContextModule = () => [
|
||||
@@ -121,7 +117,7 @@ const getSopClassHandlerModule = (/* ... */) => {
|
||||
|
||||
const getToolbarModule = () => {};
|
||||
|
||||
// displaySet, viewportIndex, dataSource
|
||||
// displaySet, dataSource
|
||||
const getViewportModule = () => {
|
||||
const wrappedViewport = props => {
|
||||
return (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-rt
|
||||
@@ -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.2",
|
||||
"version": "3.7.0-beta.73",
|
||||
"description": "DICOM RT read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -31,10 +31,10 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.7.0-beta.2",
|
||||
"@ohif/extension-cornerstone": "3.7.0-beta.2",
|
||||
"@ohif/extension-default": "3.7.0-beta.2",
|
||||
"@ohif/i18n": "3.7.0-beta.2",
|
||||
"@ohif/core": "3.7.0-beta.73",
|
||||
"@ohif/extension-cornerstone": "3.7.0-beta.73",
|
||||
"@ohif/extension-default": "3.7.0-beta.73",
|
||||
"@ohif/i18n": "3.7.0-beta.73",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
@@ -7,11 +7,7 @@ const sopClassUids = ['1.2.840.10008.5.1.4.1.1.481.3'];
|
||||
|
||||
let loadPromises = {};
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
const instance = instances[0];
|
||||
|
||||
const {
|
||||
@@ -56,10 +52,7 @@ function _getDisplaySetsFromSeries(
|
||||
};
|
||||
|
||||
let referencedSeriesSequence = instance.ReferencedSeriesSequence;
|
||||
if (
|
||||
instance.ReferencedFrameOfReferenceSequence &&
|
||||
!instance.ReferencedSeriesSequence
|
||||
) {
|
||||
if (instance.ReferencedFrameOfReferenceSequence && !instance.ReferencedSeriesSequence) {
|
||||
instance.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
instance.ReferencedFrameOfReferenceSequence
|
||||
);
|
||||
@@ -72,8 +65,7 @@ function _getDisplaySetsFromSeries(
|
||||
|
||||
const referencedSeries = referencedSeriesSequence[0];
|
||||
|
||||
displaySet.referencedImages =
|
||||
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
@@ -88,14 +80,12 @@ 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];
|
||||
}
|
||||
@@ -194,11 +184,7 @@ function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
name: 'dicom-rt',
|
||||
sopClassUids,
|
||||
getDisplaySetsFromSeries: instances => {
|
||||
return _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
);
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,11 +2,10 @@ 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 => {
|
||||
@@ -36,12 +35,14 @@ const extension: Types.Extensions.Extension = {
|
||||
getViewportModule({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
}: Types.Extensions.ExtensionParams) {
|
||||
const ExtendedOHIFCornerstoneRTViewport = props => {
|
||||
return (
|
||||
<OHIFCornerstoneRTViewport
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
commandsManager={commandsManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -59,3 +60,4 @@ 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,19 +149,9 @@ 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];
|
||||
|
||||
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
||||
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||
|
||||
if (!imageId) {
|
||||
continue;
|
||||
}
|
||||
let isSupported = false;
|
||||
|
||||
const points = [];
|
||||
@@ -235,9 +200,7 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||
);
|
||||
|
||||
return imageIdSopInstanceUidPairsEntry
|
||||
? imageIdSopInstanceUidPairsEntry.imageId
|
||||
: null;
|
||||
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
|
||||
};
|
||||
|
||||
function _getImageIdSopInstanceUidPairsForDisplaySet(referencedDisplaySet) {
|
||||
@@ -258,8 +221,7 @@ function _setROIContourMetadata(
|
||||
isSupported
|
||||
) {
|
||||
const StructureSetROI = StructureSetROISequence.find(
|
||||
structureSetROI =>
|
||||
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||
structureSetROI => structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||
);
|
||||
|
||||
const ROIContourData = {
|
||||
@@ -299,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,13 +1,6 @@
|
||||
async function _hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportIndex,
|
||||
servicesManager,
|
||||
}) {
|
||||
const {
|
||||
segmentationService,
|
||||
hangingProtocolService,
|
||||
viewportGridService,
|
||||
} = servicesManager.services;
|
||||
async function _hydrateRTDisplaySet({ rtDisplaySet, viewportId, servicesManager }) {
|
||||
const { segmentationService, hangingProtocolService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
const displaySetInstanceUID = rtDisplaySet.referencedDisplaySetInstanceUID;
|
||||
|
||||
@@ -27,7 +20,7 @@ async function _hydrateRTDisplaySet({
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
@@ -39,8 +32,8 @@ async function _hydrateRTDisplaySet({
|
||||
// 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) {
|
||||
viewports.forEach(viewport => {
|
||||
if (viewport.viewportId === viewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,9 +44,16 @@ async function _hydrateRTDisplaySet({
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
updatedViewports.push({
|
||||
viewportIndex: index,
|
||||
viewportId: viewport.viewportId,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
|
||||
// Used for segmentation hydration right now, since the logic to decide whether
|
||||
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
|
||||
// so we need to re-render (force update via change of the needsRerendering) so that React
|
||||
// does the diffing and decides we should render this again (although the id and element has not changed)
|
||||
// so that the CornerstoneViewportService can decide whether to render the segmentation or not.
|
||||
needsRerendering: true,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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, {});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ButtonEnums } from '@ohif/ui';
|
||||
import hydrateRTDisplaySet from './_hydrateRT';
|
||||
|
||||
const RESPONSE = {
|
||||
@@ -9,21 +10,23 @@ const RESPONSE = {
|
||||
function promptHydrateRT({
|
||||
servicesManager,
|
||||
rtDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
toolGroupId = 'default',
|
||||
preHydrateCallbacks,
|
||||
}) {
|
||||
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 => {
|
||||
callback();
|
||||
});
|
||||
|
||||
const isHydrated = await hydrateRTDisplaySet({
|
||||
rtDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
toolGroupId,
|
||||
servicesManager,
|
||||
});
|
||||
@@ -33,17 +36,17 @@ function promptHydrateRT({
|
||||
});
|
||||
}
|
||||
|
||||
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 = [
|
||||
{
|
||||
type: 'secondary',
|
||||
type: ButtonEnums.type.secondary,
|
||||
text: 'No',
|
||||
value: RESPONSE.CANCEL,
|
||||
},
|
||||
{
|
||||
type: 'primary',
|
||||
type: ButtonEnums.type.primary,
|
||||
text: 'Yes',
|
||||
value: RESPONSE.HYDRATE_SEG,
|
||||
},
|
||||
@@ -54,7 +57,7 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
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 { ViewportActionBar, useViewportGrid, LoadingIndicatorTotalPercent } from '@ohif/ui';
|
||||
|
||||
import _hydrateRTdisplaySet from '../utils/_hydrateRT';
|
||||
import promptHydrateRT from '../utils/promptHydrateRT';
|
||||
@@ -21,10 +17,10 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
viewportIndex,
|
||||
viewportLabel,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
@@ -35,7 +31,9 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
customizationService,
|
||||
} = 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) {
|
||||
@@ -66,12 +64,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,
|
||||
@@ -91,14 +87,20 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
setElement(null);
|
||||
};
|
||||
|
||||
const storePresentationState = useCallback(() => {
|
||||
viewportGrid?.viewports.forEach(({ viewportId }) => {
|
||||
commandsManager.runCommand('storePresentation', {
|
||||
viewportId,
|
||||
});
|
||||
});
|
||||
}, [viewportGrid]);
|
||||
|
||||
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,7 +117,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
onElementDisabled={onElementDisabled}
|
||||
></Component>
|
||||
);
|
||||
}, [viewportIndex, rtDisplaySet, toolGroupId]);
|
||||
}, [viewportId, rtDisplaySet, toolGroupId]);
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
@@ -136,11 +138,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
segmentationService.jumpToSegmentCenter(
|
||||
segmentationId,
|
||||
newSelectedSegmentIndex,
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment]
|
||||
@@ -153,31 +151,28 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
promptHydrateRT({
|
||||
servicesManager,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
rtDisplaySet,
|
||||
preHydrateCallbacks: [storePresentationState],
|
||||
}).then(isHydrated => {
|
||||
if (isHydrated) {
|
||||
setIsHydrated(true);
|
||||
}
|
||||
});
|
||||
}, [servicesManager, viewportIndex, rtDisplaySet, rtIsLoading]);
|
||||
}, [servicesManager, viewportId, rtDisplaySet, rtIsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
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',
|
||||
});
|
||||
}
|
||||
@@ -212,12 +207,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: [],
|
||||
});
|
||||
}
|
||||
@@ -236,19 +229,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);
|
||||
};
|
||||
@@ -259,9 +246,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
referencedDisplaySetRef.current = null;
|
||||
};
|
||||
}, [rtDisplaySet]);
|
||||
@@ -282,7 +267,7 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
@@ -303,9 +288,16 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
} = referencedDisplaySetRef.current.metadata;
|
||||
|
||||
const onStatusClick = 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
|
||||
// required if the user has changed the viewport (other viewport than RT viewport)
|
||||
// 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({
|
||||
rtDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
@@ -333,26 +325,22 @@ function OHIFCornerstoneRTViewport(props) {
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: `RT Viewport ${SeriesDescription}`,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
: '',
|
||||
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`
|
||||
: '',
|
||||
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..."
|
||||
@@ -367,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,
|
||||
|
||||
@@ -13,25 +13,28 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
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">
|
||||
<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>
|
||||
{!isHydrated && (
|
||||
<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={onStatusClick}
|
||||
>
|
||||
@@ -44,7 +47,10 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<Tooltip
|
||||
content={<ToolTipMessage />}
|
||||
position="bottom-left"
|
||||
>
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -40,13 +40,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,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-seg
|
||||
@@ -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.2",
|
||||
"version": "3.7.0-beta.73",
|
||||
"description": "DICOM SEG read workflow",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -31,10 +31,10 @@
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.7.0-beta.2",
|
||||
"@ohif/extension-cornerstone": "3.7.0-beta.2",
|
||||
"@ohif/extension-default": "3.7.0-beta.2",
|
||||
"@ohif/i18n": "3.7.0-beta.2",
|
||||
"@ohif/core": "3.7.0-beta.73",
|
||||
"@ohif/extension-cornerstone": "3.7.0-beta.73",
|
||||
"@ohif/extension-default": "3.7.0-beta.73",
|
||||
"@ohif/i18n": "3.7.0-beta.73",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
@@ -5,7 +5,6 @@ const segProtocol: Types.HangingProtocol.Protocol = {
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Segmentations',
|
||||
// Just apply this one when specifically listed
|
||||
protocolMatchingRules: [],
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import vtkMath from '@kitware/vtk.js/Common/Core/Math';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
import { metaData, cache, triggerEvent, eventTarget } from '@cornerstonejs/core';
|
||||
import { adaptersSEG, Enums } from '@cornerstonejs/adapters';
|
||||
|
||||
import { SOPClassHandlerId } from './id';
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
|
||||
import { dicomlabToRGB } from './utils/dicomlabToRGB';
|
||||
|
||||
const sopClassUids = ['1.2.840.10008.5.1.4.1.1.66.4'];
|
||||
|
||||
let loadPromises = {};
|
||||
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
const instance = instances[0];
|
||||
|
||||
const {
|
||||
@@ -68,8 +62,7 @@ function _getDisplaySetsFromSeries(
|
||||
|
||||
const referencedSeries = referencedSeriesSequence[0];
|
||||
|
||||
displaySet.referencedImages =
|
||||
instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedImages = instance.ReferencedSeriesSequence.ReferencedInstanceSequence;
|
||||
displaySet.referencedSeriesInstanceUID = referencedSeries.SeriesInstanceUID;
|
||||
|
||||
displaySet.getReferenceDisplaySet = () => {
|
||||
@@ -84,8 +77,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;
|
||||
@@ -118,17 +110,13 @@ 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
|
||||
) {
|
||||
const segments = await _loadSegments(
|
||||
if (!segDisplaySet.segments || Object.keys(segDisplaySet.segments).length === 0) {
|
||||
await _loadSegments({
|
||||
extensionManager,
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
headers
|
||||
);
|
||||
|
||||
segDisplaySet.segments = segments;
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
const suppressEvents = true;
|
||||
@@ -147,152 +135,61 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) {
|
||||
return loadPromises[SOPInstanceUID];
|
||||
}
|
||||
|
||||
async function _loadSegments(extensionManager, segDisplaySet, headers) {
|
||||
async function _loadSegments({ extensionManager, servicesManager, segDisplaySet, headers }) {
|
||||
const utilityModule = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.utilityModule.common'
|
||||
);
|
||||
|
||||
const { segmentationService } = servicesManager.services;
|
||||
|
||||
const { dicomLoaderService } = utilityModule.exports;
|
||||
const segArrayBuffer = await dicomLoaderService.findDicomDataPromise(
|
||||
segDisplaySet,
|
||||
null,
|
||||
headers
|
||||
);
|
||||
const arrayBuffer = await dicomLoaderService.findDicomDataPromise(segDisplaySet, null, headers);
|
||||
|
||||
const dicomData = DicomMessage.readFile(segArrayBuffer);
|
||||
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
||||
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
const cachedReferencedVolume = cache.getVolume(segDisplaySet.referencedVolumeId);
|
||||
|
||||
if (!Array.isArray(dataset.SegmentSequence)) {
|
||||
dataset.SegmentSequence = [dataset.SegmentSequence];
|
||||
if (!cachedReferencedVolume) {
|
||||
throw new Error(
|
||||
'Referenced Volume is missing for the SEG, and stack viewport SEG is not supported yet'
|
||||
);
|
||||
}
|
||||
|
||||
const segments = _getSegments(dataset);
|
||||
return segments;
|
||||
const { imageIds } = cachedReferencedVolume;
|
||||
|
||||
// Todo: what should be defaults here
|
||||
const tolerance = 0.001;
|
||||
const skipOverlapping = true;
|
||||
|
||||
eventTarget.addEventListener(Enums.Events.SEGMENTATION_LOAD_PROGRESS, evt => {
|
||||
const { percentComplete } = evt.detail;
|
||||
segmentationService._broadcastEvent(segmentationService.EVENTS.SEGMENT_LOADING_COMPLETE, {
|
||||
percentComplete,
|
||||
});
|
||||
});
|
||||
|
||||
const results = await adaptersSEG.Cornerstone3D.Segmentation.generateToolState(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
metaData,
|
||||
{ skipOverlapping, tolerance, eventTarget, triggerEvent }
|
||||
);
|
||||
|
||||
results.segMetadata.data.forEach((data, i) => {
|
||||
if (i > 0) {
|
||||
data.rgba = dicomlabToRGB(data.RecommendedDisplayCIELabValue);
|
||||
}
|
||||
});
|
||||
|
||||
Object.assign(segDisplaySet, results);
|
||||
}
|
||||
|
||||
function _segmentationExists(segDisplaySet, segmentationService) {
|
||||
// This should be abstracted with the CornerstoneCacheService
|
||||
return segmentationService.getSegmentation(
|
||||
segDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
function _getPixelData(dataset, segments) {
|
||||
let frameSize = Math.ceil((dataset.Rows * dataset.Columns) / 8);
|
||||
let nextOffset = 0;
|
||||
|
||||
Object.keys(segments).forEach(segmentKey => {
|
||||
const segment = segments[segmentKey];
|
||||
segment.numberOfFrames = segment.functionalGroups.length;
|
||||
segment.size = segment.numberOfFrames * frameSize;
|
||||
segment.offset = nextOffset;
|
||||
nextOffset = segment.offset + segment.size;
|
||||
const packedSegment = dataset.PixelData[0].slice(
|
||||
segment.offset,
|
||||
nextOffset
|
||||
);
|
||||
|
||||
segment.pixelData = dcmjs.data.BitArray.unpack(packedSegment);
|
||||
segment.geometry = geometryFromFunctionalGroups(
|
||||
dataset,
|
||||
segment.functionalGroups
|
||||
);
|
||||
});
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
function geometryFromFunctionalGroups(dataset, perFrame) {
|
||||
let pixelMeasures =
|
||||
dataset.SharedFunctionalGroupsSequence.PixelMeasuresSequence;
|
||||
let planeOrientation =
|
||||
dataset.SharedFunctionalGroupsSequence.PlaneOrientationSequence;
|
||||
let planePosition = perFrame[0].PlanePositionSequence; // TODO: assume sorted frames!
|
||||
|
||||
const geometry = {};
|
||||
|
||||
// NB: DICOM PixelSpacing is defined as Row then Column,
|
||||
// unlike ImageOrientationPatient
|
||||
let spacingBetweenSlices = pixelMeasures.SpacingBetweenSlices;
|
||||
if (!spacingBetweenSlices) {
|
||||
if (pixelMeasures.SliceThickness) {
|
||||
console.log('Using SliceThickness as SpacingBetweenSlices');
|
||||
spacingBetweenSlices = pixelMeasures.SliceThickness;
|
||||
}
|
||||
}
|
||||
geometry.spacing = [
|
||||
pixelMeasures.PixelSpacing[1],
|
||||
pixelMeasures.PixelSpacing[0],
|
||||
spacingBetweenSlices,
|
||||
].map(Number);
|
||||
|
||||
geometry.dimensions = [dataset.Columns, dataset.Rows, perFrame.length].map(
|
||||
Number
|
||||
);
|
||||
|
||||
let orientation = planeOrientation.ImageOrientationPatient.map(Number);
|
||||
const columnStepToPatient = orientation.slice(0, 3);
|
||||
const rowStepToPatient = orientation.slice(3, 6);
|
||||
geometry.planeNormal = [];
|
||||
vtkMath.cross(columnStepToPatient, rowStepToPatient, geometry.planeNormal);
|
||||
|
||||
let firstPosition = perFrame[0].PlanePositionSequence.ImagePositionPatient.map(
|
||||
Number
|
||||
);
|
||||
let lastPosition = perFrame[
|
||||
perFrame.length - 1
|
||||
].PlanePositionSequence.ImagePositionPatient.map(Number);
|
||||
geometry.sliceStep = [];
|
||||
vtkMath.subtract(lastPosition, firstPosition, geometry.sliceStep);
|
||||
vtkMath.normalize(geometry.sliceStep);
|
||||
geometry.direction = columnStepToPatient
|
||||
.concat(rowStepToPatient)
|
||||
.concat(geometry.sliceStep);
|
||||
geometry.origin = planePosition.ImagePositionPatient.map(Number);
|
||||
|
||||
return geometry;
|
||||
}
|
||||
|
||||
function _getSegments(dataset) {
|
||||
const segments = {};
|
||||
|
||||
dataset.SegmentSequence.forEach(segment => {
|
||||
const cielab = segment.RecommendedDisplayCIELabValue;
|
||||
const rgba = dcmjs.data.Colors.dicomlab2RGB(cielab).map(x =>
|
||||
Math.round(x * 255)
|
||||
);
|
||||
|
||||
rgba.push(255);
|
||||
const segmentNumber = segment.SegmentNumber;
|
||||
|
||||
segments[segmentNumber] = {
|
||||
color: rgba,
|
||||
functionalGroups: [],
|
||||
offset: null,
|
||||
size: null,
|
||||
pixelData: null,
|
||||
label: segment.SegmentLabel,
|
||||
};
|
||||
});
|
||||
|
||||
// make a list of functional groups per segment
|
||||
dataset.PerFrameFunctionalGroupsSequence.forEach(functionalGroup => {
|
||||
const segmentNumber =
|
||||
functionalGroup.SegmentIdentificationSequence.ReferencedSegmentNumber;
|
||||
segments[segmentNumber].functionalGroups.push(functionalGroup);
|
||||
});
|
||||
|
||||
return _getPixelData(dataset, segments);
|
||||
return segmentationService.getSegmentation(segDisplaySet.displaySetInstanceUID);
|
||||
}
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
);
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
return [
|
||||
|
||||
@@ -6,11 +6,10 @@ import { Types } from '@ohif/core';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import PanelSegmentation from './panels/PanelSegmentation';
|
||||
import getHangingProtocolModule from './getHangingProtocolModule';
|
||||
import hydrateSEGDisplaySet from './utils/_hydrateSEG';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport'
|
||||
);
|
||||
return import(/* webpackPrefetch: true */ './viewports/OHIFCornerstoneSEGViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstoneSEGViewport = props => {
|
||||
@@ -37,11 +36,7 @@ const extension = {
|
||||
* 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[] => {
|
||||
getPanelModule: ({ servicesManager, commandsManager, extensionManager }): Types.Panel[] => {
|
||||
const wrappedPanelSegmentation = () => {
|
||||
return (
|
||||
<PanelSegmentation
|
||||
@@ -69,14 +64,13 @@ const extension = {
|
||||
<OHIFCornerstoneSEGViewport
|
||||
servicesManager={servicesManager}
|
||||
extensionManager={extensionManager}
|
||||
commandsManager={commandsManager}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return [
|
||||
{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport },
|
||||
];
|
||||
return [{ name: 'dicom-seg', component: ExtendedOHIFCornerstoneSEGViewport }];
|
||||
},
|
||||
/**
|
||||
* SopClassHandlerModule should provide a list of sop class handlers that will be
|
||||
@@ -88,4 +82,5 @@ const extension = {
|
||||
getHangingProtocolModule,
|
||||
};
|
||||
|
||||
export default extension;
|
||||
export default extension;
|
||||
export { hydrateSEGDisplaySet };
|
||||
@@ -2,14 +2,13 @@ import React, { useEffect, useState, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SegmentationGroupTable } from '@ohif/ui';
|
||||
import callInputDialog from './callInputDialog';
|
||||
|
||||
import { useAppConfig } from '@state';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function PanelSegmentation({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
}) {
|
||||
export default function PanelSegmentation({ servicesManager, commandsManager }) {
|
||||
const { segmentationService, uiDialogService } = servicesManager.services;
|
||||
const [appConfig] = useAppConfig();
|
||||
const disableEditing = appConfig?.disableEditing;
|
||||
|
||||
const { t } = useTranslation('PanelSegmentation');
|
||||
const [selectedSegmentationId, setSelectedSegmentationId] = useState(null);
|
||||
@@ -17,9 +16,7 @@ export default function PanelSegmentation({
|
||||
segmentationService.getConfiguration()
|
||||
);
|
||||
|
||||
const [segmentations, setSegmentations] = useState(() =>
|
||||
segmentationService.getSegmentations()
|
||||
);
|
||||
const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations());
|
||||
|
||||
const [isMinimized, setIsMinimized] = useState({});
|
||||
|
||||
@@ -76,32 +73,20 @@ export default function PanelSegmentation({
|
||||
};
|
||||
|
||||
const getToolGroupIds = segmentationId => {
|
||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(
|
||||
segmentationId
|
||||
);
|
||||
const toolGroupIds = segmentationService.getToolGroupIdsWithSegmentation(segmentationId);
|
||||
|
||||
return toolGroupIds;
|
||||
};
|
||||
|
||||
const onSegmentClick = (segmentationId, segmentIndex) => {
|
||||
segmentationService.setActiveSegmentForSegmentation(
|
||||
segmentationId,
|
||||
segmentIndex
|
||||
);
|
||||
segmentationService.setActiveSegmentForSegmentation(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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -116,11 +101,7 @@ export default function PanelSegmentation({
|
||||
return;
|
||||
}
|
||||
|
||||
segmentationService.setSegmentLabelForSegmentation(
|
||||
segmentationId,
|
||||
segmentIndex,
|
||||
label
|
||||
);
|
||||
segmentationService.setSegmentLabelForSegmentation(segmentationId, segmentIndex, label);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -189,7 +170,7 @@ export default function PanelSegmentation({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-auto min-h-0 justify-between mt-1">
|
||||
<div className="mt-1 flex min-h-0 flex-auto flex-col justify-between">
|
||||
{/* show segmentation table */}
|
||||
{segmentations?.length ? (
|
||||
<SegmentationGroupTable
|
||||
@@ -203,6 +184,7 @@ export default function PanelSegmentation({
|
||||
onSegmentationEdit={onSegmentationEdit}
|
||||
onSegmentClick={onSegmentClick}
|
||||
onSegmentEdit={onSegmentEdit}
|
||||
disableEditing={disableEditing}
|
||||
onSegmentColorClick={onSegmentColorClick}
|
||||
onSegmentDelete={onSegmentDelete}
|
||||
onToggleSegmentVisibility={onToggleSegmentVisibility}
|
||||
@@ -210,25 +192,13 @@ export default function PanelSegmentation({
|
||||
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
|
||||
segmentationConfig={{ initialConfig: segmentationConfiguration }}
|
||||
setRenderOutline={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderOutline',
|
||||
value
|
||||
)
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderOutline', value)
|
||||
}
|
||||
setOutlineOpacityActive={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'outlineOpacity',
|
||||
value
|
||||
)
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineOpacity', value)
|
||||
}
|
||||
setRenderFill={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'renderFill',
|
||||
value
|
||||
)
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'renderFill', value)
|
||||
}
|
||||
setRenderInactiveSegmentations={value =>
|
||||
_setSegmentationConfiguration(
|
||||
@@ -238,25 +208,13 @@ export default function PanelSegmentation({
|
||||
)
|
||||
}
|
||||
setOutlineWidthActive={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'outlineWidthActive',
|
||||
value
|
||||
)
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'outlineWidthActive', value)
|
||||
}
|
||||
setFillAlpha={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'fillAlpha',
|
||||
value
|
||||
)
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlpha', value)
|
||||
}
|
||||
setFillAlphaInactive={value =>
|
||||
_setSegmentationConfiguration(
|
||||
selectedSegmentationId,
|
||||
'fillAlphaInactive',
|
||||
value
|
||||
)
|
||||
_setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Input, Dialog } from '@ohif/ui';
|
||||
import { Input, Dialog, ButtonEnums } from '@ohif/ui';
|
||||
|
||||
function callInputDialog(uiDialogService, label, callback) {
|
||||
const dialogId = 'enter-segment-label';
|
||||
@@ -29,8 +29,8 @@ function callInputDialog(uiDialogService, label, callback) {
|
||||
noCloseButton: true,
|
||||
onClose: () => uiDialogService.dismiss({ id: dialogId }),
|
||||
actions: [
|
||||
{ id: 'cancel', text: 'Cancel', type: 'primary' },
|
||||
{ id: 'save', text: 'Confirm', type: 'secondary' },
|
||||
{ id: 'cancel', text: 'Cancel', type: ButtonEnums.type.secondary },
|
||||
{ id: 'save', text: 'Confirm', type: ButtonEnums.type.primary },
|
||||
],
|
||||
onSubmit: onSubmitHandler,
|
||||
body: ({ value, setValue }) => {
|
||||
@@ -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 => {
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
async function _hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId: targetViewportId,
|
||||
servicesManager,
|
||||
}) {
|
||||
const {
|
||||
segmentationService,
|
||||
hangingProtocolService,
|
||||
viewportGridService,
|
||||
} = servicesManager.services;
|
||||
const { segmentationService, hangingProtocolService, viewportGridService } =
|
||||
servicesManager.services;
|
||||
|
||||
const displaySetInstanceUID = segDisplaySet.referencedDisplaySetInstanceUID;
|
||||
|
||||
@@ -27,7 +24,7 @@ async function _hydrateSEGDisplaySet({
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
|
||||
viewportIndex,
|
||||
targetViewportId,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
@@ -37,8 +34,8 @@ async function _hydrateSEGDisplaySet({
|
||||
// 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) {
|
||||
viewports.forEach((viewport, viewportId) => {
|
||||
if (targetViewportId === viewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,9 +46,16 @@ async function _hydrateSEGDisplaySet({
|
||||
|
||||
if (shouldDisplaySeg) {
|
||||
updatedViewports.push({
|
||||
viewportIndex: index,
|
||||
viewportId,
|
||||
displaySetInstanceUIDs: viewport.displaySetInstanceUIDs,
|
||||
viewportOptions: {
|
||||
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
|
||||
// Used for segmentation hydration right now, since the logic to decide whether
|
||||
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
|
||||
// so we need to re-render (force update via change of the needsRerendering) so that React
|
||||
// does the diffing and decides we should render this again (although the id and element has not changed)
|
||||
// so that the CornerstoneViewportService can decide whether to render the segmentation or not.
|
||||
needsRerendering: true,
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
/**
|
||||
* Converts a CIELAB color to an RGB color using the dcmjs library.
|
||||
* @param cielab - The CIELAB color to convert.
|
||||
* @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));
|
||||
|
||||
return rgb;
|
||||
}
|
||||
|
||||
export { dicomlabToRGB };
|
||||
@@ -1,10 +1,5 @@
|
||||
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, {});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ButtonEnums } from '@ohif/ui';
|
||||
import hydrateSEGDisplaySet from './_hydrateSEG';
|
||||
|
||||
const RESPONSE = {
|
||||
@@ -6,23 +7,20 @@ const RESPONSE = {
|
||||
HYDRATE_SEG: 5,
|
||||
};
|
||||
|
||||
function promptHydrateSEG({
|
||||
servicesManager,
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
}) {
|
||||
function promptHydrateSEG({ servicesManager, segDisplaySet, viewportId, preHydrateCallbacks }) {
|
||||
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 => {
|
||||
callback();
|
||||
});
|
||||
|
||||
const isHydrated = await hydrateSEGDisplaySet({
|
||||
segDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
@@ -31,17 +29,17 @@ 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 = [
|
||||
{
|
||||
type: 'secondary',
|
||||
type: ButtonEnums.type.secondary,
|
||||
text: 'No',
|
||||
value: RESPONSE.CANCEL,
|
||||
},
|
||||
{
|
||||
type: 'primary',
|
||||
type: ButtonEnums.type.primary,
|
||||
text: 'Yes',
|
||||
value: RESPONSE.HYDRATE_SEG,
|
||||
},
|
||||
@@ -52,7 +50,7 @@ function _askHydrate(uiViewportDialogService, viewportIndex) {
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
|
||||
@@ -2,11 +2,7 @@ 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, ViewportActionBar } from '@ohif/ui';
|
||||
import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup';
|
||||
import promptHydrateSEG from '../utils/promptHydrateSEG';
|
||||
import hydrateSEGDisplaySet from '../utils/_hydrateSEG';
|
||||
@@ -20,13 +16,14 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
children,
|
||||
displaySets,
|
||||
viewportOptions,
|
||||
viewportIndex,
|
||||
viewportLabel,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('SEGViewport');
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
@@ -36,7 +33,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
customizationService,
|
||||
} = 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) {
|
||||
@@ -48,7 +45,6 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
|
||||
// States
|
||||
const [isToolGroupCreated, setToolGroupCreated] = useState(false);
|
||||
const [selectedSegment, setSelectedSegment] = useState(1);
|
||||
|
||||
// Hydration means that the SEG is opened and segments are loaded into the
|
||||
@@ -67,11 +63,12 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
// refs
|
||||
const referencedDisplaySetRef = useRef(null);
|
||||
|
||||
const { viewports, activeViewportIndex } = viewportGrid;
|
||||
const { viewports, activeViewportId } = viewportGrid;
|
||||
|
||||
const referencedDisplaySet = segDisplaySet.getReferenceDisplaySet();
|
||||
const referencedDisplaySetMetadata = _getReferencedDisplaySetMetadata(
|
||||
referencedDisplaySet
|
||||
referencedDisplaySet,
|
||||
segDisplaySet
|
||||
);
|
||||
|
||||
referencedDisplaySetRef.current = {
|
||||
@@ -92,14 +89,20 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
setElement(null);
|
||||
};
|
||||
|
||||
const storePresentationState = useCallback(() => {
|
||||
viewportGrid?.viewports.forEach(({ viewportId }) => {
|
||||
commandsManager.runCommand('storePresentation', {
|
||||
viewportId,
|
||||
});
|
||||
});
|
||||
}, [viewportGrid]);
|
||||
|
||||
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 (
|
||||
@@ -117,7 +120,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
// initialImageIndex={initialImageIndex}
|
||||
></Component>
|
||||
);
|
||||
}, [viewportIndex, segDisplaySet, toolGroupId]);
|
||||
}, [viewportId, segDisplaySet, toolGroupId]);
|
||||
|
||||
const onSegmentChange = useCallback(
|
||||
direction => {
|
||||
@@ -139,11 +142,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
newSelectedSegmentIndex = numberOfSegments - 1;
|
||||
}
|
||||
|
||||
segmentationService.jumpToSegmentCenter(
|
||||
segmentationId,
|
||||
newSelectedSegmentIndex,
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.jumpToSegmentCenter(segmentationId, newSelectedSegmentIndex, toolGroupId);
|
||||
setSelectedSegment(newSelectedSegmentIndex);
|
||||
},
|
||||
[selectedSegment]
|
||||
@@ -156,31 +155,28 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
promptHydrateSEG({
|
||||
servicesManager,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
segDisplaySet,
|
||||
preHydrateCallbacks: [storePresentationState],
|
||||
}).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',
|
||||
message: 'Overlapping segments detected which is not currently supported',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
@@ -215,12 +211,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: [],
|
||||
});
|
||||
}
|
||||
@@ -241,19 +235,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
|
||||
);
|
||||
|
||||
setToolGroupCreated(true);
|
||||
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);
|
||||
@@ -265,9 +251,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
|
||||
return () => {
|
||||
// remove the segmentation representations if seg displayset changed
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(
|
||||
toolGroupId
|
||||
);
|
||||
segmentationService.removeSegmentationRepresentationFromToolGroup(toolGroupId);
|
||||
referencedDisplaySetRef.current = null;
|
||||
};
|
||||
}, [segDisplaySet]);
|
||||
@@ -288,7 +272,7 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
@@ -308,15 +292,21 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
} = 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,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
setIsHydrated(isHydrated);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewportActionBar
|
||||
@@ -337,26 +327,23 @@ function OHIFCornerstoneSEGViewport(props) {
|
||||
studyDate: formatDate(StudyDate),
|
||||
seriesDescription: `SEG Viewport ${SeriesDescription}`,
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
: '',
|
||||
patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '',
|
||||
patientSex: PatientSex || '',
|
||||
patientAge: PatientAge || '',
|
||||
MRN: PatientID || '',
|
||||
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
|
||||
thickness: SliceThickness ? utils.roundNumber(SliceThickness, 2) : '',
|
||||
thicknessUnits: SliceThickness !== undefined ? 'mm' : '',
|
||||
spacing:
|
||||
SpacingBetweenSlices !== undefined
|
||||
? `${SpacingBetweenSlices.toFixed(2)}mm`
|
||||
: '',
|
||||
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..."
|
||||
@@ -371,7 +358,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,
|
||||
@@ -381,20 +368,28 @@ OHIFCornerstoneSEGViewport.defaultProps = {
|
||||
customProps: {},
|
||||
};
|
||||
|
||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet) {
|
||||
function _getReferencedDisplaySetMetadata(referencedDisplaySet, segDisplaySet) {
|
||||
const {
|
||||
SharedFunctionalGroupsSequence: [SharedFunctionalGroup],
|
||||
} = segDisplaySet.instance;
|
||||
const {
|
||||
PixelMeasuresSequence: [PixelMeasures],
|
||||
} = SharedFunctionalGroup;
|
||||
const { SpacingBetweenSlices, SliceThickness } = PixelMeasures;
|
||||
|
||||
const image0 = referencedDisplaySet.images[0];
|
||||
const referencedDisplaySetMetadata = {
|
||||
PatientID: image0.PatientID,
|
||||
PatientName: image0.PatientName,
|
||||
PatientSex: image0.PatientSex,
|
||||
PatientAge: image0.PatientAge,
|
||||
SliceThickness: image0.SliceThickness,
|
||||
SliceThickness: image0.SliceThickness || SliceThickness,
|
||||
StudyDate: image0.StudyDate,
|
||||
SeriesDescription: image0.SeriesDescription,
|
||||
SeriesInstanceUID: image0.SeriesInstanceUID,
|
||||
SeriesNumber: image0.SeriesNumber,
|
||||
ManufacturerModelName: image0.ManufacturerModelName,
|
||||
SpacingBetweenSlices: image0.SpacingBetweenSlices,
|
||||
SpacingBetweenSlices: image0.SpacingBetweenSlices || SpacingBetweenSlices,
|
||||
};
|
||||
|
||||
return referencedDisplaySetMetadata;
|
||||
|
||||
@@ -2,37 +2,39 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon, Tooltip } from '@ohif/ui';
|
||||
|
||||
|
||||
export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
let ToolTipMessage = null;
|
||||
let StatusIcon = null;
|
||||
|
||||
const {t} = useTranslation("Common");
|
||||
const loadStr = t("LOAD");
|
||||
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" />;
|
||||
case false:
|
||||
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">
|
||||
<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>
|
||||
{!isHydrated && (
|
||||
<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={onStatusClick}
|
||||
>
|
||||
@@ -42,11 +44,13 @@ export default function _getStatusComponent({ isHydrated, onStatusClick }) {
|
||||
</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,
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone-dicom-sr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cloud data source config:** GUI and API for configuring a cloud data source with Google cloud healthcare implementation ([#3589](https://github.com/OHIF/Viewers/issues/3589)) ([a336992](https://github.com/OHIF/Viewers/commit/a336992971c07552c9dbb6e1de43169d37762ef1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone-dicom-sr",
|
||||
"version": "3.7.0-beta.2",
|
||||
"version": "3.7.0-beta.73",
|
||||
"description": "OHIF extension for an SR Cornerstone Viewport",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -32,10 +32,10 @@
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "3.7.0-beta.2",
|
||||
"@ohif/extension-cornerstone": "3.7.0-beta.2",
|
||||
"@ohif/extension-measurement-tracking": "3.7.0-beta.2",
|
||||
"@ohif/ui": "3.7.0-beta.2",
|
||||
"@ohif/core": "3.7.0-beta.73",
|
||||
"@ohif/extension-cornerstone": "3.7.0-beta.73",
|
||||
"@ohif/extension-measurement-tracking": "3.7.0-beta.73",
|
||||
"@ohif/ui": "3.7.0-beta.73",
|
||||
"dcmjs": "^0.29.5",
|
||||
"dicom-parser": "^1.8.9",
|
||||
"hammerjs": "^2.0.8",
|
||||
@@ -44,9 +44,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.1.8",
|
||||
"@cornerstonejs/core": "^1.1.8",
|
||||
"@cornerstonejs/tools": "^1.1.8",
|
||||
"@cornerstonejs/adapters": "^1.13.2",
|
||||
"@cornerstonejs/core": "^1.13.2",
|
||||
"@cornerstonejs/tools": "^1.13.2",
|
||||
"classnames": "^2.3.2"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -54,16 +50,8 @@ 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.
|
||||
@@ -91,28 +79,19 @@ 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');
|
||||
}
|
||||
|
||||
@@ -130,12 +109,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.');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ const srProtocol: Types.HangingProtocol.Protocol = {
|
||||
// Don't store this hanging protocol as it applies to the currently active
|
||||
// display set by default
|
||||
// cacheId: null,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'SR Key Images',
|
||||
// Just apply this one when specifically listed
|
||||
protocolMatchingRules: [],
|
||||
|
||||
@@ -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.
|
||||
@@ -93,11 +85,7 @@ function addInstances(
|
||||
* @param servicesManager is the services that can be used for creating
|
||||
* @returns The list of display sets created for the given instances object
|
||||
*/
|
||||
function _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
) {
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
// If the series has no instances, stop here
|
||||
if (!instances || !instances.length) {
|
||||
throw new Error('No instances were provided');
|
||||
@@ -123,8 +111,7 @@ function _getDisplaySetsFromSeries(
|
||||
|
||||
if (
|
||||
!ConceptNameCodeSequence ||
|
||||
ConceptNameCodeSequence.CodeValue !==
|
||||
CodeNameCodeSequenceValues.ImagingMeasurementReport
|
||||
ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.ImagingMeasurementReport
|
||||
) {
|
||||
servicesManager.services.uiNotificationService.show({
|
||||
title: 'DICOM SR',
|
||||
@@ -184,36 +171,21 @@ function _load(displaySet, servicesManager, extensionManager) {
|
||||
|
||||
// Check currently added displaySets and add measurements if the sources exist.
|
||||
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
||||
_checkIfCanAddMeasurementsToDisplaySet(
|
||||
displaySet,
|
||||
activeDisplaySet,
|
||||
dataSource
|
||||
);
|
||||
_checkIfCanAddMeasurementsToDisplaySet(displaySet, activeDisplaySet, dataSource);
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
srDisplaySet,
|
||||
newDisplaySet,
|
||||
dataSource
|
||||
) {
|
||||
function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource) {
|
||||
let unloadedMeasurements = srDisplaySet.measurements.filter(
|
||||
measurement => measurement.loaded === false
|
||||
);
|
||||
@@ -248,8 +220,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 +228,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
||||
});
|
||||
});
|
||||
|
||||
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(
|
||||
newDisplaySet
|
||||
);
|
||||
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
|
||||
|
||||
for (const imageId of imageIdsForDisplaySet) {
|
||||
if (!unloadedMeasurements.length) {
|
||||
@@ -267,25 +236,13 @@ 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(
|
||||
measurement,
|
||||
SOPInstanceUID,
|
||||
frameNumber
|
||||
)
|
||||
) {
|
||||
addMeasurement(
|
||||
measurement,
|
||||
imageId,
|
||||
newDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
|
||||
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
|
||||
|
||||
unloadedMeasurements.splice(j, 1);
|
||||
}
|
||||
@@ -294,11 +251,7 @@ 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
|
||||
@@ -308,8 +261,9 @@ function _measurementReferencesSOPInstanceUID(
|
||||
measurement.coords[0].ReferencedSOPSequence[0]?.ReferencedFrameNumber) ||
|
||||
1;
|
||||
|
||||
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber))
|
||||
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let j = 0; j < coords.length; j++) {
|
||||
const coord = coords[j];
|
||||
@@ -323,11 +277,7 @@ function _measurementReferencesSOPInstanceUID(
|
||||
|
||||
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(
|
||||
instances,
|
||||
servicesManager,
|
||||
extensionManager
|
||||
);
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
return [
|
||||
@@ -342,30 +292,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);
|
||||
|
||||
@@ -378,15 +320,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 =>
|
||||
@@ -395,22 +333,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.
|
||||
@@ -419,9 +351,7 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
||||
item.ConceptNameCodeSequence.CodeValue !==
|
||||
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
|
||||
) {
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||
trackingUniqueIdentifier
|
||||
].push(item);
|
||||
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier].push(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -446,18 +376,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) {
|
||||
@@ -467,9 +391,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,
|
||||
@@ -484,10 +406,7 @@ function _processTID1410Measurement(mergedContentSequence) {
|
||||
|
||||
if (MeasuredValueSequence) {
|
||||
measurement.labels.push(
|
||||
_getLabelFromMeasuredValueSequence(
|
||||
ConceptNameCodeSequence,
|
||||
MeasuredValueSequence
|
||||
)
|
||||
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -496,32 +415,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 = {
|
||||
@@ -537,8 +446,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,
|
||||
@@ -553,8 +461,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
||||
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
||||
) &&
|
||||
FindingSite.ConceptCodeSequence.CodeValue ===
|
||||
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
FindingSite.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||
);
|
||||
|
||||
if (cornerstoneFreeTextFindingSite) {
|
||||
@@ -566,18 +473,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;
|
||||
}
|
||||
@@ -590,10 +491,7 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||
|
||||
if (MeasuredValueSequence) {
|
||||
measurement.labels.push(
|
||||
_getLabelFromMeasuredValueSequence(
|
||||
ConceptNameCodeSequence,
|
||||
MeasuredValueSequence
|
||||
)
|
||||
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -633,17 +531,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,
|
||||
@@ -653,24 +546,20 @@ 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 = [];
|
||||
|
||||
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
||||
const { ReferencedSOPSequence } = item;
|
||||
if (!ReferencedSOPSequence) return;
|
||||
if (!ReferencedSOPSequence) {
|
||||
return;
|
||||
}
|
||||
for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) {
|
||||
if (ref.ReferencedSOPClassUID) {
|
||||
const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref;
|
||||
@@ -687,7 +576,9 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
||||
}
|
||||
|
||||
function _getSequenceAsArray(sequence) {
|
||||
if (!sequence) return [];
|
||||
if (!sequence) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(sequence) ? sequence : [sequence];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||
import getHangingProtocolModule, {
|
||||
srProtocol,
|
||||
} from './getHangingProtocolModule';
|
||||
import getHangingProtocolModule, { 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 [
|
||||
{
|
||||
|
||||
@@ -18,9 +18,7 @@ import toolNames from './tools/toolNames';
|
||||
/**
|
||||
* @param {object} configuration
|
||||
*/
|
||||
export default function init({
|
||||
configuration = {},
|
||||
}: Types.Extensions.ExtensionParams): void {
|
||||
export default function init({ configuration = {} }: Types.Extensions.ExtensionParams): void {
|
||||
addTool(DICOMSRDisplayTool);
|
||||
addToolInstance(toolNames.SRLength, LengthTool, {});
|
||||
addToolInstance(toolNames.SRBidirectional, BidirectionalTool);
|
||||
|
||||
@@ -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?.cachedStats?.TrackingUniqueIdentifier)
|
||||
);
|
||||
|
||||
if (!viewport._actors?.size) {
|
||||
@@ -138,8 +121,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}`);
|
||||
@@ -221,15 +203,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 +224,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,23 +282,19 @@ 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 = <Array<Types.Point2>>(
|
||||
utilities.math.ellipse.getCanvasEllipseCorners([
|
||||
canvasCoordinates[2],
|
||||
canvasCoordinates[3],
|
||||
canvasCoordinates[0],
|
||||
canvasCoordinates[1],
|
||||
])
|
||||
);
|
||||
canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners([
|
||||
canvasCoordinates[2],
|
||||
canvasCoordinates[3],
|
||||
canvasCoordinates[0],
|
||||
canvasCoordinates[1],
|
||||
]) as Array<Types.Point2>;
|
||||
} else {
|
||||
canvasCorners = <Array<Types.Point2>>(
|
||||
utilities.math.ellipse.getCanvasEllipseCorners(canvasCoordinates)
|
||||
);
|
||||
canvasCorners = utilities.math.ellipse.getCanvasEllipseCorners(
|
||||
canvasCoordinates
|
||||
) as Array<Types.Point2>;
|
||||
}
|
||||
|
||||
const lineUID = `${index}`;
|
||||
@@ -368,23 +337,14 @@ 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
|
||||
);
|
||||
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(
|
||||
|
||||
@@ -8,11 +8,7 @@ const EPSILON = 1e-4;
|
||||
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
|
||||
export default function addMeasurement(
|
||||
measurement,
|
||||
imageId,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
export default function addMeasurement(measurement, imageId, displaySetInstanceUID) {
|
||||
// TODO -> Render rotated ellipse .
|
||||
const toolName = toolNames.DICOMSRDisplay;
|
||||
|
||||
@@ -31,12 +27,7 @@ export default function addMeasurement(
|
||||
}
|
||||
|
||||
measurementData.renderableData[GraphicType].push(
|
||||
_getRenderableData(
|
||||
GraphicType,
|
||||
GraphicData,
|
||||
imageId,
|
||||
measurement.TrackingIdentifier
|
||||
)
|
||||
_getRenderableData(GraphicType, GraphicData, imageId, measurement.TrackingIdentifier)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -86,12 +77,7 @@ export default function addMeasurement(
|
||||
delete measurement.coords;
|
||||
}
|
||||
|
||||
function _getRenderableData(
|
||||
GraphicType,
|
||||
GraphicData,
|
||||
imageId,
|
||||
TrackingIdentifier
|
||||
) {
|
||||
function _getRenderableData(GraphicType, GraphicData, imageId, TrackingIdentifier) {
|
||||
const [cornerstoneTag, toolName] = TrackingIdentifier.split(':');
|
||||
|
||||
let renderableData: csTypes.Point3[];
|
||||
@@ -207,38 +193,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,12 +3,11 @@ 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
|
||||
);
|
||||
if (!referencedDisplaySet.images) return;
|
||||
const { displaySetInstanceUID, ReferencedSOPInstanceUID: sopUid } = measurement;
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
if (!referencedDisplaySet.images) {
|
||||
return;
|
||||
}
|
||||
return referencedDisplaySet.images.find(it => it.SOPInstanceUID === sopUid);
|
||||
};
|
||||
|
||||
@@ -16,16 +15,17 @@ 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) {
|
||||
const { imageId } = measurement;
|
||||
if (!imageId) continue;
|
||||
if (instanceById[imageId]) continue;
|
||||
if (!imageId) {
|
||||
continue;
|
||||
}
|
||||
if (instanceById[imageId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const instance = findInstance(measurement, displaySetService);
|
||||
if (!instance) {
|
||||
@@ -50,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,
|
||||
|
||||
@@ -9,11 +9,12 @@ const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => {
|
||||
let instanceFound;
|
||||
|
||||
displaySets.find(displaySet => {
|
||||
if (!displaySet.images) return false;
|
||||
if (!displaySet.images) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { utilities, metaData } from '@cornerstonejs/core';
|
||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||
import getLabelFromDCMJSImportedToolData from './getLabelFromDCMJSImportedToolData';
|
||||
import { adaptersSR } from '@cornerstonejs/adapters';
|
||||
import { annotation as CsAnnotation } from '@cornerstonejs/tools';
|
||||
const { locking } = CsAnnotation;
|
||||
|
||||
const { guid } = OHIF.utils;
|
||||
const { MeasurementReport, CORNERSTONE_3D_TAG } = adaptersSR.Cornerstone3D;
|
||||
@@ -12,20 +14,26 @@ const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
||||
const supportedLegacyCornerstoneTags = ['cornerstoneTools@^4.0.0'];
|
||||
|
||||
const convertCode = (codingValues, code) => {
|
||||
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') return;
|
||||
if (!code || code.CodingSchemeDesignator === 'CORNERSTONEJS') {
|
||||
return;
|
||||
}
|
||||
const ref = `${code.CodingSchemeDesignator}:${code.CodeValue}`;
|
||||
const ret = { ...codingValues[ref], ref, ...code, text: code.CodeMeaning };
|
||||
return ret;
|
||||
};
|
||||
|
||||
const convertSites = (codingValues, sites) => {
|
||||
if (!sites || !sites.length) return;
|
||||
if (!sites || !sites.length) {
|
||||
return;
|
||||
}
|
||||
const ret = [];
|
||||
// Do as a loop to convert away from Proxy instances
|
||||
for (let i = 0; i < sites.length; i++) {
|
||||
// Deal with irregular conversion from dcmjs
|
||||
const site = convertCode(codingValues, sites[i][0] || sites[i]);
|
||||
if (site) ret.push(site);
|
||||
if (site) {
|
||||
ret.push(site);
|
||||
}
|
||||
}
|
||||
return (ret.length && ret) || undefined;
|
||||
};
|
||||
@@ -35,23 +43,16 @@ const convertSites = (codingValues, sites) => {
|
||||
*
|
||||
*/
|
||||
export default function hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
{ servicesManager, extensionManager, appConfig },
|
||||
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(
|
||||
@@ -115,16 +116,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];
|
||||
@@ -140,10 +139,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);
|
||||
@@ -152,23 +148,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];
|
||||
@@ -198,21 +190,13 @@ 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);
|
||||
|
||||
measurementService.addRawMeasurement(
|
||||
const newAnnotationUID = measurementService.addRawMeasurement(
|
||||
source,
|
||||
annotationType,
|
||||
{ annotation },
|
||||
@@ -220,6 +204,11 @@ export default function hydrateStructuredReport(
|
||||
dataSource
|
||||
);
|
||||
|
||||
if (disableEditing) {
|
||||
const addedAnnotation = annotationManager.getAnnotation(newAnnotationUID);
|
||||
locking.setAnnotationLocked(addedAnnotation, true);
|
||||
}
|
||||
|
||||
if (!imageIds.includes(imageId)) {
|
||||
imageIds.push(imageId);
|
||||
}
|
||||
@@ -245,15 +234,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 = [];
|
||||
|
||||
@@ -263,13 +251,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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -7,11 +7,11 @@ import { setTrackingUniqueIdentifiersForElement } from '../tools/modules/dicomSR
|
||||
|
||||
import { Icon, Tooltip, useViewportGrid, ViewportActionBar } from '@ohif/ui';
|
||||
import hydrateStructuredReport from '../utils/hydrateStructuredReport';
|
||||
import { useAppConfig } from '@state';
|
||||
|
||||
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';
|
||||
|
||||
@@ -20,18 +20,18 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
children,
|
||||
dataSource,
|
||||
displaySets,
|
||||
viewportIndex,
|
||||
viewportLabel,
|
||||
viewportOptions,
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
measurementService,
|
||||
} = servicesManager.services;
|
||||
const [appConfig] = useAppConfig();
|
||||
|
||||
const { displaySetService, cornerstoneViewportService, measurementService } =
|
||||
servicesManager.services;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
|
||||
// SR viewport will always have a single display set
|
||||
if (displaySets.length > 1) {
|
||||
@@ -43,15 +43,10 @@ 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;
|
||||
|
||||
// Optional hook into tracking extension, if present.
|
||||
let trackedMeasurements;
|
||||
@@ -76,16 +71,14 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
sendTrackedMeasurementsEvent = (eventName, { displaySetInstanceUID }) => {
|
||||
measurementService.clearMeasurements();
|
||||
const { SeriesInstanceUIDs } = hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager },
|
||||
{ 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],
|
||||
},
|
||||
]);
|
||||
@@ -123,11 +116,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
|
||||
const updateViewport = useCallback(
|
||||
newMeasurementSelected => {
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
sopClassUids,
|
||||
} = srDisplaySet;
|
||||
const { StudyInstanceUID, displaySetInstanceUID, sopClassUids } = srDisplaySet;
|
||||
|
||||
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
||||
return;
|
||||
@@ -136,9 +125,7 @@ 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.');
|
||||
}
|
||||
|
||||
_getViewportReferencedDisplaySetData(
|
||||
@@ -160,19 +147,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);
|
||||
@@ -180,7 +159,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
}
|
||||
});
|
||||
},
|
||||
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportIndex]
|
||||
[dataSource, srDisplaySet, activeImageDisplaySetData, viewportId]
|
||||
);
|
||||
|
||||
const getCornerstoneViewport = useCallback(() => {
|
||||
@@ -225,9 +204,10 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
}}
|
||||
onElementEnabled={onElementEnabled}
|
||||
initialImageIndex={initialImageIndex}
|
||||
isJumpToMeasurementDisabled={true}
|
||||
></Component>
|
||||
);
|
||||
}, [activeImageDisplaySetData, viewportIndex, measurementSelected]);
|
||||
}, [activeImageDisplaySetData, viewportId, measurementSelected]);
|
||||
|
||||
const onMeasurementChange = useCallback(
|
||||
direction => {
|
||||
@@ -250,12 +230,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
setTrackingIdentifiers(newMeasurementSelected);
|
||||
updateViewport(newMeasurementSelected);
|
||||
},
|
||||
[
|
||||
measurementSelected,
|
||||
measurementCount,
|
||||
updateViewport,
|
||||
setTrackingIdentifiers,
|
||||
]
|
||||
[measurementSelected, measurementCount, updateViewport, setTrackingIdentifiers]
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -265,12 +240,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[activeViewportId];
|
||||
if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) {
|
||||
viewportGridService.setDisplaySetsForViewport({
|
||||
viewportIndex: activeViewportIndex,
|
||||
viewportId: activeViewportId,
|
||||
displaySetInstanceUIDs: [],
|
||||
});
|
||||
}
|
||||
@@ -337,7 +310,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
@@ -369,7 +342,7 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
getStatusComponent={() =>
|
||||
_getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
isTracked: false,
|
||||
isRehydratable: srDisplaySet.isRehydratable,
|
||||
isLocked,
|
||||
@@ -383,23 +356,19 @@ function OHIFCornerstoneSRViewport(props) {
|
||||
currentSeries: SeriesNumber,
|
||||
seriesDescription: SeriesDescription || '',
|
||||
patientInformation: {
|
||||
patientName: PatientName
|
||||
? OHIF.utils.formatPN(PatientName.Alphabetic)
|
||||
: '',
|
||||
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`
|
||||
: '',
|
||||
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>
|
||||
@@ -409,7 +378,7 @@ 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,
|
||||
@@ -434,9 +403,7 @@ async function _getViewportReferencedDisplaySetData(
|
||||
|
||||
const { displaySetInstanceUID } = measurement;
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
const image0 = referencedDisplaySet.images[0];
|
||||
const referencedDisplaySetMetadata = {
|
||||
@@ -458,7 +425,7 @@ async function _getViewportReferencedDisplaySetData(
|
||||
|
||||
function _getStatusComponent({
|
||||
srDisplaySet,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
isRehydratable,
|
||||
isLocked,
|
||||
sendTrackedMeasurementsEvent,
|
||||
@@ -466,7 +433,7 @@ function _getStatusComponent({
|
||||
const handleMouseUp = () => {
|
||||
sendTrackedMeasurementsEvent('HYDRATE_SR', {
|
||||
displaySetInstanceUID: srDisplaySet.displaySetInstanceUID,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -476,8 +443,7 @@ function _getStatusComponent({
|
||||
// 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;
|
||||
|
||||
@@ -507,22 +473,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}
|
||||
>
|
||||
@@ -535,7 +504,10 @@ function _getStatusComponent({
|
||||
return (
|
||||
<>
|
||||
{ToolTipMessage && (
|
||||
<Tooltip content={<ToolTipMessage />} position="bottom-left">
|
||||
<Tooltip
|
||||
content={<ToolTipMessage />}
|
||||
position="bottom-left"
|
||||
>
|
||||
<StatusArea />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -40,13 +40,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,
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [3.7.0-beta.73](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.72...v3.7.0-beta.73) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.72](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.71...v3.7.0-beta.72) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.71](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.70...v3.7.0-beta.71) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.70](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.69...v3.7.0-beta.70) (2023-09-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.69](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.68...v3.7.0-beta.69) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.68](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.67...v3.7.0-beta.68) (2023-09-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.67](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.66...v3.7.0-beta.67) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.66](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.65...v3.7.0-beta.66) (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.65](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.64...v3.7.0-beta.65) (2023-09-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ImageOverlayViewerTool:** add ImageOverlayViewer tool that can render image overlay (pixel overlay) of the DICOM images ([#3163](https://github.com/OHIF/Viewers/issues/3163)) ([69115da](https://github.com/OHIF/Viewers/commit/69115da06d2d437b57e66608b435bb0bc919a90f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.64](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.63...v3.7.0-beta.64) (2023-09-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.63](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.62...v3.7.0-beta.63) (2023-09-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **grid:** remove viewportIndex and only rely on viewportId ([#3591](https://github.com/OHIF/Viewers/issues/3591)) ([4c6ff87](https://github.com/OHIF/Viewers/commit/4c6ff873e887cc30ffc09223f5cb99e5f94c9cdd))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.62](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.61...v3.7.0-beta.62) (2023-08-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **data source UI config:** Popup the configuration dialogue whenever a data source is not fully configured ([#3620](https://github.com/OHIF/Viewers/issues/3620)) ([adedc8c](https://github.com/OHIF/Viewers/commit/adedc8c382e18a2e86a569e3d023cc55a157363f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.61](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.60...v3.7.0-beta.61) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.60](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.59...v3.7.0-beta.60) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.59](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.58...v3.7.0-beta.59) (2023-08-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.58](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.57...v3.7.0-beta.58) (2023-08-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.7.0-beta.57](https://github.com/OHIF/Viewers/compare/v3.7.0-beta.56...v3.7.0-beta.57) (2023-08-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **memory leak:** array buffer was sticking around in volume viewports ([#3611](https://github.com/OHIF/Viewers/issues/3611)) ([65b49ae](https://github.com/OHIF/Viewers/commit/65b49aeb1b5f38224e4892bdf32453500ee351f8))
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "3.7.0-beta.2",
|
||||
"version": "3.7.0-beta.73",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -36,9 +36,9 @@
|
||||
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjpeg": "^1.2.2",
|
||||
"@cornerstonejs/codec-openjph": "^2.4.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.1.8",
|
||||
"@ohif/core": "3.7.0-beta.2",
|
||||
"@ohif/ui": "3.7.0-beta.2",
|
||||
"@cornerstonejs/dicom-image-loader": "^1.13.2",
|
||||
"@ohif/core": "3.7.0-beta.73",
|
||||
"@ohif/ui": "3.7.0-beta.73",
|
||||
"dcmjs": "^0.29.6",
|
||||
"dicom-parser": "^1.8.21",
|
||||
"hammerjs": "^2.0.8",
|
||||
@@ -52,10 +52,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@cornerstonejs/adapters": "^1.1.8",
|
||||
"@cornerstonejs/core": "^1.1.8",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.1.8",
|
||||
"@cornerstonejs/tools": "^1.1.8",
|
||||
"@cornerstonejs/adapters": "^1.13.2",
|
||||
"@cornerstonejs/core": "^1.13.2",
|
||||
"@cornerstonejs/streaming-image-volume-loader": "^1.13.2",
|
||||
"@cornerstonejs/tools": "^1.13.2",
|
||||
"@kitware/vtk.js": "27.3.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"lodash.debounce": "4.0.8",
|
||||
|
||||
@@ -10,17 +10,8 @@ import {
|
||||
utilities as csUtils,
|
||||
} from '@cornerstonejs/core';
|
||||
import { MeasurementService } from '@ohif/core';
|
||||
import {
|
||||
CinePlayer,
|
||||
useCine,
|
||||
useViewportGrid,
|
||||
Notification,
|
||||
useViewportDialog,
|
||||
} from '@ohif/ui';
|
||||
import {
|
||||
IStackViewport,
|
||||
IVolumeViewport,
|
||||
} from '@cornerstonejs/core/dist/esm/types';
|
||||
import { Notification, useViewportDialog } from '@ohif/ui';
|
||||
import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/dist/esm/types';
|
||||
|
||||
import { setEnabledElement } from '../state';
|
||||
|
||||
@@ -28,6 +19,8 @@ import './OHIFCornerstoneViewport.css';
|
||||
import CornerstoneOverlays from './Overlays/CornerstoneOverlays';
|
||||
import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
|
||||
import CornerstoneServices from '../types/CornerstoneServices';
|
||||
import CinePlayer from '../components/CinePlayer';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
const STACK = 'stack';
|
||||
|
||||
@@ -46,24 +39,19 @@ function areEqual(prevProps, nextProps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.viewportOptions.orientation !==
|
||||
nextProps.viewportOptions.orientation
|
||||
) {
|
||||
if (prevProps.viewportOptions.orientation !== nextProps.viewportOptions.orientation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.viewportOptions.toolGroupId !==
|
||||
nextProps.viewportOptions.toolGroupId
|
||||
) {
|
||||
if (prevProps.viewportOptions.toolGroupId !== nextProps.viewportOptions.toolGroupId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.viewportOptions.viewportType !==
|
||||
nextProps.viewportOptions.viewportType
|
||||
) {
|
||||
if (prevProps.viewportOptions.viewportType !== nextProps.viewportOptions.viewportType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextProps.viewportOptions.needsRerendering) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -79,8 +67,7 @@ function areEqual(prevProps, nextProps) {
|
||||
|
||||
const foundDisplaySet = nextDisplaySets.find(
|
||||
nextDisplaySet =>
|
||||
nextDisplaySet.displaySetInstanceUID ===
|
||||
prevDisplaySet.displaySetInstanceUID
|
||||
nextDisplaySet.displaySetInstanceUID === prevDisplaySet.displaySetInstanceUID
|
||||
);
|
||||
|
||||
if (!foundDisplaySet) {
|
||||
@@ -95,9 +82,7 @@ function areEqual(prevProps, nextProps) {
|
||||
// check if their imageIds are the same
|
||||
if (foundDisplaySet.images?.length) {
|
||||
for (let j = 0; j < foundDisplaySet.images.length; j++) {
|
||||
if (
|
||||
foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId
|
||||
) {
|
||||
if (foundDisplaySet.images[j].imageId !== prevDisplaySet.images[j].imageId) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -111,25 +96,24 @@ function areEqual(prevProps, nextProps) {
|
||||
// Then we don't need to worry about the re-renders if the props change.
|
||||
const OHIFCornerstoneViewport = React.memo(props => {
|
||||
const {
|
||||
viewportIndex,
|
||||
displaySets,
|
||||
dataSource,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
onElementEnabled,
|
||||
onElementDisabled,
|
||||
isJumpToMeasurementDisabled,
|
||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||
// to set the initial state of the viewport's first image to render
|
||||
initialImageIndex,
|
||||
} = props;
|
||||
|
||||
const viewportId = viewportOptions.viewportId;
|
||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||
const [{ activeViewportIndex }] = useViewportGrid();
|
||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||
|
||||
const elementRef = useRef();
|
||||
|
||||
const {
|
||||
@@ -145,74 +129,6 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
|
||||
const [viewportDialogState] = useViewportDialog();
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = cine.isPlaying || false;
|
||||
const frameRate = cine.frameRate || 24;
|
||||
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
if (isPlaying) {
|
||||
cineService.playClip(enabledVPElement, {
|
||||
framesPerSecond: validFrameRate,
|
||||
});
|
||||
} else {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
|
||||
return () => {
|
||||
cineService.setCine({ id: viewportIndex, isPlaying: false });
|
||||
eventTarget.removeEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_NEW_STACK,
|
||||
cineHandler
|
||||
);
|
||||
};
|
||||
}, [enabledVPElement]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cines || !cines[viewportIndex] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
if (enabledVPElement && cines?.[viewportIndex]?.isPlaying) {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
}
|
||||
};
|
||||
}, [cines, viewportIndex, cineService, enabledVPElement, cineHandler]);
|
||||
|
||||
const cine = cines[viewportIndex];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
|
||||
const handleCineClose = () => {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'MoreTools',
|
||||
itemId: 'cine',
|
||||
interactionType: 'toggle',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
// useCallback for scroll bar height calculation
|
||||
const setImageScrollBarHeight = useCallback(() => {
|
||||
const scrollbarHeight = `${elementRef.current.clientHeight - 20}px`;
|
||||
@@ -227,54 +143,17 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
}
|
||||
}, [elementRef]);
|
||||
|
||||
const storePresentation = () => {
|
||||
const currentPresentation = cornerstoneViewportService.getPresentation(
|
||||
viewportIndex
|
||||
);
|
||||
if (!currentPresentation || !currentPresentation.presentationIds) return;
|
||||
const {
|
||||
lutPresentationStore,
|
||||
positionPresentationStore,
|
||||
} = stateSyncService.getState();
|
||||
const { presentationIds } = currentPresentation;
|
||||
const { lutPresentationId, positionPresentationId } = presentationIds || {};
|
||||
const storeState = {};
|
||||
if (lutPresentationId) {
|
||||
storeState.lutPresentationStore = {
|
||||
...lutPresentationStore,
|
||||
[lutPresentationId]: currentPresentation,
|
||||
};
|
||||
}
|
||||
if (positionPresentationId) {
|
||||
storeState.positionPresentationStore = {
|
||||
...positionPresentationStore,
|
||||
[positionPresentationId]: currentPresentation,
|
||||
};
|
||||
}
|
||||
stateSyncService.store(storeState);
|
||||
};
|
||||
const cleanUpServices = useCallback(
|
||||
viewportInfo => {
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
const syncGroups = viewportInfo.getSyncGroups();
|
||||
|
||||
const cleanUpServices = useCallback(() => {
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
||||
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
const syncGroups = viewportInfo.getSyncGroups();
|
||||
|
||||
toolGroupService.removeViewportFromToolGroup(viewportId, renderingEngineId);
|
||||
|
||||
syncGroupService.removeViewportFromSyncGroup(
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
syncGroups
|
||||
);
|
||||
}, [viewportIndex, viewportOptions.viewportId]);
|
||||
syncGroupService.removeViewportFromSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||
},
|
||||
[viewportId]
|
||||
);
|
||||
|
||||
const elementEnabledHandler = useCallback(
|
||||
evt => {
|
||||
@@ -284,71 +163,48 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
}
|
||||
|
||||
const { viewportId, element } = evt.detail;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(
|
||||
viewportId
|
||||
);
|
||||
const viewportIndex = viewportInfo.getViewportIndex();
|
||||
|
||||
setEnabledElement(viewportIndex, element);
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
setEnabledElement(viewportId, element);
|
||||
setEnabledVPElement(element);
|
||||
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
const toolGroupId = viewportInfo.getToolGroupId();
|
||||
const syncGroups = viewportInfo.getSyncGroups();
|
||||
|
||||
toolGroupService.addViewportToToolGroup(
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
toolGroupId
|
||||
);
|
||||
toolGroupService.addViewportToToolGroup(viewportId, renderingEngineId, toolGroupId);
|
||||
|
||||
syncGroupService.addViewportToSyncGroup(
|
||||
viewportId,
|
||||
renderingEngineId,
|
||||
syncGroups
|
||||
);
|
||||
syncGroupService.addViewportToSyncGroup(viewportId, renderingEngineId, syncGroups);
|
||||
|
||||
if (onElementEnabled) {
|
||||
onElementEnabled(evt);
|
||||
}
|
||||
},
|
||||
[viewportIndex, onElementEnabled, toolGroupService]
|
||||
[viewportId, onElementEnabled, toolGroupService]
|
||||
);
|
||||
|
||||
// disable the element upon unmounting
|
||||
useEffect(() => {
|
||||
cornerstoneViewportService.enableViewport(
|
||||
viewportIndex,
|
||||
viewportOptions,
|
||||
elementRef.current
|
||||
);
|
||||
cornerstoneViewportService.enableViewport(viewportId, elementRef.current);
|
||||
|
||||
eventTarget.addEventListener(
|
||||
Enums.Events.ELEMENT_ENABLED,
|
||||
elementEnabledHandler
|
||||
);
|
||||
eventTarget.addEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
||||
|
||||
setImageScrollBarHeight();
|
||||
|
||||
return () => {
|
||||
storePresentation();
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
cleanUpServices();
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
cornerstoneViewportService.disableElement(viewportIndex);
|
||||
cleanUpServices(viewportInfo);
|
||||
cornerstoneViewportService.storePresentation({ viewportId });
|
||||
|
||||
if (onElementDisabled) {
|
||||
onElementDisabled(viewportInfo);
|
||||
}
|
||||
|
||||
eventTarget.removeEventListener(
|
||||
Enums.Events.ELEMENT_ENABLED,
|
||||
elementEnabledHandler
|
||||
);
|
||||
eventTarget.removeEventListener(Enums.Events.ELEMENT_ENABLED, elementEnabledHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -363,10 +219,15 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = displaySetService.subscribe(
|
||||
displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED,
|
||||
async invalidatedDisplaySetInstanceUID => {
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
async ({
|
||||
displaySetInstanceUID: invalidatedDisplaySetInstanceUID,
|
||||
invalidateData,
|
||||
}: Types.DisplaySetSeriesMetadataInvalidatedEvent) => {
|
||||
if (!invalidateData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
if (viewportInfo.hasDisplaySet(invalidatedDisplaySetInstanceUID)) {
|
||||
const viewportData = viewportInfo.getViewportData();
|
||||
@@ -378,18 +239,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
);
|
||||
|
||||
const keepCamera = true;
|
||||
cornerstoneViewportService.updateViewport(
|
||||
viewportIndex,
|
||||
newViewportData,
|
||||
keepCamera
|
||||
);
|
||||
cornerstoneViewportService.updateViewport(viewportId, newViewportData, keepCamera);
|
||||
}
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewportIndex]);
|
||||
}, [viewportId]);
|
||||
|
||||
useEffect(() => {
|
||||
// handle the default viewportType to be stack
|
||||
@@ -408,33 +265,37 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
// The presentation state will have been stored previously by closing
|
||||
// a viewport. Otherwise, this viewport will be unchanged and the
|
||||
// presentation information will be directly carried over.
|
||||
const {
|
||||
lutPresentationStore,
|
||||
positionPresentationStore,
|
||||
} = stateSyncService.getState();
|
||||
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
|
||||
const { presentationIds } = viewportOptions;
|
||||
const presentations = {
|
||||
positionPresentation:
|
||||
positionPresentationStore[presentationIds?.positionPresentationId],
|
||||
lutPresentation:
|
||||
lutPresentationStore[presentationIds?.lutPresentationId],
|
||||
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
||||
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
|
||||
};
|
||||
let measurement;
|
||||
if (cacheJumpToMeasurementEvent?.viewportIndex === viewportIndex) {
|
||||
if (cacheJumpToMeasurementEvent?.viewportId === viewportId) {
|
||||
measurement = cacheJumpToMeasurementEvent.measurement;
|
||||
// Delete the position presentation so that viewport navigates direct
|
||||
presentations.positionPresentation = null;
|
||||
cacheJumpToMeasurementEvent = null;
|
||||
}
|
||||
|
||||
// Note: This is a hack to get the grid to re-render the OHIFCornerstoneViewport component
|
||||
// Used for segmentation hydration right now, since the logic to decide whether
|
||||
// a viewport needs to render a segmentation lives inside the CornerstoneViewportService
|
||||
// so we need to re-render (force update via change of the needsRerendering) so that React
|
||||
// does the diffing and decides we should render this again (although the id and element has not changed)
|
||||
// so that the CornerstoneViewportService can decide whether to render the segmentation or not. Not that we reached here we can turn it off.
|
||||
if (viewportOptions.needsRerendering) {
|
||||
viewportOptions.needsRerendering = false;
|
||||
}
|
||||
|
||||
cornerstoneViewportService.setViewportData(
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
viewportData,
|
||||
viewportOptions,
|
||||
displaySetOptions,
|
||||
presentations
|
||||
);
|
||||
|
||||
if (measurement) {
|
||||
cs3DTools.annotation.selection.setAnnotationSelected(measurement.uid);
|
||||
}
|
||||
@@ -454,11 +315,15 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
* the cache for jumping to see if there is any jump queued, then we jump to the correct slice.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isJumpToMeasurementDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unsubscribeFromJumpToMeasurementEvents = _subscribeToJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
@@ -468,7 +333,7 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
@@ -477,17 +342,14 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
return () => {
|
||||
unsubscribeFromJumpToMeasurementEvents();
|
||||
};
|
||||
}, [displaySets, elementRef, viewportIndex]);
|
||||
}, [displaySets, elementRef, viewportId]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="viewport-wrapper">
|
||||
<ReactResizeDetector
|
||||
handleWidth
|
||||
handleHeight
|
||||
skipOnMount={true} // Todo: make these configurable
|
||||
refreshMode={'debounce'}
|
||||
refreshRate={200} // transition amount in side panel
|
||||
refreshMode="debounce"
|
||||
refreshRate={50} // Wait 50 ms after last move to render
|
||||
onResize={onResize}
|
||||
targetRef={elementRef.current}
|
||||
/>
|
||||
@@ -499,34 +361,20 @@ const OHIFCornerstoneViewport = React.memo(props => {
|
||||
ref={elementRef}
|
||||
></div>
|
||||
<CornerstoneOverlays
|
||||
viewportIndex={viewportIndex}
|
||||
viewportId={viewportId}
|
||||
toolBarService={toolbarService}
|
||||
element={elementRef.current}
|
||||
scrollbarHeight={scrollbarHeight}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
{isCineEnabled && (
|
||||
<CinePlayer
|
||||
className="absolute left-1/2 -translate-x-1/2 bottom-3"
|
||||
isPlaying={isPlaying}
|
||||
onClose={handleCineClose}
|
||||
onPlayPauseChange={isPlaying =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
isPlaying,
|
||||
})
|
||||
}
|
||||
onFrameRateChange={frameRate =>
|
||||
cineService.setCine({
|
||||
id: activeViewportIndex,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<CinePlayer
|
||||
enabledVPElement={enabledVPElement}
|
||||
viewportId={viewportId}
|
||||
servicesManager={servicesManager}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute w-full">
|
||||
{viewportDialogState.viewportIndex === viewportIndex && (
|
||||
{viewportDialogState.viewportId === viewportId && (
|
||||
<Notification
|
||||
id="viewport-notification"
|
||||
message={viewportDialogState.message}
|
||||
@@ -545,35 +393,35 @@ function _subscribeToJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
const displaysUIDs = displaySets.map(
|
||||
displaySet => displaySet.displaySetInstanceUID
|
||||
);
|
||||
const { unsubscribe } = measurementService.subscribe(
|
||||
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_VIEWPORT,
|
||||
props => {
|
||||
cacheJumpToMeasurementEvent = props;
|
||||
const { viewportIndex: jumpIndex, measurement, isConsumed } = props;
|
||||
if (!measurement || isConsumed) return;
|
||||
const { viewportId: jumpId, measurement, isConsumed } = props;
|
||||
if (!measurement || isConsumed) {
|
||||
return;
|
||||
}
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport === undefined) {
|
||||
// Decide on which viewport should handle this
|
||||
cacheJumpToMeasurementEvent.cornerstoneViewport = cornerstoneViewportService.getViewportIndexToJump(
|
||||
jumpIndex,
|
||||
measurement.displaySetInstanceUID,
|
||||
{ referencedImageId: measurement.referencedImageId }
|
||||
);
|
||||
cacheJumpToMeasurementEvent.cornerstoneViewport =
|
||||
cornerstoneViewportService.getViewportIdToJump(
|
||||
jumpId,
|
||||
measurement.displaySetInstanceUID,
|
||||
{ referencedImageId: measurement.referencedImageId }
|
||||
);
|
||||
}
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportIndex) {
|
||||
if (cacheJumpToMeasurementEvent.cornerstoneViewport !== viewportId) {
|
||||
return;
|
||||
}
|
||||
_jumpToMeasurement(
|
||||
measurement,
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
@@ -590,20 +438,22 @@ function _checkForCachedJumpToMeasurementEvents(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
displaySets,
|
||||
viewportGridService,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
if (!cacheJumpToMeasurementEvent) return;
|
||||
if (!cacheJumpToMeasurementEvent) {
|
||||
return;
|
||||
}
|
||||
if (cacheJumpToMeasurementEvent.isConsumed) {
|
||||
cacheJumpToMeasurementEvent = null;
|
||||
return;
|
||||
}
|
||||
const displaysUIDs = displaySets.map(
|
||||
displaySet => displaySet.displaySetInstanceUID
|
||||
);
|
||||
if (!displaysUIDs?.length) return;
|
||||
const displaysUIDs = displaySets.map(displaySet => displaySet.displaySetInstanceUID);
|
||||
if (!displaysUIDs?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Jump to measurement if the measurement exists
|
||||
const { measurement } = cacheJumpToMeasurementEvent;
|
||||
@@ -612,7 +462,7 @@ function _checkForCachedJumpToMeasurementEvents(
|
||||
_jumpToMeasurement(
|
||||
measurement,
|
||||
elementRef,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
@@ -625,7 +475,7 @@ function _checkForCachedJumpToMeasurementEvents(
|
||||
function _jumpToMeasurement(
|
||||
measurement,
|
||||
targetElementRef,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
measurementService,
|
||||
displaySetService,
|
||||
viewportGridService,
|
||||
@@ -639,27 +489,19 @@ function _jumpToMeasurement(
|
||||
return;
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const referencedDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
// Todo: setCornerstoneMeasurementActive should be handled by the toolGroupManager
|
||||
// to set it properly
|
||||
// setCornerstoneMeasurementActive(measurement);
|
||||
|
||||
viewportGridService.setActiveViewportIndex(viewportIndex);
|
||||
viewportGridService.setActiveViewportId(viewportId);
|
||||
|
||||
const enabledElement = getEnabledElement(targetElement);
|
||||
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
if (enabledElement) {
|
||||
// See how the jumpToSlice() of Cornerstone3D deals with imageIdx param.
|
||||
const viewport = enabledElement.viewport as
|
||||
| IStackViewport
|
||||
| IVolumeViewport;
|
||||
const viewport = enabledElement.viewport as IStackViewport | IVolumeViewport;
|
||||
|
||||
let imageIdIndex = 0;
|
||||
let viewportCameraDirectionMatch = true;
|
||||
@@ -667,14 +509,9 @@ function _jumpToMeasurement(
|
||||
if (viewport instanceof StackViewport) {
|
||||
const imageIds = viewport.getImageIds();
|
||||
imageIdIndex = imageIds.findIndex(imageId => {
|
||||
const {
|
||||
SOPInstanceUID: aSOPInstanceUID,
|
||||
frameNumber: aFrameNumber,
|
||||
} = getSOPInstanceAttributes(imageId);
|
||||
return (
|
||||
aSOPInstanceUID === SOPInstanceUID &&
|
||||
(!frameNumber || frameNumber === aFrameNumber)
|
||||
);
|
||||
const { SOPInstanceUID: aSOPInstanceUID, frameNumber: aFrameNumber } =
|
||||
getSOPInstanceAttributes(imageId);
|
||||
return aSOPInstanceUID === SOPInstanceUID && (!frameNumber || frameNumber === aFrameNumber);
|
||||
});
|
||||
} else {
|
||||
// for volume viewport we can't rely on the imageIdIndex since it can be
|
||||
@@ -689,10 +526,7 @@ function _jumpToMeasurement(
|
||||
// should compare abs for both planes since the direction can be flipped
|
||||
if (
|
||||
measurementViewPlane &&
|
||||
!csUtils.isEqual(
|
||||
measurementViewPlane.map(Math.abs),
|
||||
viewportViewPlane.map(Math.abs)
|
||||
)
|
||||
!csUtils.isEqual(measurementViewPlane.map(Math.abs), viewportViewPlane.map(Math.abs))
|
||||
) {
|
||||
viewportCameraDirectionMatch = false;
|
||||
}
|
||||
@@ -716,21 +550,22 @@ function _jumpToMeasurement(
|
||||
// Component displayName
|
||||
OHIFCornerstoneViewport.displayName = 'OHIFCornerstoneViewport';
|
||||
|
||||
OHIFCornerstoneViewport.defaultProps = {
|
||||
isJumpToMeasurementDisabled: false,
|
||||
};
|
||||
|
||||
OHIFCornerstoneViewport.propTypes = {
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
displaySets: PropTypes.array.isRequired,
|
||||
dataSource: PropTypes.object.isRequired,
|
||||
viewportOptions: PropTypes.object,
|
||||
displaySetOptions: PropTypes.arrayOf(PropTypes.any),
|
||||
servicesManager: PropTypes.object.isRequired,
|
||||
onElementEnabled: PropTypes.func,
|
||||
isJumpToMeasurementDisabled: PropTypes.bool,
|
||||
// Note: you SHOULD NOT use the initialImageIdOrIndex for manipulation
|
||||
// of the imageData in the OHIFCornerstoneViewport. This prop is used
|
||||
// to set the initial state of the viewport's first image to render
|
||||
initialImageIdOrIndex: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
initialImageIdOrIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
};
|
||||
|
||||
export default OHIFCornerstoneViewport;
|
||||
@@ -6,7 +6,7 @@ import ViewportOrientationMarkers from './ViewportOrientationMarkers';
|
||||
import ViewportImageSliceLoadingIndicator from './ViewportImageSliceLoadingIndicator';
|
||||
|
||||
function CornerstoneOverlays(props) {
|
||||
const { viewportIndex, element, scrollbarHeight, servicesManager } = props;
|
||||
const { viewportId, element, scrollbarHeight, servicesManager } = props;
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
const [imageSliceData, setImageSliceData] = useState({
|
||||
imageIndex: 0,
|
||||
@@ -18,7 +18,7 @@ function CornerstoneOverlays(props) {
|
||||
const { unsubscribe } = cornerstoneViewportService.subscribe(
|
||||
cornerstoneViewportService.EVENTS.VIEWPORT_DATA_CHANGED,
|
||||
props => {
|
||||
if (props.viewportIndex !== viewportIndex) {
|
||||
if (props.viewportId !== viewportId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,15 +29,14 @@ function CornerstoneOverlays(props) {
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewportIndex]);
|
||||
}, [viewportId]);
|
||||
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (viewportData) {
|
||||
const viewportInfo =
|
||||
cornerstoneViewportService.getViewportInfoByIndex(viewportIndex);
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
if (viewportInfo?.viewportOptions?.customViewportProps?.hideOverlays) {
|
||||
return null;
|
||||
@@ -47,7 +46,7 @@ function CornerstoneOverlays(props) {
|
||||
return (
|
||||
<div className="noselect">
|
||||
<ViewportImageScrollbar
|
||||
viewportIndex={viewportIndex}
|
||||
viewportId={viewportId}
|
||||
viewportData={viewportData}
|
||||
element={element}
|
||||
imageSliceData={imageSliceData}
|
||||
@@ -59,7 +58,7 @@ function CornerstoneOverlays(props) {
|
||||
<CustomizableViewportOverlay
|
||||
imageSliceData={imageSliceData}
|
||||
viewportData={viewportData}
|
||||
viewportIndex={viewportIndex}
|
||||
viewportId={viewportId}
|
||||
servicesManager={servicesManager}
|
||||
element={element}
|
||||
/>
|
||||
@@ -74,7 +73,7 @@ function CornerstoneOverlays(props) {
|
||||
element={element}
|
||||
viewportData={viewportData}
|
||||
servicesManager={servicesManager}
|
||||
viewportIndex={viewportIndex}
|
||||
viewportId={viewportId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,7 @@ import { vec3 } from 'gl-matrix';
|
||||
import PropTypes from 'prop-types';
|
||||
import { metaData, Enums, utilities } from '@cornerstonejs/core';
|
||||
import { ViewportOverlay } from '@ohif/ui';
|
||||
import {
|
||||
formatPN,
|
||||
formatDICOMDate,
|
||||
formatDICOMTime,
|
||||
formatNumberPrecision,
|
||||
} from './utils';
|
||||
import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils';
|
||||
import { InstanceMetadata } from 'platform/core/src/types';
|
||||
import { ServicesManager } from '@ohif/core';
|
||||
import { ImageSliceData } from '@cornerstonejs/core/dist/esm/types';
|
||||
@@ -21,7 +16,6 @@ interface OverlayItemProps {
|
||||
element: any;
|
||||
viewportData: any;
|
||||
imageSliceData: ImageSliceData;
|
||||
viewportIndex: number | null;
|
||||
servicesManager: ServicesManager;
|
||||
instance: InstanceMetadata;
|
||||
customization: any;
|
||||
@@ -56,13 +50,9 @@ function VOIOverlayItem({ voi, customization }: OverlayItemProps) {
|
||||
style={{ color: (customization && customization.color) || undefined }}
|
||||
>
|
||||
<span className="mr-1 shrink-0">W:</span>
|
||||
<span className="ml-1 mr-2 font-light shrink-0">
|
||||
{windowWidth.toFixed(0)}
|
||||
</span>
|
||||
<span className="ml-1 mr-2 shrink-0 font-light">{windowWidth.toFixed(0)}</span>
|
||||
<span className="mr-1 shrink-0">L:</span>
|
||||
<span className="ml-1 font-light shrink-0">
|
||||
{windowCenter.toFixed(0)}
|
||||
</span>
|
||||
<span className="ml-1 shrink-0 font-light">{windowCenter.toFixed(0)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -114,14 +104,11 @@ function CustomizableViewportOverlay({
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
}) {
|
||||
const {
|
||||
toolbarService,
|
||||
cornerstoneViewportService,
|
||||
customizationService,
|
||||
} = servicesManager.services;
|
||||
const { toolbarService, cornerstoneViewportService, customizationService } =
|
||||
servicesManager.services;
|
||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [activeTools, setActiveTools] = useState([]);
|
||||
@@ -150,15 +137,10 @@ function CustomizableViewportOverlay({
|
||||
|
||||
const instanceNumber = useMemo(() => {
|
||||
if (viewportData != null) {
|
||||
return _getInstanceNumber(
|
||||
viewportData,
|
||||
viewportIndex,
|
||||
imageIndex,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
return _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService);
|
||||
}
|
||||
return null;
|
||||
}, [viewportData, viewportIndex, imageIndex, cornerstoneViewportService]);
|
||||
}, [viewportData, viewportId, imageIndex, cornerstoneViewportService]);
|
||||
|
||||
/**
|
||||
* Initial toolbar state
|
||||
@@ -179,10 +161,7 @@ function CustomizableViewportOverlay({
|
||||
}
|
||||
|
||||
const { lower, upper } = range;
|
||||
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
|
||||
lower,
|
||||
upper
|
||||
);
|
||||
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
|
||||
|
||||
setVOI({ windowCenter, windowWidth });
|
||||
};
|
||||
@@ -192,7 +171,7 @@ function CustomizableViewportOverlay({
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
||||
};
|
||||
}, [viewportIndex, viewportData, voi, element]);
|
||||
}, [viewportId, viewportData, voi, element]);
|
||||
|
||||
/**
|
||||
* Updating the scale when the viewport changes its zoom
|
||||
@@ -205,9 +184,7 @@ function CustomizableViewportOverlay({
|
||||
previousCamera.parallelScale !== camera.parallelScale ||
|
||||
previousCamera.scale !== camera.scale
|
||||
) {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
@@ -226,8 +203,7 @@ function CustomizableViewportOverlay({
|
||||
|
||||
const { spacing } = imageData;
|
||||
// convert parallel scale to scale
|
||||
const scale =
|
||||
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
|
||||
const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
|
||||
setScale(scale);
|
||||
}
|
||||
};
|
||||
@@ -237,7 +213,7 @@ function CustomizableViewportOverlay({
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
|
||||
};
|
||||
}, [viewportIndex, viewportData, cornerstoneViewportService, element]);
|
||||
}, [viewportId, viewportData, cornerstoneViewportService, element]);
|
||||
|
||||
/**
|
||||
* Updating the active tools when the toolbar changes
|
||||
@@ -262,7 +238,7 @@ function CustomizableViewportOverlay({
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
customization: item,
|
||||
formatters: {
|
||||
@@ -296,7 +272,7 @@ function CustomizableViewportOverlay({
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
customizationService,
|
||||
instance,
|
||||
@@ -343,9 +319,7 @@ function CustomizableViewportOverlay({
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`bottomLeftOverlayItem_${i}`}>
|
||||
{_renderOverlayItem(item)}
|
||||
</div>
|
||||
<div key={`bottomLeftOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -356,9 +330,7 @@ function CustomizableViewportOverlay({
|
||||
return (
|
||||
<>
|
||||
{items.map((item, i) => (
|
||||
<div key={`bottomRightOverlayItem_${i}`}>
|
||||
{_renderOverlayItem(item)}
|
||||
</div>
|
||||
<div key={`bottomRightOverlayItem_${i}`}>{_renderOverlayItem(item)}</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -379,7 +351,7 @@ function _getViewportInstance(viewportData, imageIndex) {
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
imageId = viewportData.data.imageIds[imageIndex];
|
||||
} else if (viewportData.viewportType === Enums.ViewportType.ORTHOGRAPHIC) {
|
||||
const volumes = viewportData.volumes;
|
||||
const volumes = viewportData.data;
|
||||
if (volumes && volumes.length == 1) {
|
||||
const volume = volumes[0];
|
||||
imageId = volume.imageIds[imageIndex];
|
||||
@@ -388,12 +360,7 @@ function _getViewportInstance(viewportData, imageIndex) {
|
||||
return imageId ? metaData.get('instance', imageId) || {} : {};
|
||||
}
|
||||
|
||||
function _getInstanceNumber(
|
||||
viewportData,
|
||||
viewportIndex,
|
||||
imageIndex,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
function _getInstanceNumber(viewportData, viewportId, imageIndex, cornerstoneViewportService) {
|
||||
let instanceNumber;
|
||||
|
||||
if (viewportData.viewportType === Enums.ViewportType.STACK) {
|
||||
@@ -406,7 +373,7 @@ function _getInstanceNumber(
|
||||
instanceNumber = _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
imageIndex,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
}
|
||||
@@ -436,12 +403,7 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
// Since volume viewports can be in any view direction, they can render
|
||||
// a reconstructed image which don't have imageIds; therefore, no instance and instanceNumber
|
||||
// Here we check if viewport is in the acquisition direction and if so, we get the instanceNumber
|
||||
function _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
imageIndex,
|
||||
viewportIndex,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
function _getInstanceNumberFromVolume(viewportData, viewportId, cornerstoneViewportService) {
|
||||
const volumes = viewportData.volumes;
|
||||
|
||||
// Todo: support fusion of acquisition plane which has instanceNumber
|
||||
@@ -452,9 +414,7 @@ function _getInstanceNumberFromVolume(
|
||||
const volume = volumes[0];
|
||||
const { direction, imageIds } = volume;
|
||||
|
||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!cornerstoneViewport) {
|
||||
return;
|
||||
@@ -477,8 +437,7 @@ function _getInstanceNumberFromVolume(
|
||||
return {};
|
||||
}
|
||||
|
||||
const { instanceNumber } =
|
||||
metaData.get('generalImageModule', imageId) || {};
|
||||
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
|
||||
return parseInt(instanceNumber);
|
||||
}
|
||||
}
|
||||
@@ -486,7 +445,7 @@ function _getInstanceNumberFromVolume(
|
||||
CustomizableViewportOverlay.propTypes = {
|
||||
viewportData: PropTypes.object,
|
||||
imageIndex: PropTypes.number,
|
||||
viewportIndex: PropTypes.number,
|
||||
viewportId: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CustomizableViewportOverlay;
|
||||
@@ -7,34 +7,24 @@ import { ServicesManger } from '@ohif/core';
|
||||
|
||||
function CornerstoneImageScrollbar({
|
||||
viewportData,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
element,
|
||||
imageSliceData,
|
||||
setImageSliceData,
|
||||
scrollbarHeight,
|
||||
servicesManager,
|
||||
}) {
|
||||
const {
|
||||
cineService,
|
||||
cornerstoneViewportService,
|
||||
} = (servicesManager as ServicesManger).services;
|
||||
const { cineService, cornerstoneViewportService } = (servicesManager as ServicesManger).services;
|
||||
|
||||
const onImageScrollbarChange = (imageIndex, viewportIndex) => {
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(
|
||||
viewportId
|
||||
);
|
||||
const onImageScrollbarChange = (imageIndex, viewportId) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
const { isCineEnabled } = cineService.getState();
|
||||
|
||||
if (isCineEnabled) {
|
||||
// on image scrollbar change, stop the CINE if it is playing
|
||||
cineService.stopClip(element);
|
||||
cineService.setCine({ id: viewportIndex, isPlaying: false });
|
||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||
}
|
||||
|
||||
csToolsUtils.jumpToSlice(viewport.element, {
|
||||
@@ -48,9 +38,7 @@ function CornerstoneImageScrollbar({
|
||||
return;
|
||||
}
|
||||
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
@@ -79,7 +67,7 @@ function CornerstoneImageScrollbar({
|
||||
const { imageIndex, numberOfSlices } = sliceData;
|
||||
setImageSliceData({ imageIndex, numberOfSlices });
|
||||
}
|
||||
}, [viewportIndex, viewportData]);
|
||||
}, [viewportId, viewportData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewportData?.viewportType !== Enums.ViewportType.STACK) {
|
||||
@@ -95,16 +83,10 @@ function CornerstoneImageScrollbar({
|
||||
});
|
||||
};
|
||||
|
||||
element.addEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_SCROLL,
|
||||
updateStackIndex
|
||||
);
|
||||
element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_SCROLL,
|
||||
updateStackIndex
|
||||
);
|
||||
element.removeEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, updateStackIndex);
|
||||
};
|
||||
}, [viewportData, element]);
|
||||
|
||||
@@ -122,19 +104,14 @@ function CornerstoneImageScrollbar({
|
||||
element.addEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(
|
||||
Enums.Events.VOLUME_NEW_IMAGE,
|
||||
updateVolumeIndex
|
||||
);
|
||||
element.removeEventListener(Enums.Events.VOLUME_NEW_IMAGE, updateVolumeIndex);
|
||||
};
|
||||
}, [viewportData, element]);
|
||||
|
||||
return (
|
||||
<ImageScrollbar
|
||||
onChange={evt => onImageScrollbarChange(evt, viewportIndex)}
|
||||
max={
|
||||
imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0
|
||||
}
|
||||
onChange={evt => onImageScrollbarChange(evt, viewportId)}
|
||||
max={imageSliceData.numberOfSlices ? imageSliceData.numberOfSlices - 1 : 0}
|
||||
height={scrollbarHeight}
|
||||
value={imageSliceData.imageIndex}
|
||||
/>
|
||||
@@ -143,7 +120,7 @@ function CornerstoneImageScrollbar({
|
||||
|
||||
CornerstoneImageScrollbar.propTypes = {
|
||||
viewportData: PropTypes.object,
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
viewportId: PropTypes.string.isRequired,
|
||||
element: PropTypes.instanceOf(Element),
|
||||
scrollbarHeight: PropTypes.string,
|
||||
imageSliceData: PropTypes.object.isRequired,
|
||||
|
||||
+8
-20
@@ -33,26 +33,14 @@ function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
element.addEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_SCROLL,
|
||||
setLoadingState
|
||||
);
|
||||
element.addEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, setLoadingState);
|
||||
element.addEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
|
||||
element.addEventListener(
|
||||
Enums.Events.STACK_NEW_IMAGE,
|
||||
setFinishLoadingState
|
||||
);
|
||||
element.addEventListener(Enums.Events.STACK_NEW_IMAGE, setFinishLoadingState);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(
|
||||
Enums.Events.STACK_VIEWPORT_SCROLL,
|
||||
setLoadingState
|
||||
);
|
||||
element.removeEventListener(Enums.Events.STACK_VIEWPORT_SCROLL, setLoadingState);
|
||||
|
||||
element.removeEventListener(
|
||||
Enums.Events.STACK_NEW_IMAGE,
|
||||
setFinishLoadingState
|
||||
);
|
||||
element.removeEventListener(Enums.Events.STACK_NEW_IMAGE, setFinishLoadingState);
|
||||
|
||||
element.removeEventListener(Enums.Events.IMAGE_LOAD_ERROR, setErrorState);
|
||||
};
|
||||
@@ -61,8 +49,8 @@ function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-black opacity-50 absolute h-full w-full top-0 left-0">
|
||||
<div className="flex transparent items-center justify-center w-full h-full">
|
||||
<div className="absolute top-0 left-0 h-full w-full bg-black opacity-50">
|
||||
<div className="transparent flex h-full w-full items-center justify-center">
|
||||
<p className="text-primary-light text-xl font-light">
|
||||
<h4>Error Loading Image</h4>
|
||||
<p>An error has occurred.</p>
|
||||
@@ -78,8 +66,8 @@ function ViewportImageSliceLoadingIndicator({ viewportData, element }) {
|
||||
return (
|
||||
// IMPORTANT: we need to use the pointer-events-none class to prevent the loading indicator from
|
||||
// interacting with the mouse, since scrolling should propagate to the viewport underneath
|
||||
<div className="pointer-events-none bg-black opacity-50 absolute h-full w-full top-0 left-0">
|
||||
<div className="flex transparent items-center justify-center w-full h-full">
|
||||
<div className="pointer-events-none absolute top-0 left-0 h-full w-full bg-black opacity-50">
|
||||
<div className="transparent flex h-full w-full items-center justify-center">
|
||||
<p className="text-primary-light text-xl font-light">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,16 +13,13 @@ import { vec3 } from 'gl-matrix';
|
||||
|
||||
import './ViewportOrientationMarkers.css';
|
||||
|
||||
const {
|
||||
getOrientationStringLPS,
|
||||
invertOrientationStringLPS,
|
||||
} = utilities.orientation;
|
||||
const { getOrientationStringLPS, invertOrientationStringLPS } = utilities.orientation;
|
||||
|
||||
function ViewportOrientationMarkers({
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
orientationMarkers = ['top', 'left'],
|
||||
}) {
|
||||
@@ -33,9 +30,7 @@ function ViewportOrientationMarkers({
|
||||
const { cornerstoneViewportService } = servicesManager.services;
|
||||
|
||||
useEffect(() => {
|
||||
const cameraModifiedListener = (
|
||||
evt: Types.EventTypes.CameraModifiedEvent
|
||||
) => {
|
||||
const cameraModifiedListener = (evt: Types.EventTypes.CameraModifiedEvent) => {
|
||||
const { rotation, previousCamera, camera } = evt.detail;
|
||||
|
||||
if (rotation !== undefined) {
|
||||
@@ -57,16 +52,10 @@ function ViewportOrientationMarkers({
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener(
|
||||
Enums.Events.CAMERA_MODIFIED,
|
||||
cameraModifiedListener
|
||||
);
|
||||
element.addEventListener(Enums.Events.CAMERA_MODIFIED, cameraModifiedListener);
|
||||
|
||||
return () => {
|
||||
element.removeEventListener(
|
||||
Enums.Events.CAMERA_MODIFIED,
|
||||
cameraModifiedListener
|
||||
);
|
||||
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, cameraModifiedListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -85,8 +74,7 @@ function ViewportOrientationMarkers({
|
||||
return false;
|
||||
}
|
||||
|
||||
({ rowCosines, columnCosines } =
|
||||
metaData.get('imagePlaneModule', imageId) || {});
|
||||
({ rowCosines, columnCosines } = metaData.get('imagePlaneModule', imageId) || {});
|
||||
} else {
|
||||
if (!element || !getEnabledElement(element)) {
|
||||
return '';
|
||||
@@ -114,9 +102,7 @@ function ViewportOrientationMarkers({
|
||||
flipHorizontal
|
||||
);
|
||||
|
||||
const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
if (!ohifViewport) {
|
||||
console.log('ViewportOrientationMarkers::No viewport');
|
||||
@@ -126,9 +112,7 @@ function ViewportOrientationMarkers({
|
||||
|
||||
// Todo: probably this can be done in a better way in which we identify bright
|
||||
// background
|
||||
const isLight = backgroundColor
|
||||
? csUtils.isEqual(backgroundColor, [1, 1, 1])
|
||||
: false;
|
||||
const isLight = backgroundColor ? csUtils.isEqual(backgroundColor, [1, 1, 1]) : false;
|
||||
|
||||
return orientationMarkers.map((m, index) => (
|
||||
<div
|
||||
@@ -174,13 +158,7 @@ ViewportOrientationMarkers.defaultProps = {
|
||||
* @param {*} rotation in degrees
|
||||
* @returns
|
||||
*/
|
||||
function _getOrientationMarkers(
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
rotation,
|
||||
flipVertical,
|
||||
flipHorizontal
|
||||
) {
|
||||
function _getOrientationMarkers(rowCosines, columnCosines, rotation, flipVertical, flipHorizontal) {
|
||||
const rowString = getOrientationStringLPS(rowCosines);
|
||||
const columnString = getOrientationStringLPS(columnCosines);
|
||||
const oppositeRowString = invertOrientationStringLPS(rowString);
|
||||
|
||||
@@ -11,13 +11,10 @@ function CornerstoneViewportOverlay({
|
||||
element,
|
||||
viewportData,
|
||||
imageSliceData,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
servicesManager,
|
||||
}) {
|
||||
const {
|
||||
cornerstoneViewportService,
|
||||
toolbarService,
|
||||
} = servicesManager.services;
|
||||
const { cornerstoneViewportService, toolbarService } = servicesManager.services;
|
||||
const [voi, setVOI] = useState({ windowCenter: null, windowWidth: null });
|
||||
const [scale, setScale] = useState(1);
|
||||
const [activeTools, setActiveTools] = useState([]);
|
||||
@@ -60,10 +57,7 @@ function CornerstoneViewportOverlay({
|
||||
}
|
||||
|
||||
const { lower, upper } = range;
|
||||
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(
|
||||
lower,
|
||||
upper
|
||||
);
|
||||
const { windowWidth, windowCenter } = utilities.windowLevel.toWindowLevel(lower, upper);
|
||||
|
||||
setVOI({ windowCenter, windowWidth });
|
||||
};
|
||||
@@ -73,7 +67,7 @@ function CornerstoneViewportOverlay({
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.VOI_MODIFIED, updateVOI);
|
||||
};
|
||||
}, [viewportIndex, viewportData, voi, element]);
|
||||
}, [viewportId, viewportData, voi, element]);
|
||||
|
||||
/**
|
||||
* Updating the scale when the viewport changes its zoom
|
||||
@@ -86,9 +80,7 @@ function CornerstoneViewportOverlay({
|
||||
previousCamera.parallelScale !== camera.parallelScale ||
|
||||
previousCamera.scale !== camera.scale
|
||||
) {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
@@ -107,8 +99,7 @@ function CornerstoneViewportOverlay({
|
||||
|
||||
const { spacing } = imageData;
|
||||
// convert parallel scale to scale
|
||||
const scale =
|
||||
(element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
|
||||
const scale = (element.clientHeight * spacing[0] * 0.5) / camera.parallelScale;
|
||||
setScale(scale);
|
||||
}
|
||||
};
|
||||
@@ -118,7 +109,7 @@ function CornerstoneViewportOverlay({
|
||||
return () => {
|
||||
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, updateScale);
|
||||
};
|
||||
}, [viewportIndex, viewportData]);
|
||||
}, [viewportId, viewportData]);
|
||||
|
||||
const getTopLeftContent = useCallback(() => {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
@@ -168,7 +159,7 @@ function CornerstoneViewportOverlay({
|
||||
instanceNumber = _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
imageIndex,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
cornerstoneViewportService
|
||||
);
|
||||
}
|
||||
@@ -183,15 +174,13 @@ function CornerstoneViewportOverlay({
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}, [imageSliceData, viewportData, viewportIndex]);
|
||||
}, [imageSliceData, viewportData, viewportId]);
|
||||
|
||||
if (!viewportData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ohifViewport = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
if (!ohifViewport) {
|
||||
return null;
|
||||
@@ -201,9 +190,7 @@ function CornerstoneViewportOverlay({
|
||||
|
||||
// Todo: probably this can be done in a better way in which we identify bright
|
||||
// background
|
||||
const isLight = backgroundColor
|
||||
? utilities.isEqual(backgroundColor, [1, 1, 1])
|
||||
: false;
|
||||
const isLight = backgroundColor ? utilities.isEqual(backgroundColor, [1, 1, 1]) : false;
|
||||
|
||||
return (
|
||||
<ViewportOverlay
|
||||
@@ -240,7 +227,7 @@ function _getInstanceNumberFromStack(viewportData, imageIndex) {
|
||||
function _getInstanceNumberFromVolume(
|
||||
viewportData,
|
||||
imageIndex,
|
||||
viewportIndex,
|
||||
viewportId,
|
||||
cornerstoneViewportService
|
||||
) {
|
||||
const volumes = viewportData.volumes;
|
||||
@@ -253,9 +240,7 @@ function _getInstanceNumberFromVolume(
|
||||
const volume = volumes[0];
|
||||
const { direction, imageIds } = volume;
|
||||
|
||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
const cornerstoneViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
if (!cornerstoneViewport) {
|
||||
return;
|
||||
@@ -278,8 +263,7 @@ function _getInstanceNumberFromVolume(
|
||||
return {};
|
||||
}
|
||||
|
||||
const { instanceNumber } =
|
||||
metaData.get('generalImageModule', imageId) || {};
|
||||
const { instanceNumber } = metaData.get('generalImageModule', imageId) || {};
|
||||
return parseInt(instanceNumber);
|
||||
}
|
||||
}
|
||||
@@ -287,7 +271,7 @@ function _getInstanceNumberFromVolume(
|
||||
CornerstoneViewportOverlay.propTypes = {
|
||||
viewportData: PropTypes.object,
|
||||
imageIndex: PropTypes.number,
|
||||
viewportIndex: PropTypes.number,
|
||||
viewportId: PropTypes.string,
|
||||
servicesManager: PropTypes.instanceOf(ServicesManager),
|
||||
};
|
||||
|
||||
|
||||
@@ -75,22 +75,16 @@ export function formatPN(name) {
|
||||
* Gets compression type
|
||||
*
|
||||
* @param {number} imageId
|
||||
* @returns {string} comrpession type.
|
||||
* @returns {string} compression type.
|
||||
*/
|
||||
export function getCompression(imageId) {
|
||||
const generalImageModule = metaData.get('generalImageModule', imageId) || {};
|
||||
const {
|
||||
lossyImageCompression,
|
||||
lossyImageCompressionRatio,
|
||||
lossyImageCompressionMethod,
|
||||
} = generalImageModule;
|
||||
const { lossyImageCompression, lossyImageCompressionRatio, lossyImageCompressionMethod } =
|
||||
generalImageModule;
|
||||
|
||||
if (lossyImageCompression === '01' && lossyImageCompressionRatio !== '') {
|
||||
const compressionMethod = lossyImageCompressionMethod || 'Lossy: ';
|
||||
const compressionRatio = formatNumberPrecision(
|
||||
lossyImageCompressionRatio,
|
||||
2
|
||||
);
|
||||
const compressionRatio = formatNumberPrecision(lossyImageCompressionRatio, 2);
|
||||
return compressionMethod + compressionRatio + ' : 1';
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import toggleStackImageSync from './utils/stackSync/toggleStackImageSync';
|
||||
import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/utils/selection';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import { CornerstoneServices } from './types';
|
||||
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||
|
||||
function commandsModule({
|
||||
servicesManager,
|
||||
@@ -28,6 +29,7 @@ function commandsModule({
|
||||
toolGroupService,
|
||||
cineService,
|
||||
toolbarService,
|
||||
stateSyncService,
|
||||
uiDialogService,
|
||||
cornerstoneViewportService,
|
||||
uiNotificationService,
|
||||
@@ -56,9 +58,7 @@ function commandsModule({
|
||||
// filter by allowed selected tools from config property (if there is any)
|
||||
const isToolAllowed =
|
||||
!optionsToUse.allowedSelectedTools ||
|
||||
optionsToUse.allowedSelectedTools.includes(
|
||||
firstAnnotationSelected?.metadata?.toolName
|
||||
);
|
||||
optionsToUse.allowedSelectedTools.includes(firstAnnotationSelected?.metadata?.toolName);
|
||||
if (isToolAllowed) {
|
||||
optionsToUse.nearbyToolData = firstAnnotationSelected;
|
||||
} else {
|
||||
@@ -88,10 +88,7 @@ function commandsModule({
|
||||
},
|
||||
|
||||
getNearbyToolData({ nearbyToolData, element, canvasCoordinates }) {
|
||||
return (
|
||||
nearbyToolData ??
|
||||
cstUtils.getAnnotationNearPoint(element, canvasCoordinates)
|
||||
);
|
||||
return nearbyToolData ?? cstUtils.getAnnotationNearPoint(element, canvasCoordinates);
|
||||
},
|
||||
getNearbyAnnotation({ element, canvasCoordinates }) {
|
||||
const nearbyToolData = actions.getNearbyToolData({
|
||||
@@ -108,18 +105,14 @@ function commandsModule({
|
||||
}
|
||||
|
||||
const { renderingEngineId, viewportId } = enabledElement;
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||
viewportId,
|
||||
renderingEngineId
|
||||
);
|
||||
const toolGroup = ToolGroupManager.getToolGroupForViewport(viewportId, renderingEngineId);
|
||||
|
||||
const toolInstance = toolGroup.getToolInstance(toolName);
|
||||
|
||||
return toolInstance?.constructor?.isAnnotation ?? true;
|
||||
};
|
||||
|
||||
return nearbyToolData?.metadata?.toolName &&
|
||||
isAnnotation(nearbyToolData.metadata.toolName)
|
||||
return nearbyToolData?.metadata?.toolName && isAnnotation(nearbyToolData.metadata.toolName)
|
||||
? nearbyToolData
|
||||
: null;
|
||||
},
|
||||
@@ -152,11 +145,7 @@ function commandsModule({
|
||||
label,
|
||||
});
|
||||
|
||||
measurementService.update(
|
||||
updatedMeasurement.uid,
|
||||
updatedMeasurement,
|
||||
true
|
||||
);
|
||||
measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
|
||||
},
|
||||
false
|
||||
);
|
||||
@@ -219,27 +208,20 @@ function commandsModule({
|
||||
}
|
||||
}
|
||||
}
|
||||
measurementService.update(
|
||||
updatedMeasurement.uid,
|
||||
updatedMeasurement,
|
||||
true
|
||||
);
|
||||
measurementService.update(updatedMeasurement.uid, updatedMeasurement, true);
|
||||
},
|
||||
|
||||
// Retrieve value commands
|
||||
getActiveViewportEnabledElement: _getActiveViewportEnabledElement,
|
||||
|
||||
setViewportActive: ({ viewportId }) => {
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(
|
||||
viewportId
|
||||
);
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
if (!viewportInfo) {
|
||||
console.warn('No viewport found for viewportId:', viewportId);
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportIndex = viewportInfo.getViewportIndex();
|
||||
viewportGridService.setActiveViewportIndex(viewportIndex);
|
||||
viewportGridService.setActiveViewportId(viewportId);
|
||||
},
|
||||
arrowTextCallback: ({ callback, data }) => {
|
||||
callInputDialog(uiDialogService, data, callback);
|
||||
@@ -249,9 +231,7 @@ function commandsModule({
|
||||
const { isCineEnabled } = cineService.getState();
|
||||
cineService.setIsCineEnabled(!isCineEnabled);
|
||||
toolbarService.setButton('Cine', { props: { isActive: !isCineEnabled } });
|
||||
viewports.forEach((_, index) =>
|
||||
cineService.setCine({ id: index, isPlaying: false })
|
||||
);
|
||||
viewports.forEach((_, index) => cineService.setCine({ id: index, isPlaying: false }));
|
||||
},
|
||||
setWindowLevel({ window, level, toolGroupId }) {
|
||||
// convert to numbers
|
||||
@@ -259,9 +239,7 @@ function commandsModule({
|
||||
const windowCenterNum = Number(level);
|
||||
|
||||
const { viewportId } = _getActiveViewportEnabledElement();
|
||||
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(
|
||||
viewportId
|
||||
);
|
||||
const viewportToolGroupId = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (toolGroupId && toolGroupId !== viewportToolGroupId) {
|
||||
return;
|
||||
@@ -271,10 +249,7 @@ function commandsModule({
|
||||
const renderingEngine = cornerstoneViewportService.getRenderingEngine();
|
||||
const viewport = renderingEngine.getViewport(viewportId);
|
||||
|
||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(
|
||||
windowWidthNum,
|
||||
windowCenterNum
|
||||
);
|
||||
const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum);
|
||||
|
||||
viewport.setProperties({
|
||||
voiRange: {
|
||||
@@ -292,7 +267,7 @@ function commandsModule({
|
||||
toolbarService.recordInteraction(props);
|
||||
},
|
||||
|
||||
setToolActive: ({ toolName, toolGroupId = null }) => {
|
||||
setToolActive: ({ toolName, toolGroupId = null, toggledState }) => {
|
||||
if (toolName === 'Crosshairs') {
|
||||
const activeViewportToolGroup = toolGroupService.getToolGroup(null);
|
||||
|
||||
@@ -309,9 +284,11 @@ function commandsModule({
|
||||
}
|
||||
}
|
||||
|
||||
const { viewports } = viewportGridService.getState() || {
|
||||
viewports: [],
|
||||
};
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
if (!viewports.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolGroup = toolGroupService.getToolGroup(toolGroupId);
|
||||
const toolGroupViewportIds = toolGroup?.getViewportIds?.();
|
||||
@@ -321,14 +298,8 @@ function commandsModule({
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredViewports = viewports.filter(viewport => {
|
||||
if (!viewport.viewportOptions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return toolGroupViewportIds.includes(
|
||||
viewport.viewportOptions.viewportId
|
||||
);
|
||||
const filteredViewports = Array.from(viewports.values()).filter(viewport => {
|
||||
return toolGroupViewportIds.includes(viewport.viewportId);
|
||||
});
|
||||
|
||||
if (!filteredViewports.length) {
|
||||
@@ -357,6 +328,14 @@ function commandsModule({
|
||||
toolGroup.setToolPassive(activeToolName);
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a toggle state, then simply set the enabled/disabled state without
|
||||
// setting the tool active.
|
||||
if (toggledState != null) {
|
||||
toggledState ? toolGroup.setToolEnabled(toolName) : toolGroup.setToolDisabled(toolName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the new toolName to be active
|
||||
toolGroup.setToolActive(toolName, {
|
||||
bindings: [
|
||||
@@ -367,13 +346,9 @@ function commandsModule({
|
||||
});
|
||||
},
|
||||
showDownloadViewportModal: () => {
|
||||
const { activeViewportIndex } = viewportGridService.getState();
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
|
||||
if (
|
||||
!cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
activeViewportIndex
|
||||
)
|
||||
) {
|
||||
if (!cornerstoneViewportService.getCornerstoneViewport(activeViewportId)) {
|
||||
// Cannot download a non-cornerstone viewport (image).
|
||||
uiNotificationService.show({
|
||||
title: 'Download Image',
|
||||
@@ -390,7 +365,7 @@ function commandsModule({
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
title: 'Download High Quality Image',
|
||||
contentProps: {
|
||||
activeViewportIndex,
|
||||
activeViewportId,
|
||||
onClose: uiModalService.hide,
|
||||
cornerstoneViewportService,
|
||||
},
|
||||
@@ -457,11 +432,9 @@ function commandsModule({
|
||||
|
||||
const { viewport } = enabledElement;
|
||||
|
||||
if (viewport instanceof StackViewport) {
|
||||
const { invert } = viewport.getProperties();
|
||||
viewport.setProperties({ invert: !invert });
|
||||
viewport.render();
|
||||
}
|
||||
const { invert } = viewport.getProperties();
|
||||
viewport.setProperties({ invert: !invert });
|
||||
viewport.render();
|
||||
},
|
||||
resetViewport: () => {
|
||||
const enabledElement = _getActiveViewportEnabledElement();
|
||||
@@ -514,9 +487,7 @@ function commandsModule({
|
||||
}
|
||||
viewport = enabledElement.viewport;
|
||||
} else {
|
||||
viewport = cornerstoneViewportService.getCornerstoneViewport(
|
||||
gridViewport.id
|
||||
);
|
||||
viewport = cornerstoneViewportService.getCornerstoneViewport(gridViewport.id);
|
||||
}
|
||||
|
||||
// Get number of slices
|
||||
@@ -526,14 +497,12 @@ function commandsModule({
|
||||
if (viewport instanceof StackViewport) {
|
||||
numberOfSlices = viewport.getImageIds().length;
|
||||
} else if (viewport instanceof VolumeViewport) {
|
||||
numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport)
|
||||
.numberOfSlices;
|
||||
numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport).numberOfSlices;
|
||||
} else {
|
||||
throw new Error('Unsupported viewport type');
|
||||
}
|
||||
|
||||
const jumpIndex =
|
||||
imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex;
|
||||
const jumpIndex = imageIndex < 0 ? numberOfSlices + imageIndex : imageIndex;
|
||||
if (jumpIndex >= numberOfSlices || jumpIndex < 0) {
|
||||
throw new Error(`Can't jump to ${imageIndex}`);
|
||||
}
|
||||
@@ -554,15 +523,8 @@ function commandsModule({
|
||||
|
||||
cstUtils.scroll(viewport, options);
|
||||
},
|
||||
setViewportColormap: ({
|
||||
viewportIndex,
|
||||
displaySetInstanceUID,
|
||||
colormap,
|
||||
immediate = false,
|
||||
}) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewportByIndex(
|
||||
viewportIndex
|
||||
);
|
||||
setViewportColormap: ({ viewportId, displaySetInstanceUID, colormap, immediate = false }) => {
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
const actorEntries = viewport.getActors();
|
||||
|
||||
@@ -578,17 +540,15 @@ function commandsModule({
|
||||
viewport.render();
|
||||
}
|
||||
},
|
||||
incrementActiveViewport: () => {
|
||||
const { activeViewportIndex, viewports } = viewportGridService.getState();
|
||||
const nextViewportIndex = (activeViewportIndex + 1) % viewports.length;
|
||||
viewportGridService.setActiveViewportIndex(nextViewportIndex);
|
||||
},
|
||||
decrementActiveViewport: () => {
|
||||
const { activeViewportIndex, viewports } = viewportGridService.getState();
|
||||
changeActiveViewport: ({ direction = 1 }) => {
|
||||
const { activeViewportId, viewports } = viewportGridService.getState();
|
||||
const viewportIds = Array.from(viewports.keys());
|
||||
const currentIndex = viewportIds.indexOf(activeViewportId);
|
||||
const nextViewportIndex =
|
||||
(activeViewportIndex - 1 + viewports.length) % viewports.length;
|
||||
viewportGridService.setActiveViewportIndex(nextViewportIndex);
|
||||
(currentIndex + direction + viewportIds.length) % viewportIds.length;
|
||||
viewportGridService.setActiveViewportId(viewportIds[nextViewportIndex] as string);
|
||||
},
|
||||
|
||||
toggleStackImageSync: ({ toggledState }) => {
|
||||
toggleStackImageSync({
|
||||
getEnabledElement,
|
||||
@@ -596,19 +556,13 @@ function commandsModule({
|
||||
toggledState,
|
||||
});
|
||||
},
|
||||
toggleReferenceLines: ({ toggledState }) => {
|
||||
const { activeViewportIndex } = viewportGridService.getState();
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex(
|
||||
activeViewportIndex
|
||||
);
|
||||
setSourceViewportForReferenceLinesTool: ({ toggledState }) => {
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(activeViewportId);
|
||||
|
||||
const viewportId = viewportInfo.getViewportId();
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toggledState) {
|
||||
toolGroup.setToolDisabled(ReferenceLinesTool.toolName);
|
||||
}
|
||||
|
||||
toolGroup.setToolConfiguration(
|
||||
ReferenceLinesTool.toolName,
|
||||
{
|
||||
@@ -616,7 +570,9 @@ function commandsModule({
|
||||
},
|
||||
true // overwrite
|
||||
);
|
||||
toolGroup.setToolEnabled(ReferenceLinesTool.toolName);
|
||||
},
|
||||
storePresentation: ({ viewportId }) => {
|
||||
cornerstoneViewportService.storePresentation({ viewportId });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -673,10 +629,11 @@ function commandsModule({
|
||||
options: { rotation: -90 },
|
||||
},
|
||||
incrementActiveViewport: {
|
||||
commandFn: actions.incrementActiveViewport,
|
||||
commandFn: actions.changeActiveViewport,
|
||||
},
|
||||
decrementActiveViewport: {
|
||||
commandFn: actions.decrementActiveViewport,
|
||||
commandFn: actions.changeActiveViewport,
|
||||
options: { direction: -1 },
|
||||
},
|
||||
flipViewportHorizontal: {
|
||||
commandFn: actions.flipViewportHorizontal,
|
||||
@@ -739,8 +696,13 @@ function commandsModule({
|
||||
toggleStackImageSync: {
|
||||
commandFn: actions.toggleStackImageSync,
|
||||
},
|
||||
toggleReferenceLines: {
|
||||
commandFn: actions.toggleReferenceLines,
|
||||
setSourceViewportForReferenceLinesTool: {
|
||||
commandFn: actions.setSourceViewportForReferenceLinesTool,
|
||||
},
|
||||
storePresentation: {
|
||||
commandFn: actions.storePresentation,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { CinePlayer, useCine, useViewportGrid } from '@ohif/ui';
|
||||
import { Enums, eventTarget } from '@cornerstonejs/core';
|
||||
|
||||
function WrappedCinePlayer({ enabledVPElement, viewportId, servicesManager }) {
|
||||
const { toolbarService, customizationService } = servicesManager.services;
|
||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||
const [{ activeViewportId }] = useViewportGrid();
|
||||
|
||||
const { component: CinePlayerComponent = CinePlayer } =
|
||||
customizationService.get('cinePlayer') ?? {};
|
||||
|
||||
const handleCineClose = () => {
|
||||
toolbarService.recordInteraction({
|
||||
groupId: 'MoreTools',
|
||||
itemId: 'cine',
|
||||
interactionType: 'toggle',
|
||||
commands: [
|
||||
{
|
||||
commandName: 'toggleCine',
|
||||
commandOptions: {},
|
||||
context: 'CORNERSTONE',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const cineHandler = () => {
|
||||
if (!cines || !cines[viewportId] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cine = cines[viewportId];
|
||||
const isPlaying = cine.isPlaying || false;
|
||||
const frameRate = cine.frameRate || 24;
|
||||
|
||||
const validFrameRate = Math.max(frameRate, 1);
|
||||
|
||||
if (isPlaying) {
|
||||
cineService.playClip(enabledVPElement, {
|
||||
framesPerSecond: validFrameRate,
|
||||
});
|
||||
} else {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
eventTarget.addEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, cineHandler);
|
||||
|
||||
return () => {
|
||||
cineService.setCine({ id: viewportId, isPlaying: false });
|
||||
eventTarget.removeEventListener(Enums.Events.STACK_VIEWPORT_NEW_STACK, cineHandler);
|
||||
};
|
||||
}, [enabledVPElement]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cines || !cines[viewportId] || !enabledVPElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
cineHandler();
|
||||
|
||||
return () => {
|
||||
if (enabledVPElement && cines?.[viewportId]?.isPlaying) {
|
||||
cineService.stopClip(enabledVPElement);
|
||||
}
|
||||
};
|
||||
}, [cines, viewportId, cineService, enabledVPElement, cineHandler]);
|
||||
|
||||
const cine = cines[viewportId];
|
||||
const isPlaying = (cine && cine.isPlaying) || false;
|
||||
|
||||
return (
|
||||
isCineEnabled && (
|
||||
<CinePlayerComponent
|
||||
className="absolute left-1/2 bottom-3 -translate-x-1/2"
|
||||
isPlaying={isPlaying}
|
||||
onClose={handleCineClose}
|
||||
onPlayPauseChange={isPlaying =>
|
||||
cineService.setCine({
|
||||
id: activeViewportId,
|
||||
isPlaying,
|
||||
})
|
||||
}
|
||||
onFrameRateChange={frameRate =>
|
||||
cineService.setCine({
|
||||
id: activeViewportId,
|
||||
frameRate,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default WrappedCinePlayer;
|
||||
@@ -0,0 +1,3 @@
|
||||
import CinePlayer from './CinePlayer';
|
||||
|
||||
export default CinePlayer;
|
||||
@@ -1,6 +1,23 @@
|
||||
.dicom-upload-drop-area-border-dash {
|
||||
background-image: repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to right, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%), repeating-linear-gradient(to bottom, #7BB2CE 0%, #7BB2CE 50%, transparent 50%, transparent 100%);
|
||||
background-position: left top, left bottom, left top, right top;
|
||||
background-image: repeating-linear-gradient(
|
||||
to right,
|
||||
#7bb2ce 0%,
|
||||
#7bb2ce 50%,
|
||||
transparent 50%,
|
||||
transparent 100%
|
||||
),
|
||||
repeating-linear-gradient(to right, #7bb2ce 0%, #7bb2ce 50%, transparent 50%, transparent 100%),
|
||||
repeating-linear-gradient(to bottom, #7bb2ce 0%, #7bb2ce 50%, transparent 50%, transparent 100%),
|
||||
repeating-linear-gradient(to bottom, #7bb2ce 0%, #7bb2ce 50%, transparent 50%, transparent 100%);
|
||||
background-position:
|
||||
left top,
|
||||
left bottom,
|
||||
left top,
|
||||
right top;
|
||||
background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
|
||||
background-size: 20px 3px, 20px 3px, 3px 20px, 3px 20px;
|
||||
background-size:
|
||||
20px 3px,
|
||||
20px 3px,
|
||||
3px 20px,
|
||||
3px 20px;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import DicomFileUploader from '../../utils/DicomFileUploader';
|
||||
import DicomUploadProgress from './DicomUploadProgress';
|
||||
import { Button } from '@ohif/ui';
|
||||
import { Button, ButtonEnums } from '@ohif/ui';
|
||||
import './DicomUpload.css';
|
||||
|
||||
type DicomUploadProps = {
|
||||
@@ -14,19 +14,13 @@ type DicomUploadProps = {
|
||||
onStarted: () => void;
|
||||
};
|
||||
|
||||
function DicomUpload({
|
||||
dataSource,
|
||||
onComplete,
|
||||
onStarted,
|
||||
}: DicomUploadProps): ReactElement {
|
||||
function DicomUpload({ dataSource, onComplete, onStarted }: DicomUploadProps): ReactElement {
|
||||
const baseClassNames = 'min-h-[480px] flex flex-col bg-black select-none';
|
||||
const [dicomFileUploaderArr, setDicomFileUploaderArr] = useState([]);
|
||||
|
||||
const onDrop = useCallback(async acceptedFiles => {
|
||||
onStarted();
|
||||
setDicomFileUploaderArr(
|
||||
acceptedFiles.map(file => new DicomFileUploader(file, dataSource))
|
||||
);
|
||||
setDicomFileUploaderArr(acceptedFiles.map(file => new DicomFileUploader(file, dataSource)));
|
||||
}, []);
|
||||
|
||||
const getDropZoneComponent = (): ReactElement => {
|
||||
@@ -40,15 +34,16 @@ function DicomUpload({
|
||||
{({ getRootProps }) => (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className="m-5 dicom-upload-drop-area-border-dash flex flex-col items-center justify-center h-full"
|
||||
className="dicom-upload-drop-area-border-dash m-5 flex h-full flex-col items-center justify-center"
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<Dropzone onDrop={onDrop} noDrag>
|
||||
<Dropzone
|
||||
onDrop={onDrop}
|
||||
noDrag
|
||||
>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div {...getRootProps()}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={false}
|
||||
onClick={() => {}}
|
||||
>
|
||||
@@ -58,13 +53,14 @@ function DicomUpload({
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
<Dropzone onDrop={onDrop} noDrag>
|
||||
<Dropzone
|
||||
onDrop={onDrop}
|
||||
noDrag
|
||||
>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div {...getRootProps()}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primaryDark"
|
||||
border="primaryActive"
|
||||
type={ButtonEnums.type.secondary}
|
||||
disabled={false}
|
||||
onClick={() => {}}
|
||||
>
|
||||
@@ -80,9 +76,7 @@ function DicomUpload({
|
||||
</Dropzone>
|
||||
</div>
|
||||
<div className="pt-5">or drag images or folders here</div>
|
||||
<div className="pt-3 text-aqua-pale text-lg">
|
||||
(DICOM files supported)
|
||||
</div>
|
||||
<div className="text-aqua-pale pt-3 text-lg">(DICOM files supported)</div>
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
@@ -99,9 +93,7 @@ function DicomUpload({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={classNames('h-[480px]', baseClassNames)}>
|
||||
{getDropZoneComponent()}
|
||||
</div>
|
||||
<div className={classNames('h-[480px]', baseClassNames)}>{getDropZoneComponent()}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState, ReactElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Icon, ProgressLoadingBar } from '@ohif/ui';
|
||||
import DicomFileUploader, {
|
||||
@@ -39,18 +33,14 @@ const BASE_INTERVAL_TIME = 15000;
|
||||
// calculate the upload rate.
|
||||
const UPLOAD_RATE_THRESHOLD = 75;
|
||||
|
||||
const NO_WRAP_ELLIPSIS_CLASS_NAMES =
|
||||
'text-ellipsis whitespace-nowrap overflow-hidden';
|
||||
const NO_WRAP_ELLIPSIS_CLASS_NAMES = 'text-ellipsis whitespace-nowrap overflow-hidden';
|
||||
|
||||
function DicomUploadProgress({
|
||||
dicomFileUploaderArr,
|
||||
onComplete,
|
||||
}: DicomUploadProgressProps): ReactElement {
|
||||
const [totalUploadSize] = useState(
|
||||
dicomFileUploaderArr.reduce(
|
||||
(acc, fileUploader) => acc + fileUploader.getFileSize(),
|
||||
0
|
||||
)
|
||||
dicomFileUploaderArr.reduce((acc, fileUploader) => acc + fileUploader.getFileSize(), 0)
|
||||
);
|
||||
|
||||
const currentUploadSizeRef = useRef<number>(0);
|
||||
@@ -83,15 +73,13 @@ function DicomUploadProgress({
|
||||
let intervalStartTime = Date.now();
|
||||
|
||||
const setUploadRateRef = () => {
|
||||
const uploadSizeFromStartOfInterval =
|
||||
currentUploadSizeRef.current - intervalStartUploadSize;
|
||||
const uploadSizeFromStartOfInterval = currentUploadSizeRef.current - intervalStartUploadSize;
|
||||
|
||||
const now = Date.now();
|
||||
const timeSinceStartOfInterval = now - intervalStartTime;
|
||||
|
||||
// Calculate and set the upload rate (ref)
|
||||
uploadRateRef.current =
|
||||
uploadSizeFromStartOfInterval / timeSinceStartOfInterval;
|
||||
uploadRateRef.current = uploadSizeFromStartOfInterval / timeSinceStartOfInterval;
|
||||
|
||||
// Reset the interval starting values.
|
||||
intervalStartUploadSize = currentUploadSizeRef.current;
|
||||
@@ -134,28 +122,19 @@ function DicomUploadProgress({
|
||||
const updateProgress = (percentComplete: number) => {
|
||||
const previousFileUploadSize = currentFileUploadSize;
|
||||
|
||||
currentFileUploadSize = Math.round(
|
||||
(percentComplete / 100) * fileUploader.getFileSize()
|
||||
);
|
||||
currentFileUploadSize = Math.round((percentComplete / 100) * fileUploader.getFileSize());
|
||||
|
||||
currentUploadSizeRef.current = Math.min(
|
||||
totalUploadSize,
|
||||
currentUploadSizeRef.current -
|
||||
previousFileUploadSize +
|
||||
currentFileUploadSize
|
||||
currentUploadSizeRef.current - previousFileUploadSize + currentFileUploadSize
|
||||
);
|
||||
|
||||
setPercentComplete(
|
||||
(currentUploadSizeRef.current / totalUploadSize) * 100
|
||||
);
|
||||
setPercentComplete((currentUploadSizeRef.current / totalUploadSize) * 100);
|
||||
|
||||
if (uploadRateRef.current !== 0) {
|
||||
const uploadSizeRemaining =
|
||||
totalUploadSize - currentUploadSizeRef.current;
|
||||
const uploadSizeRemaining = totalUploadSize - currentUploadSizeRef.current;
|
||||
|
||||
const timeRemaining = Math.round(
|
||||
uploadSizeRemaining / uploadRateRef.current
|
||||
);
|
||||
const timeRemaining = Math.round(uploadSizeRemaining / uploadRateRef.current);
|
||||
|
||||
if (currentTimeRemaining === null) {
|
||||
currentTimeRemaining = timeRemaining;
|
||||
@@ -168,9 +147,7 @@ function DicomUploadProgress({
|
||||
// due to rounding, inaccuracies in the estimate and slight variations
|
||||
// in upload rates over time.
|
||||
if (timeRemaining < ONE_MINUTE) {
|
||||
const currentSecondsRemaining = Math.ceil(
|
||||
currentTimeRemaining / ONE_SECOND
|
||||
);
|
||||
const currentSecondsRemaining = Math.ceil(currentTimeRemaining / ONE_SECOND);
|
||||
const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND);
|
||||
const delta = secondsRemaining - currentSecondsRemaining;
|
||||
if (delta < 0 || delta > 2) {
|
||||
@@ -181,9 +158,7 @@ function DicomUploadProgress({
|
||||
}
|
||||
|
||||
if (timeRemaining < ONE_HOUR) {
|
||||
const currentMinutesRemaining = Math.ceil(
|
||||
currentTimeRemaining / ONE_MINUTE
|
||||
);
|
||||
const currentMinutesRemaining = Math.ceil(currentTimeRemaining / ONE_MINUTE);
|
||||
const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE);
|
||||
const delta = minutesRemaining - currentMinutesRemaining;
|
||||
if (delta < 0 || delta > 2) {
|
||||
@@ -199,9 +174,7 @@ function DicomUploadProgress({
|
||||
}
|
||||
};
|
||||
|
||||
const progressCallback = (
|
||||
progressEvent: DicomFileUploaderProgressEvent
|
||||
) => {
|
||||
const progressCallback = (progressEvent: DicomFileUploaderProgressEvent) => {
|
||||
updateProgress(progressEvent.percentComplete);
|
||||
};
|
||||
|
||||
@@ -249,16 +222,12 @@ function DicomUploadProgress({
|
||||
|
||||
if (timeRemaining < ONE_MINUTE) {
|
||||
const secondsRemaining = Math.ceil(timeRemaining / ONE_SECOND);
|
||||
return `${secondsRemaining} ${
|
||||
secondsRemaining === 1 ? 'second' : 'seconds'
|
||||
}`;
|
||||
return `${secondsRemaining} ${secondsRemaining === 1 ? 'second' : 'seconds'}`;
|
||||
}
|
||||
|
||||
if (timeRemaining < ONE_HOUR) {
|
||||
const minutesRemaining = Math.ceil(timeRemaining / ONE_MINUTE);
|
||||
return `${minutesRemaining} ${
|
||||
minutesRemaining === 1 ? 'minute' : 'minutes'
|
||||
}`;
|
||||
return `${minutesRemaining} ${minutesRemaining === 1 ? 'minute' : 'minutes'}`;
|
||||
}
|
||||
|
||||
const hoursRemaining = Math.ceil(timeRemaining / ONE_HOUR);
|
||||
@@ -278,9 +247,7 @@ function DicomUploadProgress({
|
||||
const showInfiniteProgressBar = useCallback((): boolean => {
|
||||
return (
|
||||
getPercentCompleteRounded() < 1 &&
|
||||
(progressBarContainerRef?.current?.offsetWidth ?? 0) *
|
||||
(percentComplete / 100) <
|
||||
1
|
||||
(progressBarContainerRef?.current?.offsetWidth ?? 0) * (percentComplete / 100) < 1
|
||||
);
|
||||
}, [getPercentCompleteRounded, percentComplete]);
|
||||
|
||||
@@ -300,17 +267,13 @@ function DicomUploadProgress({
|
||||
|
||||
const getNumCompletedAndTimeRemainingComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="text-lg px-1 pb-4 h-14 flex bg-primary-dark items-center">
|
||||
<div className="bg-primary-dark flex h-14 items-center px-1 pb-4 text-lg">
|
||||
{numFilesCompleted === dicomFileUploaderArr.length ? (
|
||||
<>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{`${
|
||||
dicomFileUploaderArr.length
|
||||
} ${
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{`${dicomFileUploaderArr.length} ${
|
||||
dicomFileUploaderArr.length > 1 ? 'files' : 'file'
|
||||
} completed.`}</span>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={false}
|
||||
className="ml-auto"
|
||||
onClick={onComplete}
|
||||
@@ -326,18 +289,14 @@ function DicomUploadProgress({
|
||||
>
|
||||
{`${numFilesCompleted} of ${dicomFileUploaderArr.length}`}
|
||||
</span>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>{' files completed.'} </span>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
|
||||
{' files completed.'}
|
||||
</span>
|
||||
<span className={NO_WRAP_ELLIPSIS_CLASS_NAMES}>
|
||||
{timeRemaining
|
||||
? `Less than ${getFormattedTimeRemaining()} remaining. `
|
||||
: ''}
|
||||
{timeRemaining ? `Less than ${getFormattedTimeRemaining()} remaining. ` : ''}
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
NO_WRAP_ELLIPSIS_CLASS_NAMES,
|
||||
'cursor-pointer text-primary-active hover:text-primary-light active:text-aqua-pale ml-auto'
|
||||
'text-primary-active hover:text-primary-light active:text-aqua-pale ml-auto cursor-pointer'
|
||||
)}
|
||||
onClick={cancelAllUploads}
|
||||
>
|
||||
@@ -351,14 +310,13 @@ function DicomUploadProgress({
|
||||
|
||||
const getShowFailedOnlyIconComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="ml-auto flex justify-center w-6">
|
||||
<div className="ml-auto flex w-6 justify-center">
|
||||
{numFails > 0 && (
|
||||
<div
|
||||
onClick={() =>
|
||||
setShowFailedOnly(currentShowFailedOnly => !currentShowFailedOnly)
|
||||
}
|
||||
>
|
||||
<Icon className="cursor-pointer" name="icon-status-alert"></Icon>
|
||||
<div onClick={() => setShowFailedOnly(currentShowFailedOnly => !currentShowFailedOnly)}>
|
||||
<Icon
|
||||
className="cursor-pointer"
|
||||
name="icon-status-alert"
|
||||
></Icon>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -367,31 +325,28 @@ function DicomUploadProgress({
|
||||
|
||||
const getPercentCompleteComponent = (): ReactElement => {
|
||||
return (
|
||||
<div className="overflow-y-scroll ohif-scrollbar px-2 border-b border-secondary-light">
|
||||
<div className="flex w-full p-2.5 items-center min-h-14">
|
||||
<div className="ohif-scrollbar border-secondary-light overflow-y-scroll border-b px-2">
|
||||
<div className="min-h-14 flex w-full items-center p-2.5">
|
||||
{numFilesCompleted === dicomFileUploaderArr.length ? (
|
||||
<>
|
||||
<div className="text-xl text-primary-light">
|
||||
<div className="text-primary-light text-xl">
|
||||
{numFails > 0
|
||||
? `Completed with ${numFails} ${
|
||||
numFails > 1 ? 'errors' : 'error'
|
||||
}!`
|
||||
? `Completed with ${numFails} ${numFails > 1 ? 'errors' : 'error'}!`
|
||||
: 'Completed!'}
|
||||
</div>
|
||||
{getShowFailedOnlyIconComponent()}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div ref={progressBarContainerRef} className="flex-grow">
|
||||
<div
|
||||
ref={progressBarContainerRef}
|
||||
className="flex-grow"
|
||||
>
|
||||
<ProgressLoadingBar
|
||||
progress={
|
||||
showInfiniteProgressBar()
|
||||
? undefined
|
||||
: Math.min(100, percentComplete)
|
||||
}
|
||||
progress={showInfiniteProgressBar() ? undefined : Math.min(100, percentComplete)}
|
||||
></ProgressLoadingBar>
|
||||
</div>
|
||||
<div className="w-24 ml-1 flex items-center">
|
||||
<div className="ml-1 flex w-24 items-center">
|
||||
<div className="w-10 text-right">{`${getPercentCompleteRounded()}%`}</div>
|
||||
{getShowFailedOnlyIconComponent()}
|
||||
</div>
|
||||
@@ -403,16 +358,15 @@ function DicomUploadProgress({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col grow">
|
||||
<div className="flex grow flex-col">
|
||||
{getNumCompletedAndTimeRemainingComponent()}
|
||||
<div className="flex flex-col bg-black text-lg overflow-hidden grow">
|
||||
<div className="flex grow flex-col overflow-hidden bg-black text-lg">
|
||||
{getPercentCompleteComponent()}
|
||||
<div className="overflow-y-scroll ohif-scrollbar px-2 grow h-1">
|
||||
<div className="ohif-scrollbar h-1 grow overflow-y-scroll px-2">
|
||||
{dicomFileUploaderArr
|
||||
.filter(
|
||||
dicomFileUploader =>
|
||||
!showFailedOnly ||
|
||||
dicomFileUploader.getStatus() === UploadStatus.Failed
|
||||
!showFailedOnly || dicomFileUploader.getStatus() === UploadStatus.Failed
|
||||
)
|
||||
.map(dicomFileUploader => (
|
||||
<DicomUploadProgressItem
|
||||
@@ -427,9 +381,7 @@ function DicomUploadProgress({
|
||||
}
|
||||
|
||||
DicomUploadProgress.propTypes = {
|
||||
dicomFileUploaderArr: PropTypes.arrayOf(
|
||||
PropTypes.instanceOf(DicomFileUploader)
|
||||
).isRequired,
|
||||
dicomFileUploaderArr: PropTypes.arrayOf(PropTypes.instanceOf(DicomFileUploader)).isRequired,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
ReactElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { ReactElement, memo, useCallback, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DicomFileUploader, {
|
||||
DicomFileUploaderProgressEvent,
|
||||
@@ -21,13 +15,10 @@ type DicomUploadProgressItemProps = {
|
||||
// eslint-disable-next-line react/display-name
|
||||
const DicomUploadProgressItem = memo(
|
||||
({ dicomFileUploader }: DicomUploadProgressItemProps): ReactElement => {
|
||||
const [percentComplete, setPercentComplete] = useState(
|
||||
dicomFileUploader.getPercentComplete()
|
||||
);
|
||||
const [percentComplete, setPercentComplete] = useState(dicomFileUploader.getPercentComplete());
|
||||
const [failedReason, setFailedReason] = useState('');
|
||||
const [status, setStatus] = useState(dicomFileUploader.getStatus());
|
||||
|
||||
console.info(`${dicomFileUploader.getFileId()}`);
|
||||
const isComplete = useCallback(() => {
|
||||
return (
|
||||
status === UploadStatus.Failed ||
|
||||
@@ -63,7 +54,10 @@ const DicomUploadProgressItem = memo(
|
||||
switch (dicomFileUploader.getStatus()) {
|
||||
case UploadStatus.Success:
|
||||
return (
|
||||
<Icon name="status-tracked" className="text-primary-light"></Icon>
|
||||
<Icon
|
||||
name="status-tracked"
|
||||
className="text-primary-light"
|
||||
></Icon>
|
||||
);
|
||||
case UploadStatus.InProgress:
|
||||
return <Icon name="icon-transferring"></Icon>;
|
||||
@@ -77,27 +71,25 @@ const DicomUploadProgressItem = memo(
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full p-2.5 text-lg min-h-14 items-center border-b border-secondary-light overflow-hidden">
|
||||
<div className="flex flex-col gap-1 self-top w-0 grow shrink">
|
||||
<div className="min-h-14 border-secondary-light flex w-full items-center overflow-hidden border-b p-2.5 text-lg">
|
||||
<div className="self-top flex w-0 shrink grow flex-col gap-1">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex w-6 justify-center items-center shrink-0">
|
||||
{getStatusIcon()}
|
||||
</div>
|
||||
<div className="text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<div className="flex w-6 shrink-0 items-center justify-center">{getStatusIcon()}</div>
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{dicomFileUploader.getFileName()}
|
||||
</div>
|
||||
</div>
|
||||
{failedReason && <div className="pl-10">{failedReason}</div>}
|
||||
</div>
|
||||
<div className="w-24 flex items-center">
|
||||
<div className="flex w-24 items-center">
|
||||
{!isComplete() && (
|
||||
<>
|
||||
{dicomFileUploader.getStatus() === UploadStatus.InProgress && (
|
||||
<div className="w-10 text-right">{percentComplete}%</div>
|
||||
)}
|
||||
<div className="flex cursor-pointer ml-auto">
|
||||
<div className="ml-auto flex cursor-pointer">
|
||||
<Icon
|
||||
className="self-center text-primary-active"
|
||||
className="text-primary-active self-center"
|
||||
name="close"
|
||||
onClick={cancelUpload}
|
||||
/>
|
||||
|
||||
@@ -4,9 +4,8 @@ const mpr: Types.HangingProtocol.Protocol = {
|
||||
id: 'mpr',
|
||||
name: 'Multi-Planar Reconstruction',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
createdDate: '2021-02-23',
|
||||
modifiedDate: '2023-04-03',
|
||||
modifiedDate: '2023-08-15',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
// Unknown number of priors referenced - so just match any study
|
||||
@@ -89,6 +88,7 @@ const mpr: Types.HangingProtocol.Protocol = {
|
||||
viewports: [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'mpr-axial',
|
||||
toolGroupId: 'mpr',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
@@ -112,6 +112,7 @@ const mpr: Types.HangingProtocol.Protocol = {
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'mpr-sagittal',
|
||||
toolGroupId: 'mpr',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
@@ -135,6 +136,7 @@ const mpr: Types.HangingProtocol.Protocol = {
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'mpr-coronal',
|
||||
toolGroupId: 'mpr',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
@@ -164,7 +166,6 @@ const mpr: Types.HangingProtocol.Protocol = {
|
||||
const mprAnd3DVolumeViewport = {
|
||||
id: 'mprAnd3DVolumeViewport',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'mpr',
|
||||
createdDate: '2023-03-15T10:29:44.894Z',
|
||||
modifiedDate: '2023-03-15T10:29:44.894Z',
|
||||
|
||||
@@ -24,7 +24,6 @@ import { toolNames } from './initCornerstoneTools';
|
||||
import { getEnabledElement, reset as enabledElementReset } from './state';
|
||||
import dicomLoaderService from './utils/dicomLoaderService';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import { registerColormap } from './utils/colormap/transferFunctionHelpers';
|
||||
|
||||
import { id } from './id';
|
||||
import * as csWADOImageLoader from './initWADOImageLoader.js';
|
||||
@@ -32,9 +31,7 @@ import { measurementMappingUtils } from './utils/measurementServiceMappings';
|
||||
import type { PublicViewportOptions } from './services/ViewportService/Viewport';
|
||||
|
||||
const Component = React.lazy(() => {
|
||||
return import(
|
||||
/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport'
|
||||
);
|
||||
return import(/* webpackPrefetch: true */ './Viewport/OHIFCornerstoneViewport');
|
||||
});
|
||||
|
||||
const OHIFCornerstoneViewport = props => {
|
||||
@@ -62,7 +59,6 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
imageRetrievalPoolManager.clearRequestStack(type);
|
||||
});
|
||||
|
||||
csWADOImageLoader.destroy();
|
||||
enabledElementReset();
|
||||
},
|
||||
|
||||
@@ -71,9 +67,7 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
*
|
||||
* @param configuration.csToolsConfig - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
preRegistration: function (
|
||||
props: Types.Extensions.ExtensionParams
|
||||
): Promise<void> {
|
||||
preRegistration: function (props: Types.Extensions.ExtensionParams): Promise<void> {
|
||||
const { servicesManager } = props;
|
||||
servicesManager.registerService(CornerstoneViewportService.REGISTRATION);
|
||||
servicesManager.registerService(ToolGroupService.REGISTRATION);
|
||||
@@ -141,5 +135,10 @@ const cornerstoneExtension: Types.Extensions.Extension = {
|
||||
};
|
||||
|
||||
export type { PublicViewportOptions };
|
||||
export { measurementMappingUtils, CornerstoneExtensionTypes, toolNames , getActiveViewportEnabledElement};
|
||||
export {
|
||||
measurementMappingUtils,
|
||||
CornerstoneExtensionTypes,
|
||||
toolNames,
|
||||
getActiveViewportEnabledElement,
|
||||
};
|
||||
export default cornerstoneExtension;
|
||||
@@ -26,6 +26,7 @@ import nthLoader from './utils/nthLoader';
|
||||
import interleaveTopToBottom from './utils/interleaveTopToBottom';
|
||||
import initContextMenu from './initContextMenu';
|
||||
import initDoubleClick from './initDoubleClick';
|
||||
import { CornerstoneServices } from './types';
|
||||
|
||||
// TODO: Cypress tests are currently grabbing this from the window?
|
||||
window.cornerstone = cornerstone;
|
||||
@@ -48,31 +49,23 @@ export default async function init({
|
||||
...cornerstone.getConfiguration(),
|
||||
rendering: {
|
||||
...cornerstone.getConfiguration().rendering,
|
||||
strictZSpacingForVolumeViewport:
|
||||
appConfig.strictZSpacingForVolumeViewport,
|
||||
strictZSpacingForVolumeViewport: appConfig.strictZSpacingForVolumeViewport,
|
||||
},
|
||||
});
|
||||
|
||||
// For debugging large datasets
|
||||
const MAX_CACHE_SIZE_1GB = 1073741824;
|
||||
const maxCacheSize = appConfig.maxCacheSize;
|
||||
cornerstone.cache.setMaxCacheSize(
|
||||
maxCacheSize ? maxCacheSize : MAX_CACHE_SIZE_1GB
|
||||
);
|
||||
// For debugging large datasets, otherwise prefer the defaults
|
||||
const { maxCacheSize } = appConfig;
|
||||
if (maxCacheSize) {
|
||||
cornerstone.cache.setMaxCacheSize(maxCacheSize);
|
||||
}
|
||||
|
||||
initCornerstoneTools();
|
||||
|
||||
Settings.getRuntimeSettings().set(
|
||||
'useCursors',
|
||||
Boolean(appConfig.useCursors)
|
||||
);
|
||||
Settings.getRuntimeSettings().set('useCursors', Boolean(appConfig.useCursors));
|
||||
|
||||
const {
|
||||
userAuthenticationService,
|
||||
measurementService,
|
||||
customizationService,
|
||||
displaySetService,
|
||||
uiDialogService,
|
||||
uiModalService,
|
||||
uiNotificationService,
|
||||
cineService,
|
||||
@@ -81,28 +74,21 @@ export default async function init({
|
||||
toolGroupService,
|
||||
viewportGridService,
|
||||
stateSyncService,
|
||||
} = servicesManager.services;
|
||||
} = servicesManager.services as CornerstoneServices;
|
||||
|
||||
window.services = servicesManager.services;
|
||||
window.extensionManager = extensionManager;
|
||||
window.commandsManager = commandsManager;
|
||||
|
||||
if (
|
||||
appConfig.showWarningMessageForCrossOrigin &&
|
||||
!window.crossOriginIsolated
|
||||
) {
|
||||
if (appConfig.showWarningMessageForCrossOrigin && !window.crossOriginIsolated) {
|
||||
uiNotificationService.show({
|
||||
title: 'Cross Origin Isolation',
|
||||
message:
|
||||
'Cross Origin Isolation is not enabled, volume rendering will not work (e.g., MPR)',
|
||||
message: 'Cross Origin Isolation is not enabled, volume rendering will not work (e.g., MPR)',
|
||||
type: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
appConfig.showCPUFallbackMessage &&
|
||||
cornerstone.getShouldUseCPURendering()
|
||||
) {
|
||||
if (appConfig.showCPUFallbackMessage && cornerstone.getShouldUseCPURendering()) {
|
||||
_showCPURenderingModal(uiModalService, hangingProtocolService);
|
||||
}
|
||||
|
||||
@@ -122,18 +108,14 @@ export default async function init({
|
||||
clearOnModeExit: true,
|
||||
});
|
||||
|
||||
const labelmapRepresentation =
|
||||
cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
const labelmapRepresentation = cornerstoneTools.Enums.SegmentationRepresentations.Labelmap;
|
||||
|
||||
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(
|
||||
labelmapRepresentation,
|
||||
{
|
||||
fillAlpha: 0.3,
|
||||
fillAlphaInactive: 0.2,
|
||||
outlineOpacity: 1,
|
||||
outlineOpacityInactive: 0.65,
|
||||
}
|
||||
);
|
||||
cornerstoneTools.segmentation.config.setGlobalRepresentationConfig(labelmapRepresentation, {
|
||||
fillAlpha: 0.3,
|
||||
fillAlphaInactive: 0.2,
|
||||
outlineOpacity: 1,
|
||||
outlineOpacityInactive: 0.65,
|
||||
});
|
||||
|
||||
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||
|
||||
@@ -142,14 +124,8 @@ export default async function init({
|
||||
cornerstoneStreamingImageVolumeLoader
|
||||
);
|
||||
|
||||
hangingProtocolService.registerImageLoadStrategy(
|
||||
'interleaveCenter',
|
||||
interleaveCenterLoader
|
||||
);
|
||||
hangingProtocolService.registerImageLoadStrategy(
|
||||
'interleaveTopToBottom',
|
||||
interleaveTopToBottom
|
||||
);
|
||||
hangingProtocolService.registerImageLoadStrategy('interleaveCenter', interleaveCenterLoader);
|
||||
hangingProtocolService.registerImageLoadStrategy('interleaveTopToBottom', interleaveTopToBottom);
|
||||
hangingProtocolService.registerImageLoadStrategy('nth', nthLoader);
|
||||
|
||||
// add metadata providers
|
||||
@@ -166,12 +142,10 @@ export default async function init({
|
||||
prefetch: appConfig?.maxNumRequests?.prefetch || 10,
|
||||
};
|
||||
|
||||
initWADOImageLoader(userAuthenticationService, appConfig);
|
||||
initWADOImageLoader(userAuthenticationService, appConfig, extensionManager);
|
||||
|
||||
/* Measurement Service */
|
||||
this.measurementServiceSource = connectToolsToMeasurementService(
|
||||
servicesManager
|
||||
);
|
||||
this.measurementServiceSource = connectToolsToMeasurementService(servicesManager);
|
||||
|
||||
initCineService(cineService);
|
||||
|
||||
@@ -181,31 +155,18 @@ export default async function init({
|
||||
volumeInputArrayMap => {
|
||||
for (const entry of volumeInputArrayMap.entries()) {
|
||||
const [viewportId, volumeInputArray] = entry;
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(
|
||||
viewportId
|
||||
);
|
||||
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId);
|
||||
|
||||
const ohifViewport = cornerstoneViewportService.getViewportInfo(
|
||||
viewportId
|
||||
);
|
||||
const ohifViewport = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
|
||||
const {
|
||||
lutPresentationStore,
|
||||
positionPresentationStore,
|
||||
} = stateSyncService.getState();
|
||||
const { lutPresentationStore, positionPresentationStore } = stateSyncService.getState();
|
||||
const { presentationIds } = ohifViewport.getViewportOptions();
|
||||
const presentations = {
|
||||
positionPresentation:
|
||||
positionPresentationStore[presentationIds?.positionPresentationId],
|
||||
lutPresentation:
|
||||
lutPresentationStore[presentationIds?.lutPresentationId],
|
||||
positionPresentation: positionPresentationStore[presentationIds?.positionPresentationId],
|
||||
lutPresentation: lutPresentationStore[presentationIds?.lutPresentationId],
|
||||
};
|
||||
|
||||
cornerstoneViewportService.setVolumesForViewport(
|
||||
viewport,
|
||||
volumeInputArray,
|
||||
presentations
|
||||
);
|
||||
cornerstoneViewportService.setVolumesForViewport(viewport, volumeInputArray, presentations);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -228,9 +189,7 @@ export default async function init({
|
||||
|
||||
const resetCrosshairs = evt => {
|
||||
const { element } = evt.detail;
|
||||
const { viewportId, renderingEngineId } = cornerstone.getEnabledElement(
|
||||
element
|
||||
);
|
||||
const { viewportId, renderingEngineId } = cornerstone.getEnabledElement(element);
|
||||
|
||||
const toolGroup = cornerstoneTools.ToolGroupManager.getToolGroupForViewport(
|
||||
viewportId,
|
||||
@@ -256,10 +215,7 @@ export default async function init({
|
||||
const { element } = evt.detail;
|
||||
element.addEventListener(EVENTS.CAMERA_RESET, resetCrosshairs);
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.STACK_VIEWPORT_NEW_STACK,
|
||||
newStackCallback
|
||||
);
|
||||
eventTarget.addEventListener(EVENTS.STACK_VIEWPORT_NEW_STACK, newStackCallback);
|
||||
}
|
||||
|
||||
function elementDisabledHandler(evt) {
|
||||
@@ -274,20 +230,13 @@ export default async function init({
|
||||
// );
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.ELEMENT_ENABLED,
|
||||
elementEnabledHandler.bind(null)
|
||||
);
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.ELEMENT_DISABLED,
|
||||
elementDisabledHandler.bind(null)
|
||||
);
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
||||
|
||||
viewportGridService.subscribe(
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED,
|
||||
({ viewportIndex, viewportId }) => {
|
||||
viewportId = viewportId || `viewport-${viewportIndex}`;
|
||||
viewportGridService.EVENTS.ACTIVE_VIEWPORT_ID_CHANGED,
|
||||
({ viewportId }) => {
|
||||
const toolGroup = toolGroupService.getToolGroupForViewport(viewportId);
|
||||
|
||||
if (!toolGroup || !toolGroup._toolInstances?.['ReferenceLines']) {
|
||||
@@ -296,8 +245,7 @@ export default async function init({
|
||||
|
||||
// check if reference lines are active
|
||||
const referenceLinesEnabled =
|
||||
toolGroup._toolInstances['ReferenceLines'].mode ===
|
||||
Enums.ToolModes.Enabled;
|
||||
toolGroup._toolInstances['ReferenceLines'].mode === Enums.ToolModes.Enabled;
|
||||
|
||||
if (!referenceLinesEnabled) {
|
||||
return;
|
||||
@@ -322,10 +270,9 @@ function CPUModal() {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
Your computer does not have enough GPU power to support the default GPU
|
||||
rendering mode. OHIF has switched to CPU rendering mode. Please note
|
||||
that CPU rendering does not support all features such as Volume
|
||||
Rendering, Multiplanar Reconstruction, and Segmentation Overlays.
|
||||
Your computer does not have enough GPU power to support the default GPU rendering mode. OHIF
|
||||
has switched to CPU rendering mode. Please note that CPU rendering does not support all
|
||||
features such as Volume Rendering, Multiplanar Reconstruction, and Segmentation Overlays.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -35,9 +35,15 @@ const DEFAULT_CONTEXT_MENU_CLICKS = {
|
||||
function getEventName(evt) {
|
||||
const button = evt.detail.event.which;
|
||||
const nameArr = [];
|
||||
if (evt.detail.event.altKey) nameArr.push('alt');
|
||||
if (evt.detail.event.ctrlKey) nameArr.push('ctrl');
|
||||
if (evt.detail.event.shiftKey) nameArr.push('shift');
|
||||
if (evt.detail.event.altKey) {
|
||||
nameArr.push('alt');
|
||||
}
|
||||
if (evt.detail.event.ctrlKey) {
|
||||
nameArr.push('ctrl');
|
||||
}
|
||||
if (evt.detail.event.shiftKey) {
|
||||
nameArr.push('shift');
|
||||
}
|
||||
nameArr.push('button');
|
||||
nameArr.push(button);
|
||||
return nameArr.join('');
|
||||
@@ -54,10 +60,8 @@ function initContextMenu({
|
||||
*/
|
||||
const cornerstoneViewportHandleEvent = (name, evt) => {
|
||||
const customizations =
|
||||
customizationService.get('cornerstoneViewportClickCommands') ||
|
||||
DEFAULT_CONTEXT_MENU_CLICKS;
|
||||
customizationService.get('cornerstoneViewportClickCommands') || DEFAULT_CONTEXT_MENU_CLICKS;
|
||||
const toRun = customizations[name];
|
||||
console.log('initContextMenu::cornerstoneViewportHandleEvent', name, toRun);
|
||||
const options = {
|
||||
nearbyToolData: findNearbyToolData(commandsManager, evt),
|
||||
event: evt,
|
||||
@@ -73,35 +77,24 @@ function initContextMenu({
|
||||
function elementEnabledHandler(evt) {
|
||||
const { viewportId, element } = evt.detail;
|
||||
const viewportInfo = cornerstoneViewportService.getViewportInfo(viewportId);
|
||||
if (!viewportInfo) return;
|
||||
const viewportIndex = viewportInfo.getViewportIndex();
|
||||
if (!viewportInfo) {
|
||||
return;
|
||||
}
|
||||
// TODO check update upstream
|
||||
setEnabledElement(viewportIndex, element);
|
||||
setEnabledElement(viewportId, element);
|
||||
|
||||
element.addEventListener(
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
cornerstoneViewportHandleClick
|
||||
);
|
||||
element.addEventListener(cs3DToolsEvents.MOUSE_CLICK, cornerstoneViewportHandleClick);
|
||||
}
|
||||
|
||||
function elementDisabledHandler(evt) {
|
||||
const { element } = evt.detail;
|
||||
|
||||
element.removeEventListener(
|
||||
cs3DToolsEvents.MOUSE_CLICK,
|
||||
cornerstoneViewportHandleClick
|
||||
);
|
||||
element.removeEventListener(cs3DToolsEvents.MOUSE_CLICK, cornerstoneViewportHandleClick);
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.ELEMENT_ENABLED,
|
||||
elementEnabledHandler.bind(null)
|
||||
);
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.ELEMENT_DISABLED,
|
||||
elementDisabledHandler.bind(null)
|
||||
);
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
||||
}
|
||||
|
||||
export default initContextMenu;
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from '@cornerstonejs/tools';
|
||||
|
||||
import CalibrationLineTool from './tools/CalibrationLineTool';
|
||||
import ImageOverlayViewerTool from './tools/ImageOverlayViewerTool';
|
||||
|
||||
export default function initCornerstoneTools(configuration = {}) {
|
||||
CrosshairsTool.isAnnotation = false;
|
||||
@@ -58,6 +59,7 @@ export default function initCornerstoneTools(configuration = {}) {
|
||||
addTool(ReferenceLinesTool);
|
||||
addTool(CalibrationLineTool);
|
||||
addTool(TrackballRotateTool);
|
||||
addTool(ImageOverlayViewerTool);
|
||||
|
||||
// Modify annotation tools to use dashed lines on SR
|
||||
const annotationStyle = {
|
||||
@@ -99,6 +101,7 @@ const toolNames = {
|
||||
ReferenceLines: ReferenceLinesTool.toolName,
|
||||
CalibrationLine: CalibrationLineTool.toolName,
|
||||
TrackballRotateTool: TrackballRotateTool.toolName,
|
||||
ImageOverlayViewer: ImageOverlayViewerTool.toolName,
|
||||
};
|
||||
|
||||
export { toolNames };
|
||||
@@ -21,9 +21,15 @@ const DEFAULT_DOUBLE_CLICK = {
|
||||
*/
|
||||
function getDoubleClickEventName(evt: CustomEvent) {
|
||||
const nameArr = [];
|
||||
if (evt.detail.event.altKey) nameArr.push('alt');
|
||||
if (evt.detail.event.ctrlKey) nameArr.push('ctrl');
|
||||
if (evt.detail.event.shiftKey) nameArr.push('shift');
|
||||
if (evt.detail.event.altKey) {
|
||||
nameArr.push('alt');
|
||||
}
|
||||
if (evt.detail.event.ctrlKey) {
|
||||
nameArr.push('ctrl');
|
||||
}
|
||||
if (evt.detail.event.shiftKey) {
|
||||
nameArr.push('shift');
|
||||
}
|
||||
nameArr.push('doubleClick');
|
||||
return nameArr.join('');
|
||||
}
|
||||
@@ -33,10 +39,7 @@ export type initDoubleClickArgs = {
|
||||
commandsManager: CommandsManager;
|
||||
};
|
||||
|
||||
function initDoubleClick({
|
||||
customizationService,
|
||||
commandsManager,
|
||||
}: initDoubleClickArgs): void {
|
||||
function initDoubleClick({ customizationService, commandsManager }: initDoubleClickArgs): void {
|
||||
const cornerstoneViewportHandleDoubleClick = (evt: CustomEvent) => {
|
||||
// Do not allow double click on a tool.
|
||||
const nearbyToolData = findNearbyToolData(commandsManager, evt);
|
||||
@@ -48,8 +51,7 @@ function initDoubleClick({
|
||||
|
||||
// Allows for the customization of the double click on a viewport.
|
||||
const customizations =
|
||||
customizationService.get('cornerstoneViewportClickCommands') ||
|
||||
DEFAULT_DOUBLE_CLICK;
|
||||
customizationService.get('cornerstoneViewportClickCommands') || DEFAULT_DOUBLE_CLICK;
|
||||
|
||||
const toRun = customizations[eventName];
|
||||
|
||||
@@ -78,15 +80,9 @@ function initDoubleClick({
|
||||
);
|
||||
}
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.ELEMENT_ENABLED,
|
||||
elementEnabledHandler.bind(null)
|
||||
);
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_ENABLED, elementEnabledHandler.bind(null));
|
||||
|
||||
eventTarget.addEventListener(
|
||||
EVENTS.ELEMENT_DISABLED,
|
||||
elementDisabledHandler.bind(null)
|
||||
);
|
||||
eventTarget.addEventListener(EVENTS.ELEMENT_DISABLED, elementDisabledHandler.bind(null));
|
||||
}
|
||||
|
||||
export default initDoubleClick;
|
||||
@@ -127,11 +127,8 @@ const initMeasurementService = (
|
||||
};
|
||||
|
||||
const connectToolsToMeasurementService = servicesManager => {
|
||||
const {
|
||||
measurementService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService,
|
||||
} = servicesManager.services;
|
||||
const { measurementService, displaySetService, cornerstoneViewportService } =
|
||||
servicesManager.services;
|
||||
const csTools3DVer1MeasurementSource = initMeasurementService(
|
||||
measurementService,
|
||||
displaySetService,
|
||||
@@ -153,10 +150,7 @@ const connectToolsToMeasurementService = servicesManager => {
|
||||
} = annotationAddedEventDetail;
|
||||
const { toolName } = metadata;
|
||||
|
||||
if (
|
||||
csToolsEvent.type === completedEvt &&
|
||||
toolName === toolNames.CalibrationLine
|
||||
) {
|
||||
if (csToolsEvent.type === completedEvt && toolName === toolNames.CalibrationLine) {
|
||||
// show modal to input the measurement (mm)
|
||||
onCompletedCalibrationLine(servicesManager, csToolsEvent)
|
||||
.then(
|
||||
@@ -212,10 +206,8 @@ const connectToolsToMeasurementService = servicesManager => {
|
||||
try {
|
||||
const annotationSelectionEventDetail = csToolsEvent.detail;
|
||||
|
||||
const {
|
||||
added: addedSelectedAnnotationUIDs,
|
||||
removed: removedSelectedAnnotationUIDs,
|
||||
} = annotationSelectionEventDetail;
|
||||
const { added: addedSelectedAnnotationUIDs, removed: removedSelectedAnnotationUIDs } =
|
||||
annotationSelectionEventDetail;
|
||||
|
||||
if (removedSelectedAnnotationUIDs) {
|
||||
removedSelectedAnnotationUIDs.forEach(annotationUID =>
|
||||
@@ -283,12 +275,8 @@ const connectMeasurementServiceToTools = (
|
||||
cornerstoneViewportService,
|
||||
measurementSource
|
||||
) => {
|
||||
const {
|
||||
MEASUREMENT_REMOVED,
|
||||
MEASUREMENTS_CLEARED,
|
||||
MEASUREMENT_UPDATED,
|
||||
RAW_MEASUREMENT_ADDED,
|
||||
} = measurementService.EVENTS;
|
||||
const { MEASUREMENT_REMOVED, MEASUREMENTS_CLEARED, MEASUREMENT_UPDATED, RAW_MEASUREMENT_ADDED } =
|
||||
measurementService.EVENTS;
|
||||
|
||||
const csTools3DVer1MeasurementSource = measurementService.getSource(
|
||||
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
||||
@@ -351,11 +339,7 @@ const connectMeasurementServiceToTools = (
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
referenceSeriesUID,
|
||||
referenceStudyUID,
|
||||
SOPInstanceUID,
|
||||
} = measurement;
|
||||
const { referenceSeriesUID, referenceStudyUID, SOPInstanceUID } = measurement;
|
||||
|
||||
const instance = DicomMetadataStore.getInstance(
|
||||
referenceStudyUID,
|
||||
@@ -368,9 +352,7 @@ const connectMeasurementServiceToTools = (
|
||||
|
||||
if (measurement?.metadata?.referencedImageId) {
|
||||
imageId = measurement.metadata.referencedImageId;
|
||||
frameNumber = getSOPInstanceAttributes(
|
||||
measurement.metadata.referencedImageId
|
||||
).frameNumber;
|
||||
frameNumber = getSOPInstanceAttributes(measurement.metadata.referencedImageId).frameNumber;
|
||||
} else {
|
||||
imageId = dataSource.getImageIdsForInstance({ instance });
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import * as cornerstone from '@cornerstonejs/core';
|
||||
import { volumeLoader } from '@cornerstonejs/core';
|
||||
import { cornerstoneStreamingImageVolumeLoader } from '@cornerstonejs/streaming-image-volume-loader';
|
||||
import dicomImageLoader, {
|
||||
webWorkerManager,
|
||||
} from '@cornerstonejs/dicom-image-loader';
|
||||
import dicomImageLoader, { webWorkerManager } from '@cornerstonejs/dicom-image-loader';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import { errorHandler } from '@ohif/core';
|
||||
import { errorHandler, utils } from '@ohif/core';
|
||||
|
||||
const { registerVolumeLoader } = volumeLoader;
|
||||
|
||||
@@ -35,15 +33,13 @@ function initWebWorkers(appConfig) {
|
||||
|
||||
export default function initWADOImageLoader(
|
||||
userAuthenticationService,
|
||||
appConfig
|
||||
appConfig,
|
||||
extensionManager
|
||||
) {
|
||||
dicomImageLoader.external.cornerstone = cornerstone;
|
||||
dicomImageLoader.external.dicomParser = dicomParser;
|
||||
|
||||
registerVolumeLoader(
|
||||
'cornerstoneStreamingImageVolume',
|
||||
cornerstoneStreamingImageVolumeLoader
|
||||
);
|
||||
registerVolumeLoader('cornerstoneStreamingImageVolume', cornerstoneStreamingImageVolumeLoader);
|
||||
|
||||
dicomImageLoader.configure({
|
||||
decodeConfig: {
|
||||
@@ -54,19 +50,18 @@ export default function initWADOImageLoader(
|
||||
// we should set this flag to false.
|
||||
convertFloatPixelDataToInt: false,
|
||||
},
|
||||
beforeSend: function(xhr) {
|
||||
beforeSend: function (xhr) {
|
||||
//TODO should be removed in the future and request emitted by DicomWebDataSource
|
||||
const sourceConfig = extensionManager.getActiveDataSource()?.[0].getConfig() ?? {};
|
||||
const headers = userAuthenticationService.getAuthorizationHeader();
|
||||
const acceptHeader = utils.generateAcceptHeader(
|
||||
sourceConfig.acceptHeader,
|
||||
sourceConfig.requestTransferSyntaxUID,
|
||||
sourceConfig.omitQuotationForMultipartRequest
|
||||
);
|
||||
|
||||
// Request:
|
||||
// JPEG-LS Lossless (1.2.840.10008.1.2.4.80) if available, otherwise accept
|
||||
// whatever transfer-syntax the origin server provides.
|
||||
// For now we use image/jls and image/x-jls because some servers still use the old type
|
||||
// http://dicom.nema.org/medical/dicom/current/output/html/part18.html
|
||||
const xhrRequestHeaders = {
|
||||
Accept: appConfig.omitQuotationForMultipartRequest
|
||||
? 'multipart/related; type=application/octet-stream'
|
||||
: 'multipart/related; type="application/octet-stream"',
|
||||
// 'multipart/related; type="image/x-jls", multipart/related; type="image/jls"; transfer-syntax="1.2.840.10008.1.2.4.80", multipart/related; type="image/x-jls", multipart/related; type="application/octet-stream"; transfer-syntax=*',
|
||||
Accept: acceptHeader,
|
||||
};
|
||||
|
||||
if (headers) {
|
||||
|
||||
+11
-43
@@ -1,16 +1,8 @@
|
||||
import { ServicesManager, Types } from '@ohif/core';
|
||||
import {
|
||||
cache as cs3DCache,
|
||||
Enums,
|
||||
volumeLoader,
|
||||
utilities as utils,
|
||||
} from '@cornerstonejs/core';
|
||||
import { cache as cs3DCache, Enums, volumeLoader, utilities as utils } from '@cornerstonejs/core';
|
||||
|
||||
import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType';
|
||||
import {
|
||||
StackViewportData,
|
||||
VolumeViewportData,
|
||||
} from '../../types/CornerstoneCacheService';
|
||||
import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService';
|
||||
|
||||
const VOLUME_LOADER_SCHEME = 'cornerstoneStreamingImageVolume';
|
||||
|
||||
@@ -18,9 +10,7 @@ class CornerstoneCacheService {
|
||||
static REGISTRATION = {
|
||||
name: 'cornerstoneCacheService',
|
||||
altName: 'CornerstoneCacheService',
|
||||
create: ({
|
||||
servicesManager,
|
||||
}: Types.Extensions.ExtensionParams): CornerstoneCacheService => {
|
||||
create: ({ servicesManager }: Types.Extensions.ExtensionParams): CornerstoneCacheService => {
|
||||
return new CornerstoneCacheService(servicesManager);
|
||||
},
|
||||
};
|
||||
@@ -77,11 +67,7 @@ class CornerstoneCacheService {
|
||||
cs3DViewportType === Enums.ViewportType.ORTHOGRAPHIC ||
|
||||
cs3DViewportType === Enums.ViewportType.VOLUME_3D
|
||||
) {
|
||||
viewportData = await this._getVolumeViewportData(
|
||||
dataSource,
|
||||
displaySets,
|
||||
cs3DViewportType
|
||||
);
|
||||
viewportData = await this._getVolumeViewportData(dataSource, displaySets, cs3DViewportType);
|
||||
}
|
||||
|
||||
viewportData.viewportType = cs3DViewportType;
|
||||
@@ -134,20 +120,14 @@ class CornerstoneCacheService {
|
||||
// For Stack Viewport we don't have fusion currently
|
||||
const displaySet = displaySets[0];
|
||||
|
||||
let stackImageIds = this.stackImageIds.get(
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
let stackImageIds = this.stackImageIds.get(displaySet.displaySetInstanceUID);
|
||||
|
||||
if (!stackImageIds) {
|
||||
stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||
this.stackImageIds.set(displaySet.displaySetInstanceUID, stackImageIds);
|
||||
}
|
||||
|
||||
const {
|
||||
displaySetInstanceUID,
|
||||
StudyInstanceUID,
|
||||
isCompositeStack,
|
||||
} = displaySet;
|
||||
const { displaySetInstanceUID, StudyInstanceUID, isCompositeStack } = displaySet;
|
||||
|
||||
const StackViewportData: StackViewportData = {
|
||||
viewportType,
|
||||
@@ -196,31 +176,22 @@ class CornerstoneCacheService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const volumeLoaderSchema =
|
||||
displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
|
||||
const volumeLoaderSchema = displaySet.volumeLoaderSchema ?? VOLUME_LOADER_SCHEME;
|
||||
|
||||
const volumeId = `${volumeLoaderSchema}:${displaySet.displaySetInstanceUID}`;
|
||||
|
||||
let volumeImageIds = this.volumeImageIds.get(
|
||||
displaySet.displaySetInstanceUID
|
||||
);
|
||||
let volumeImageIds = this.volumeImageIds.get(displaySet.displaySetInstanceUID);
|
||||
|
||||
let volume = cs3DCache.getVolume(volumeId);
|
||||
|
||||
if (!volumeImageIds || !volume) {
|
||||
volumeImageIds = this._getCornerstoneVolumeImageIds(
|
||||
displaySet,
|
||||
dataSource
|
||||
);
|
||||
volumeImageIds = this._getCornerstoneVolumeImageIds(displaySet, dataSource);
|
||||
|
||||
volume = await volumeLoader.createAndCacheVolume(volumeId, {
|
||||
imageIds: volumeImageIds,
|
||||
});
|
||||
|
||||
this.volumeImageIds.set(
|
||||
displaySet.displaySetInstanceUID,
|
||||
volumeImageIds
|
||||
);
|
||||
this.volumeImageIds.set(displaySet.displaySetInstanceUID, volumeImageIds);
|
||||
}
|
||||
|
||||
volumeData.push({
|
||||
@@ -268,10 +239,7 @@ class CornerstoneCacheService {
|
||||
}
|
||||
|
||||
private _getCornerstoneVolumeImageIds(displaySet, dataSource): string[] {
|
||||
const stackImageIds = this._getCornerstoneStackImageIds(
|
||||
displaySet,
|
||||
dataSource
|
||||
);
|
||||
const stackImageIds = this._getCornerstoneStackImageIds(displaySet, dataSource);
|
||||
|
||||
return stackImageIds;
|
||||
}
|
||||
|
||||
+19
-24
@@ -7,32 +7,27 @@
|
||||
* @returns An array of object that includes data, id, segmentIndex, color
|
||||
* and geometry Id
|
||||
*/
|
||||
export function mapROIContoursToRTStructData(
|
||||
structureSet: unknown,
|
||||
rtDisplaySetUID: unknown
|
||||
) {
|
||||
return structureSet.ROIContours.map(
|
||||
({ contourPoints, ROINumber, ROIName, colorArray }) => {
|
||||
const data = contourPoints.map(({ points, ...rest }) => {
|
||||
const newPoints = points.map(({ x, y, z }) => {
|
||||
return [x, y, z];
|
||||
});
|
||||
|
||||
return {
|
||||
...rest,
|
||||
points: newPoints,
|
||||
};
|
||||
export function mapROIContoursToRTStructData(structureSet: unknown, rtDisplaySetUID: unknown) {
|
||||
return structureSet.ROIContours.map(({ contourPoints, ROINumber, ROIName, colorArray }) => {
|
||||
const data = contourPoints.map(({ points, ...rest }) => {
|
||||
const newPoints = points.map(({ x, y, z }) => {
|
||||
return [x, y, z];
|
||||
});
|
||||
|
||||
const id = ROIName || ROINumber;
|
||||
|
||||
return {
|
||||
data,
|
||||
id,
|
||||
segmentIndex: ROINumber,
|
||||
color: colorArray,
|
||||
geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`,
|
||||
...rest,
|
||||
points: newPoints,
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const id = ROIName || ROINumber;
|
||||
|
||||
return {
|
||||
data,
|
||||
id,
|
||||
segmentIndex: ROINumber,
|
||||
color: colorArray,
|
||||
geometryId: `${rtDisplaySetUID}:${id}:segmentIndex-${ROINumber}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
Loaded 100 of 1295 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user