Compare commits
157
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2e75b0f20 | ||
|
|
07b272e024 | ||
|
|
a08caa9bdb | ||
|
|
29afddb3a6 | ||
|
|
f85f856734 | ||
|
|
c6bc19c002 | ||
|
|
538555f000 | ||
|
|
8681036b21 | ||
|
|
e55b909ca6 | ||
|
|
4bb36704ee | ||
|
|
9bbdb35d88 | ||
|
|
2f443fa2ef | ||
|
|
6d7d7da287 | ||
|
|
1f6751f95b | ||
|
|
92081a097d | ||
|
|
ba1a33c3ac | ||
|
|
193ff12b6e | ||
|
|
3296ffaa07 | ||
|
|
0456d92462 | ||
|
|
db3b15e537 | ||
|
|
6e723ebced | ||
|
|
a6a231c81e | ||
|
|
7c2bd9b76a | ||
|
|
5dfa8d465d | ||
|
|
fc5301e904 | ||
|
|
88e5a7e54a | ||
|
|
5d24bb4b73 | ||
|
|
03a4056179 | ||
|
|
ec76bc3b0b | ||
|
|
b1070acaca | ||
|
|
7b5beb8f98 | ||
|
|
97410e12b9 | ||
|
|
bdecbfeee3 | ||
|
|
5a970056ae | ||
|
|
8581405352 | ||
|
|
df8c4db2ed | ||
|
|
ea62bdaddf | ||
|
|
e983b02677 | ||
|
|
1e4b78d78d | ||
|
|
ee6131f98e | ||
|
|
a56bdd035b | ||
|
|
04901c296e | ||
|
|
59cadad98c | ||
|
|
14d6454eaf | ||
|
|
64f87d2c93 | ||
|
|
cfd66e5ef7 | ||
|
|
6480389778 | ||
|
|
0e736e7a56 | ||
|
|
d246cd9842 | ||
|
|
4fa1eb683d | ||
|
|
ea6842a276 | ||
|
|
d97270f995 | ||
|
|
0ef76f8b66 | ||
|
|
05b5a10af9 | ||
|
|
e1150acaf6 | ||
|
|
4abdbe0584 | ||
|
|
9223a52af9 | ||
|
|
06f58d18bf | ||
|
|
7c5f9c44b6 | ||
|
|
ef341d1518 | ||
|
|
d956963ef0 | ||
|
|
83b7d9e8b4 | ||
|
|
12e73228d1 | ||
|
|
4002401c66 | ||
|
|
18a8e059b6 | ||
|
|
f3582e514e | ||
|
|
c76bfd4a58 | ||
|
|
7f8cf5f5da | ||
|
|
5ef926477c | ||
|
|
87f5b97983 | ||
|
|
4c7b2b4ae0 | ||
|
|
d1e52dd6a6 | ||
|
|
cb147dfc69 | ||
|
|
a5fdeddfb4 | ||
|
|
e0e5f6000f | ||
|
|
cffb7d6f2f | ||
|
|
360616be13 | ||
|
|
d8fe8929b6 | ||
|
|
6b99a21227 | ||
|
|
16631e1fc4 | ||
|
|
587de94cea | ||
|
|
0ebe8a67d2 | ||
|
|
39a988dac7 | ||
|
|
f95928bcdd | ||
|
|
0f736bd294 | ||
|
|
6cbd9839c6 | ||
|
|
649bc5825a | ||
|
|
8038009afd | ||
|
|
f07f21117d | ||
|
|
77c7463c53 | ||
|
|
449e8f25f6 | ||
|
|
437c01511e | ||
|
|
00fddc1550 | ||
|
|
00b5cccae5 | ||
|
|
e545dd5b28 | ||
|
|
efde6ed20e | ||
|
|
32022f51f2 | ||
|
|
d282f72d75 | ||
|
|
29fceacee9 | ||
|
|
a7a5a359dc | ||
|
|
e362e895d4 | ||
|
|
45a07dfa19 | ||
|
|
7323e63608 | ||
|
|
0baf18dcb2 | ||
|
|
cca3d6bff2 | ||
|
|
f3cecf129b | ||
|
|
fe03fc6f55 | ||
|
|
b59187c74d | ||
|
|
5210c80369 | ||
|
|
f57725ac8b | ||
|
|
138e0fb3bf | ||
|
|
be494a7376 | ||
|
|
ab23feefbb | ||
|
|
4c079044f6 | ||
|
|
81fcfc7370 | ||
|
|
6ec6446787 | ||
|
|
4b5a43641a | ||
|
|
2fb0b01fe2 | ||
|
|
3632e5d495 | ||
|
|
dfe566e2aa | ||
|
|
6c5ad9e98d | ||
|
|
aeb7d5edcc | ||
|
|
d5b89748c9 | ||
|
|
b161016edb | ||
|
|
2fff2d511f | ||
|
|
cc92f91c1f | ||
|
|
d2e777bafc | ||
|
|
c0c3ab307d | ||
|
|
5121913ee6 | ||
|
|
6ef2b19925 | ||
|
|
f2450be255 | ||
|
|
04ca32a93e | ||
|
|
c92de6c647 | ||
|
|
5b302843d4 | ||
|
|
7eb8c4c641 | ||
|
|
619f361510 | ||
|
|
330eeaa216 | ||
|
|
d627dee3b2 | ||
|
|
d8714ea9c4 | ||
|
|
3c05920705 | ||
|
|
c6629bda75 | ||
|
|
588bcc9d17 | ||
|
|
c31f8f9d65 | ||
|
|
c0280e28ca | ||
|
|
7c3ac387ef | ||
|
|
d6f79d9ddf | ||
|
|
9448993cf0 | ||
|
|
1c556b4cc3 | ||
|
|
94a9067fe3 | ||
|
|
cfba4df70e | ||
|
|
11c1d12872 | ||
|
|
91364db882 | ||
|
|
bea31d5fec | ||
|
|
48ecbc261a | ||
|
|
8bc467fb07 | ||
|
|
cffc44f0f4 | ||
|
|
ea0fc07e30 |
No files matched your search
+76
-45
@@ -13,11 +13,23 @@ version: 2.1
|
||||
##
|
||||
orbs:
|
||||
codecov: codecov/codecov@1.0.5
|
||||
cypress: cypress-io/cypress@1.13.0
|
||||
cypress: cypress-io/cypress@1.26.0
|
||||
executors:
|
||||
# Custom executor to override Cypress config
|
||||
deploy-to-prod-executor:
|
||||
docker:
|
||||
- image: 'cypress/browsers:node14.15.0-chrome86-ff82'
|
||||
environment:
|
||||
CYPRESS_BASE_URL: https://ohif-staging.netlify.com/
|
||||
chrome-and-pacs:
|
||||
docker:
|
||||
# Primary container image where all steps run.
|
||||
- image: 'cypress/browsers:node14.15.0-chrome86-ff82'
|
||||
- image: 'ohif/viewer-testdata:0.1-test'
|
||||
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: circleci/node:12.9.1
|
||||
- image: circleci/node:14.15.0
|
||||
environment:
|
||||
TERM: xterm # Enable colors in term
|
||||
QUICK_BUILD: true
|
||||
@@ -36,12 +48,12 @@ jobs:
|
||||
- checkout:
|
||||
post:
|
||||
- git fetch --all
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
# when lock file changes, use increasingly general patterns to restore cache
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
# - restore_cache:
|
||||
# name: Restore Yarn and Cypress Package Cache
|
||||
# keys:
|
||||
# # when lock file changes, use increasingly general patterns to restore cache
|
||||
# - yarn-packages-{{ checksum "yarn.lock" }}
|
||||
# - yarn-packages-
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
@@ -95,12 +107,12 @@ jobs:
|
||||
post:
|
||||
- git fetch --all
|
||||
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
# when lock file changes, use increasingly general patterns to restore cache
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
# - restore_cache:
|
||||
# name: Restore Yarn and Cypress Package Cache
|
||||
# keys:
|
||||
# # when lock file changes, use increasingly general patterns to restore cache
|
||||
# - yarn-packages-{{ checksum "yarn.lock" }}
|
||||
# - yarn-packages-
|
||||
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
@@ -131,12 +143,12 @@ jobs:
|
||||
- checkout:
|
||||
post:
|
||||
- git fetch --all
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
# when lock file changes, use increasingly general patterns to restore cache
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
# - restore_cache:
|
||||
# name: Restore Yarn and Cypress Package Cache
|
||||
# keys:
|
||||
# # when lock file changes, use increasingly general patterns to restore cache
|
||||
# - yarn-packages-{{ checksum "yarn.lock" }}
|
||||
# - yarn-packages-
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
@@ -170,7 +182,7 @@ jobs:
|
||||
|
||||
DEPLOY_TO_DEV:
|
||||
docker:
|
||||
- image: circleci/node:12.9.1
|
||||
- image: circleci/node:14.15.0
|
||||
environment:
|
||||
TERM: xterm
|
||||
NETLIFY_SITE_ID: 32708787-c9b0-4634-b50f-7ca41952da77
|
||||
@@ -185,7 +197,7 @@ jobs:
|
||||
|
||||
DEPLOY_TO_STAGING:
|
||||
docker:
|
||||
- image: circleci/node:12.9.1
|
||||
- image: circleci/node:14.15.0
|
||||
environment:
|
||||
TERM: xterm
|
||||
NETLIFY_SITE_ID: c7502ae3-b150-493c-8422-05701e44a969
|
||||
@@ -200,7 +212,7 @@ jobs:
|
||||
|
||||
DEPLOY_TO_PRODUCTION:
|
||||
docker:
|
||||
- image: circleci/node:12.9.1
|
||||
- image: circleci/node:14.15.0
|
||||
environment:
|
||||
TERM: xterm
|
||||
NETLIFY_SITE_ID: 79c4a5da-5c95-4dc9-84f7-45fd9dfe21b0
|
||||
@@ -225,11 +237,11 @@ jobs:
|
||||
post:
|
||||
- git fetch --all
|
||||
# Use increasingly general patterns to restore cache
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
# - restore_cache:
|
||||
# name: Restore Yarn and Cypress Package Cache
|
||||
# keys:
|
||||
# - yarn-packages-{{ checksum "yarn.lock" }}
|
||||
# - yarn-packages-
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
@@ -257,7 +269,12 @@ jobs:
|
||||
paths: .
|
||||
|
||||
DOCS_PUBLISH:
|
||||
<<: *defaults
|
||||
docker:
|
||||
# Gitbook fails due to graceful-fs updates above this node version :-(
|
||||
- image: circleci/node:12.9.1
|
||||
environment:
|
||||
TERM: xterm # Enable colors in term
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
@@ -317,16 +334,21 @@ workflows:
|
||||
# E2E: PWA
|
||||
- cypress/run:
|
||||
name: 'E2E: PWA'
|
||||
executor: cypress/browsers-chrome76
|
||||
executor: chrome-and-pacs
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
- run: |
|
||||
# Clear yarn cache; update to latest
|
||||
rm -rf ~/.yarn
|
||||
npm i -g yarn
|
||||
yarn -v
|
||||
yarn: true
|
||||
record: false
|
||||
store_artifacts: false
|
||||
record: true
|
||||
store_artifacts: true
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
yarn run build
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
|
||||
wait-on: 'http://localhost:3000'
|
||||
@@ -337,21 +359,24 @@ workflows:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/videos
|
||||
- store_test_results:
|
||||
path: platform/viewer/cypress/results
|
||||
requires:
|
||||
- UNIT_TESTS
|
||||
# E2E: script-tag
|
||||
- cypress/run:
|
||||
name: 'E2E: Script Tag'
|
||||
executor: cypress/browsers-chrome76
|
||||
executor: chrome-and-pacs
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v' # Use yarn latest
|
||||
yarn: true
|
||||
record: false
|
||||
store_artifacts: false
|
||||
record: true
|
||||
store_artifacts: true
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build:package
|
||||
build:
|
||||
npx cross-env QUICK_BUILD=true APP_CONFIG=config/dicomweb-server.js
|
||||
yarn run build:package
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
|
||||
wait-on: 'http://localhost:3000'
|
||||
@@ -362,6 +387,8 @@ workflows:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/videos
|
||||
- store_test_results:
|
||||
path: platform/viewer/cypress/results
|
||||
requires:
|
||||
- UNIT_TESTS
|
||||
|
||||
@@ -372,7 +399,7 @@ workflows:
|
||||
# Update hub.docker.org
|
||||
- cypress/run:
|
||||
name: 'Generate Percy Snapshots'
|
||||
executor: cypress/browsers-chrome76
|
||||
executor: chrome-and-pacs
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
@@ -380,7 +407,9 @@ workflows:
|
||||
yarn: true
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
build:
|
||||
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" }}'
|
||||
@@ -449,7 +478,7 @@ workflows:
|
||||
# and record a Cypress dashboard test run
|
||||
- cypress/run:
|
||||
name: 'Generate Percy Snapshots'
|
||||
executor: cypress/browsers-chrome76
|
||||
executor: chrome-and-pacs
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
@@ -457,7 +486,9 @@ workflows:
|
||||
yarn: true
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
build:
|
||||
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" }}'
|
||||
|
||||
@@ -28,6 +28,12 @@ if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
|
||||
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
|
||||
fi
|
||||
|
||||
if [ -n "${PORT}" ]
|
||||
then
|
||||
echo "Changing port to ${PORT}..."
|
||||
sed -i -e "s/listen 80/listen ${PORT}/g" /etc/nginx/conf.d/default.conf
|
||||
fi
|
||||
|
||||
echo "Starting Nginx to serve the OHIF Viewer..."
|
||||
|
||||
exec "$@"
|
||||
+2
-2
@@ -2,9 +2,9 @@
|
||||
# https://github.com/apps/stale
|
||||
#
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 21
|
||||
daysUntilStale: 180
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
daysUntilClose: 9000
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- 'Story :raised_hands:'
|
||||
|
||||
@@ -3,7 +3,6 @@ const dotenv = require('dotenv');
|
||||
//
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const PACKAGE = require('../platform/viewer/package.json');
|
||||
// ~~ RULES
|
||||
const loadShadersRule = require('./rules/loadShaders.js');
|
||||
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
||||
@@ -77,7 +76,10 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
|
||||
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
|
||||
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
|
||||
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
|
||||
'process.env.VERSION_NUMBER': webpack.DefinePlugin.runtimeValue(() => {
|
||||
const package = require('../platform/viewer/package.json');
|
||||
return JSON.stringify(package.version || '');
|
||||
}, ['../platform/viewer/package.json']),
|
||||
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
|
||||
/* i18n */
|
||||
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
|
||||
|
||||
+2
-1
@@ -21,7 +21,7 @@
|
||||
|
||||
# Stage 1: Build the application
|
||||
# docker build -t ohif/viewer:latest .
|
||||
FROM node:10.16.3-slim as builder
|
||||
FROM node:15.13.0-slim as builder
|
||||
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
@@ -39,6 +39,7 @@ COPY package.json /usr/src/app/package.json
|
||||
COPY postcss.config.js /usr/src/app/postcss.config.js
|
||||
COPY yarn.lock /usr/src/app/yarn.lock
|
||||
|
||||
RUN apt-get update && apt-get install -y python make g++
|
||||
# Run the install before copying the rest of the files
|
||||
RUN yarn config set workspaces-experimental true
|
||||
RUN yarn install
|
||||
|
||||
@@ -121,8 +121,7 @@ window.config = {
|
||||
};
|
||||
```
|
||||
|
||||
- Install the viewer:
|
||||
`window.OHIFViewer.installViewer(window.config);`
|
||||
- Install the viewer: `window.OHIFViewer.installViewer(window.config);`
|
||||
|
||||
This exact setup is demonstrated in this
|
||||
[CodeSandbox](https://codesandbox.io/s/viewer-script-tag-tprch) and in our
|
||||
@@ -259,6 +258,22 @@ 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).
|
||||
|
||||
## Projects that contributed to OHIF Viewer
|
||||
|
||||
The following is a (partial) list of projects that contributed resources towards
|
||||
development of OHIF Viewer:
|
||||
|
||||
- [NCI Imaging Data Commons (IDC) project](https://imaging.datacommons.cancer.gov/)
|
||||
supported 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 the 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.
|
||||
|
||||
## License
|
||||
|
||||
MIT © [OHIF](https://github.com/OHIF)
|
||||
|
||||
@@ -3,6 +3,89 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.10.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.6...@ohif/extension-cornerstone@2.10.7) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.5...@ohif/extension-cornerstone@2.10.6) (2021-03-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.4...@ohif/extension-cornerstone@2.10.5) (2021-03-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.3...@ohif/extension-cornerstone@2.10.4) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.2...@ohif/extension-cornerstone@2.10.3) (2021-03-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.1...@ohif/extension-cornerstone@2.10.2) (2021-02-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.10.0...@ohif/extension-cornerstone@2.10.1) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.10.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.9.6...@ohif/extension-cornerstone@2.10.0) (2020-12-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* visualize overlapping segments in cornerstone ([#2185](https://github.com/OHIF/Viewers/issues/2185)) ([29fceac](https://github.com/OHIF/Viewers/commit/29fceacee97d51f1952a0f6b574c66596d32c201))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.9.5...@ohif/extension-cornerstone@2.9.6) (2020-10-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.9.4...@ohif/extension-cornerstone@2.9.5) (2020-09-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.9.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.9.3...@ohif/extension-cornerstone@2.9.4) (2020-09-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.9.4",
|
||||
"version": "2.10.7",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -36,7 +36,7 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.16.3",
|
||||
"dcmjs": "0.18.4",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -51,6 +51,6 @@
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-cornerstone-viewport": "4.0.2"
|
||||
"react-cornerstone-viewport": "4.0.5"
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
@@ -235,8 +235,8 @@ class OHIFCornerstoneViewport extends Component {
|
||||
viewportIndex={viewportIndex}
|
||||
imageIds={imageIds}
|
||||
imageIdIndex={currentImageIdIndex}
|
||||
onNewImage={newImageHandler}
|
||||
onNewImageDebounceTime={700}
|
||||
onNewImageDebounced={newImageHandler}
|
||||
onNewImageDebounceTime={300}
|
||||
// ~~ Connected (From REDUX)
|
||||
// frameRate={frameRate}
|
||||
// isPlaying={false}
|
||||
|
||||
@@ -268,6 +268,7 @@ const commandsModule = ({ servicesManager }) => {
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
activeViewportIndex,
|
||||
refreshViewports = true,
|
||||
}) => {
|
||||
const study = studyMetadataManager.get(StudyInstanceUID);
|
||||
|
||||
@@ -278,6 +279,10 @@ const commandsModule = ({ servicesManager }) => {
|
||||
);
|
||||
});
|
||||
|
||||
if (!displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
displaySet.SOPInstanceUID = SOPInstanceUID;
|
||||
displaySet.frameIndex = frameIndex;
|
||||
|
||||
@@ -285,7 +290,9 @@ const commandsModule = ({ servicesManager }) => {
|
||||
setViewportSpecificData(activeViewportIndex, displaySet)
|
||||
);
|
||||
|
||||
refreshCornerstoneViewports();
|
||||
if (refreshViewports) {
|
||||
refreshCornerstoneViewports();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,14 @@ export default {
|
||||
},
|
||||
getViewportModule({ commandsManager }) {
|
||||
const ExtendedOHIFCornerstoneViewport = props => {
|
||||
/**
|
||||
* TODO: This appears to be used to set the redux parameters for
|
||||
* the viewport when new images are loaded. It's very ugly
|
||||
* and we should remove it.
|
||||
*/
|
||||
const onNewImageHandler = jumpData => {
|
||||
/** Do not trigger all viewports to render unnecessarily */
|
||||
jumpData.refreshViewports = false;
|
||||
commandsManager.runCommand('jumpToImage', jumpData);
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.2.0...@ohif/extension-debugging@0.2.1) (2021-03-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.1.6...@ohif/extension-debugging@0.2.0) (2021-03-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **log:** add new log service ([14d6454](https://github.com/OHIF/Viewers/commit/14d6454eafaa2ccb50e133c2945c9558052ea27e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.1.5...@ohif/extension-debugging@0.1.6) (2020-10-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.1.4...@ohif/extension-debugging@0.1.5) (2020-09-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-debugging
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-debugging",
|
||||
"version": "0.1.5",
|
||||
"version": "0.2.1",
|
||||
"description": "OHIF extension for debugging.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -7,3 +7,20 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.debug-report-modal-buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.debug-report-modal-container .errors {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.debug-report-modal-container .errors-container {
|
||||
border: 1px solid var(--active-color);
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
overflow: scroll;
|
||||
max-height: 300px;
|
||||
}
|
||||
@@ -9,12 +9,22 @@ const DubugReportModal = ({
|
||||
servers,
|
||||
extensionManager,
|
||||
mailTo,
|
||||
debugModalMessage,
|
||||
errors = [],
|
||||
}) => {
|
||||
const mailToFunction = () => {
|
||||
const StudyInstanceUID = Object.keys(studies.studyData)[0];
|
||||
const copyDebugDataToClipboard = () => {
|
||||
const body = getEmailBody();
|
||||
|
||||
const subject = encodeURI(`Issue with Study: ${StudyInstanceUID}`);
|
||||
const textArea = document.createElement('textarea');
|
||||
|
||||
textArea.value = body;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
};
|
||||
|
||||
const getEmailBody = () => {
|
||||
let body = `Enter the description of your problem here: \n\n\n`;
|
||||
|
||||
body += `============= SESSION INFO =============\n\n`;
|
||||
@@ -55,13 +65,11 @@ const DubugReportModal = ({
|
||||
body += `URL\t ${window.location.href}\n\n`;
|
||||
|
||||
// Layout
|
||||
|
||||
const { numRows, numColumns, viewportSpecificData } = viewports;
|
||||
|
||||
body += '== Viewport Layout ==\n';
|
||||
body += `Rows\t${numRows}\n`;
|
||||
body += `Columns\t${numColumns}\n\n`;
|
||||
|
||||
body += '== Viewports ==\n';
|
||||
|
||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||
@@ -75,6 +83,16 @@ const DubugReportModal = ({
|
||||
body += `[${row},${column}]\t${vsd.SeriesInstanceUID}\n`;
|
||||
});
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const mailToFunction = () => {
|
||||
const StudyInstanceUID = Object.keys(studies.studyData)[0];
|
||||
|
||||
const subject = encodeURI(`Issue with Study: ${StudyInstanceUID}`);
|
||||
|
||||
let body = getEmailBody();
|
||||
|
||||
// TODO Text dump of rest of stuff.
|
||||
|
||||
body = encodeURI(body);
|
||||
@@ -84,16 +102,27 @@ const DubugReportModal = ({
|
||||
|
||||
return (
|
||||
<div className="debug-report-modal-container">
|
||||
{mailTo ? (
|
||||
{debugModalMessage ? <p>{debugModalMessage}</p> : null}
|
||||
<div className="debug-report-modal-buttons-container">
|
||||
{mailTo ? (
|
||||
<div>
|
||||
<ToolbarButton
|
||||
label={'Send Bug Report'}
|
||||
onClick={mailToFunction}
|
||||
icon={'envelope-square'}
|
||||
isActive={false}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<ToolbarButton
|
||||
label={'Send Bug Report'}
|
||||
onClick={mailToFunction}
|
||||
icon={'envelope-square'}
|
||||
label={'Copy To Clipboard'}
|
||||
onClick={copyDebugDataToClipboard}
|
||||
icon={'clipboard'}
|
||||
isActive={false}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<table>
|
||||
{getAppVersion()}
|
||||
@@ -103,6 +132,19 @@ const DubugReportModal = ({
|
||||
{getLayout(viewports)}
|
||||
</table>
|
||||
</div>
|
||||
<div className="errors">
|
||||
<h3>Errors ({errors.length})</h3>
|
||||
<div className="errors-container">
|
||||
{errors.map(error => {
|
||||
return (
|
||||
<div>
|
||||
<pre>Message: {error.message}</pre>
|
||||
{error.error && <pre>Stack: {error.error.stack}</pre>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { useLogger } from '@ohif/ui';
|
||||
|
||||
import {
|
||||
save,
|
||||
upload,
|
||||
@@ -95,6 +97,7 @@ export function getCommands(context, servicesManager, extensionManager) {
|
||||
const { UIModalService } = servicesManager.services;
|
||||
|
||||
const WrappedDebugReportModal = function() {
|
||||
const { state: loggerState } = useLogger();
|
||||
return (
|
||||
<DebugReportModal
|
||||
viewports={viewports}
|
||||
@@ -102,6 +105,8 @@ export function getCommands(context, servicesManager, extensionManager) {
|
||||
servers={servers}
|
||||
extensionManager={extensionManager}
|
||||
mailTo={state.mailTo}
|
||||
debugModalMessage={state.debugModalMessage}
|
||||
errors={loggerState.errors}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,8 +36,14 @@ export default {
|
||||
sharedContext.dicomWebClient = dicomWebClient;
|
||||
}
|
||||
|
||||
if (configuration && configuration.mailTo) {
|
||||
state.mailTo = configuration.mailTo;
|
||||
if (configuration) {
|
||||
if (configuration.mailTo) {
|
||||
state.mailTo = configuration.mailTo;
|
||||
}
|
||||
|
||||
if (configuration.debugModalMessage) {
|
||||
state.debugModalMessage = configuration.debugModalMessage;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
const state = { mailTo: undefined };
|
||||
const state = { mailTo: undefined, debugModalMessage: undefined };
|
||||
|
||||
export default state;
|
||||
@@ -3,6 +3,65 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.4...@ohif/extension-dicom-html@1.3.5) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.3...@ohif/extension-dicom-html@1.3.4) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.2...@ohif/extension-dicom-html@1.3.3) (2021-03-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.1...@ohif/extension-dicom-html@1.3.2) (2021-02-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.0...@ohif/extension-dicom-html@1.3.1) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.9...@ohif/extension-dicom-html@1.3.0) (2020-12-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* visualize overlapping segments in cornerstone ([#2185](https://github.com/OHIF/Viewers/issues/2185)) ([29fceac](https://github.com/OHIF/Viewers/commit/29fceacee97d51f1952a0f6b574c66596d32c201))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.8...@ohif/extension-dicom-html@1.2.9) (2020-10-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.2.7...@ohif/extension-dicom-html@1.2.8) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.2.8",
|
||||
"version": "1.3.5",
|
||||
"description": "OHIF extension for rendering structured reports to HTML",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"dcmjs": "0.16.3",
|
||||
"dcmjs": "0.18.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.51.4...@ohif/extension-dicom-microscopy@0.52.0) (2021-03-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **log:** add new log service ([14d6454](https://github.com/OHIF/Viewers/commit/14d6454eafaa2ccb50e133c2945c9558052ea27e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.51.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.51.3...@ohif/extension-dicom-microscopy@0.51.4) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.51.2...@ohif/extension-dicom-microscopy@0.51.3) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-microscopy",
|
||||
"version": "0.51.3",
|
||||
"version": "0.52.0",
|
||||
"description": "OHIF extension for Dicom Microscopy",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -62,11 +62,30 @@ class DicomMicroscopyViewport extends Component {
|
||||
);
|
||||
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
|
||||
|
||||
this.viewer = new microscopyViewer({
|
||||
client: dicomWebClient,
|
||||
metadata,
|
||||
retrieveRendered: false,
|
||||
});
|
||||
try {
|
||||
this.viewer = new microscopyViewer({
|
||||
client: dicomWebClient,
|
||||
metadata,
|
||||
retrieveRendered: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Microscopy Viewer] Failed to load:', error);
|
||||
const {
|
||||
UINotificationService,
|
||||
LoggerService,
|
||||
} = this.props.servicesManager.services;
|
||||
if (UINotificationService) {
|
||||
const message =
|
||||
'Failed to load viewport. Please check that you have hardware acceleration enabled.';
|
||||
LoggerService.error({ error, message });
|
||||
UINotificationService.show({
|
||||
autoClose: false,
|
||||
title: 'Microscopy Viewport',
|
||||
message,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.viewer.render({ container });
|
||||
});
|
||||
@@ -92,8 +111,8 @@ class DicomMicroscopyViewport extends Component {
|
||||
{this.state.error ? (
|
||||
<h2>{JSON.stringify(this.state.error)}</h2>
|
||||
) : (
|
||||
<div style={style} ref={this.container} />
|
||||
)}
|
||||
<div style={style} ref={this.container} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,14 +6,6 @@ const Component = React.lazy(() => {
|
||||
return import('./DicomMicroscopyViewport');
|
||||
});
|
||||
|
||||
const DicomMicroscopyViewport = props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
@@ -21,8 +13,14 @@ export default {
|
||||
id: 'microscopy',
|
||||
version,
|
||||
|
||||
getViewportModule() {
|
||||
return DicomMicroscopyViewport;
|
||||
getViewportModule({ servicesManager }) {
|
||||
return props => {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<Component {...props} servicesManager={servicesManager} />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
},
|
||||
getSopClassHandlerModule() {
|
||||
return DicomMicroscopySopClassHandler;
|
||||
|
||||
@@ -3,6 +3,119 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.6.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.7...@ohif/extension-dicom-rt@0.6.8) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.6...@ohif/extension-dicom-rt@0.6.7) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.5...@ohif/extension-dicom-rt@0.6.6) (2021-03-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.4...@ohif/extension-dicom-rt@0.6.5) (2021-02-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.3...@ohif/extension-dicom-rt@0.6.4) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.2...@ohif/extension-dicom-rt@0.6.3) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.1...@ohif/extension-dicom-rt@0.6.2) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.6.0...@ohif/extension-dicom-rt@0.6.1) (2020-12-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* panel issues and mpr button (IDC2122-IDC2117) ([32022f5](https://github.com/OHIF/Viewers/commit/32022f51f2d24f53f4c98188980db038a6dfe76c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.5.0...@ohif/extension-dicom-rt@0.6.0) (2020-12-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* visualize overlapping segments in cornerstone ([#2185](https://github.com/OHIF/Viewers/issues/2185)) ([29fceac](https://github.com/OHIF/Viewers/commit/29fceacee97d51f1952a0f6b574c66596d32c201))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.9...@ohif/extension-dicom-rt@0.5.0) (2020-12-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update approach to better handle rtdose instead of disabling panel ([f3cecf1](https://github.com/OHIF/Viewers/commit/f3cecf129b16f8d24d239aa44fdff7f46e6ba683))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.8...@ohif/extension-dicom-rt@0.4.9) (2020-10-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.7...@ohif/extension-dicom-rt@0.4.8) (2020-10-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.6...@ohif/extension-dicom-rt@0.4.7) (2020-10-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.4.5...@ohif/extension-dicom-rt@0.4.6) (2020-09-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-rt",
|
||||
"version": "0.4.6",
|
||||
"version": "0.6.8",
|
||||
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -31,7 +31,8 @@
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"dcmjs": "0.16.3",
|
||||
"dcmjs": "0.18.4",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
|
||||
@@ -15,7 +15,7 @@ const OHIFDicomRTStructSopClassHandler = {
|
||||
id: 'OHIFDicomRTStructSopClassHandler',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUIDs,
|
||||
getDisplaySetFromSeries: function(
|
||||
getDisplaySetFromSeries: function (
|
||||
series,
|
||||
study,
|
||||
dicomWebClient,
|
||||
@@ -71,11 +71,11 @@ const OHIFDicomRTStructSopClassHandler = {
|
||||
}
|
||||
}
|
||||
|
||||
rtStructDisplaySet.getSourceDisplaySet = function(studies) {
|
||||
rtStructDisplaySet.getSourceDisplaySet = function (studies) {
|
||||
return getSourceDisplaySet(studies, rtStructDisplaySet);
|
||||
};
|
||||
|
||||
rtStructDisplaySet.load = function(referencedDisplaySet, studies) {
|
||||
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||
return loadRTStruct(
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.icons .angle-double-down, .icons .angle-double-up {
|
||||
.icons .angle-double-down,
|
||||
.icons .angle-double-up {
|
||||
margin-left: 15px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
@@ -9,16 +9,21 @@
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header .cog-icon {
|
||||
align-self: flex-end;
|
||||
color: var(--default-color);
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
min-height: 23px;
|
||||
max-height: 23px;
|
||||
}
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header .cog-icon:hover {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
@@ -31,19 +31,20 @@ const refreshViewport = () => {
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Array} props.studies
|
||||
* @param {Array} props.viewports - viewportSpecificData
|
||||
* @param {Array} props.getActiveViewport - get active viewport data
|
||||
* @param {number} props.activeIndex - activeViewportIndex
|
||||
* @param {number} props.isOpen - isOpen
|
||||
* @returns component
|
||||
*/
|
||||
const RTPanel = ({
|
||||
studies,
|
||||
viewports,
|
||||
activeIndex,
|
||||
isOpen,
|
||||
onContourItemClick,
|
||||
activeContexts = [],
|
||||
contexts = {},
|
||||
activeViewport,
|
||||
getActiveViewport,
|
||||
}) => {
|
||||
const isVTK = () => activeContexts.includes(contexts.VTK);
|
||||
const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE);
|
||||
@@ -54,11 +55,11 @@ const RTPanel = ({
|
||||
referencedDisplaysets: [],
|
||||
sets: [],
|
||||
selectedSet: null,
|
||||
isLocked: false,
|
||||
};
|
||||
|
||||
const [state, setState] = useState(DEFAULT_STATE);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const activeViewport = viewports[activeIndex];
|
||||
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||
@@ -78,6 +79,8 @@ const RTPanel = ({
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
useEffect(() => {
|
||||
updateStructureSets();
|
||||
|
||||
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||
|
||||
return () => {
|
||||
@@ -88,20 +91,30 @@ const RTPanel = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||
Modality: 'RTSTRUCT',
|
||||
});
|
||||
setState(state => ({ ...state, isLocked: !referencedDisplaysets.length }));
|
||||
}, [activeViewport]);
|
||||
|
||||
const updateStructureSets = () => {
|
||||
const viewport = getActiveViewport();
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const StructureSets = module.state.StructureSets;
|
||||
|
||||
if (StructureSets && StructureSets.length) {
|
||||
const viewportSets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||
activeViewport.SeriesInstanceUID
|
||||
viewport.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const studyMetadata = studyMetadataManager.get(viewport.StudyInstanceUID);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||
referencedSeriesInstanceUID: viewport.SeriesInstanceUID,
|
||||
Modality: 'RTSTRUCT',
|
||||
});
|
||||
|
||||
@@ -118,10 +131,6 @@ const RTPanel = ({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateStructureSets();
|
||||
}, [studies, viewports, activeIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowSettings(showSettings && !isOpen);
|
||||
}, [isOpen]);
|
||||
@@ -231,16 +240,19 @@ const RTPanel = ({
|
||||
return (
|
||||
<div className="dcmrt-panel">
|
||||
<div className="dcmrt-panel-header">
|
||||
{' '}
|
||||
{!state.isLocked && (
|
||||
<Icon
|
||||
className="cog-icon"
|
||||
name="cog"
|
||||
width="25px"
|
||||
height="25px"
|
||||
onClick={() => setShowSettings(true)}
|
||||
/>
|
||||
)}
|
||||
<h3>RT Structure Sets</h3>
|
||||
<Icon
|
||||
className="cog-icon"
|
||||
name="cog"
|
||||
width="25px"
|
||||
height="25px"
|
||||
onClick={() => setShowSettings(true)}
|
||||
/>
|
||||
</div>
|
||||
{!state.referencedDisplaysets.length && (
|
||||
{!state.isLocked && !state.referencedDisplaysets.length && (
|
||||
<LoadingIndicator expand height="70px" width="70px" />
|
||||
)}
|
||||
{state.sets &&
|
||||
@@ -249,12 +261,15 @@ const RTPanel = ({
|
||||
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||
viewports[activeIndex].SeriesInstanceUID
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const loadedSet = sets.find(
|
||||
set => set.SeriesInstanceUID === SeriesInstanceUID
|
||||
);
|
||||
|
||||
if (!loadedSet) return null;
|
||||
|
||||
return (
|
||||
<PanelSection
|
||||
key={SeriesInstanceUID}
|
||||
@@ -276,17 +291,17 @@ const RTPanel = ({
|
||||
module.setters.hideStructureSet(loadedSet.SeriesInstanceUID);
|
||||
}
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||
viewports[activeIndex].SeriesInstanceUID
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
setState(state => ({ ...state, sets }));
|
||||
refreshViewport();
|
||||
}}
|
||||
onExpandChange={async () => {
|
||||
if (!isLoaded) {
|
||||
await displaySet.load(viewports[activeIndex], studies);
|
||||
await displaySet.load(activeViewport, studies);
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||
viewports[activeIndex].SeriesInstanceUID
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
const selectedSet = sets.find(
|
||||
set => set.SeriesInstanceUID === SeriesInstanceUID
|
||||
|
||||
@@ -5,6 +5,9 @@ import id from './id.js';
|
||||
import RTPanel from './components/RTPanel/RTPanel';
|
||||
import { version } from '../package.json';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
@@ -45,24 +48,34 @@ export default {
|
||||
icon: 'list',
|
||||
label: 'RTSTRUCT',
|
||||
target: 'rt-panel',
|
||||
isDisabled: studies => {
|
||||
isDisabled: (studies, activeViewport) => {
|
||||
if (!studies) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < studies.length; i++) {
|
||||
const study = studies[i];
|
||||
|
||||
if (study && study.series) {
|
||||
for (let j = 0; j < study.series.length; j++) {
|
||||
const series = study.series[j];
|
||||
if (
|
||||
/* Could be expanded to contain RTPLAN and RTDOSE information in the future */
|
||||
['RTSTRUCT'].includes(series.Modality)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (activeViewport) {
|
||||
const study = studies.find(
|
||||
s => s.StudyInstanceUID === activeViewport.StudyInstanceUID
|
||||
);
|
||||
const ds = study.displaySets.find(
|
||||
ds =>
|
||||
ds.displaySetInstanceUID ===
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const referencedDisplaySets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||
Modality: 'RTSTRUCT',
|
||||
});
|
||||
if (
|
||||
referencedDisplaySets &&
|
||||
referencedDisplaySets.some(ds =>
|
||||
['RTSTRUCT'].includes(ds.Modality)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
|
||||
import TOOL_NAMES from './utils/toolNames';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
|
||||
const globalImageIdSpecificToolStateManager =
|
||||
@@ -86,8 +87,20 @@ export default async function loadRTStruct(
|
||||
ContourGeometricType,
|
||||
} = ContourSequenceArray[c];
|
||||
|
||||
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
||||
const sopInstanceUID = ContourImageSequence
|
||||
? ContourImageSequence.ReferencedSOPInstanceUID
|
||||
: _getClosestSOPInstanceUID(
|
||||
ContourData,
|
||||
ContourGeometricType,
|
||||
NumberOfContourPoints,
|
||||
imageIdSopInstanceUidPairs
|
||||
);
|
||||
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||
|
||||
if (!imageId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
|
||||
toolState,
|
||||
imageId,
|
||||
@@ -286,7 +299,7 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||
);
|
||||
|
||||
return imageIdSopInstanceUidPairsEntry.imageId;
|
||||
return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null;
|
||||
};
|
||||
|
||||
function _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||
@@ -324,3 +337,78 @@ function _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||
function _toArray(objOrArray) {
|
||||
return Array.isArray(objOrArray) ? objOrArray : [objOrArray];
|
||||
}
|
||||
|
||||
function _getClosestSOPInstanceUID(
|
||||
ContourData,
|
||||
ContourGeometricType,
|
||||
NumberOfContourPoints,
|
||||
imageIdSopInstanceUidPairs
|
||||
) {
|
||||
const closest = {
|
||||
distance: Infinity,
|
||||
sopInstanceUID: null,
|
||||
};
|
||||
|
||||
let point;
|
||||
|
||||
switch (ContourGeometricType) {
|
||||
case 'POINT':
|
||||
point = ContourData;
|
||||
break;
|
||||
case 'CLOSED_PLANAR':
|
||||
case 'OPEN_PLANAR':
|
||||
// These are defined as planar, so get the of the region to get the
|
||||
// Best mapping to a plane even if its potentially off center.
|
||||
|
||||
point = [0, 0, 0];
|
||||
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
|
||||
point[0] += ContourData[p];
|
||||
point[1] += ContourData[p + 1];
|
||||
point[2] += ContourData[p + 2];
|
||||
}
|
||||
|
||||
point[0] /= NumberOfContourPoints;
|
||||
point[1] /= NumberOfContourPoints;
|
||||
point[2] /= NumberOfContourPoints;
|
||||
}
|
||||
|
||||
imageIdSopInstanceUidPairs.forEach(pair => {
|
||||
const { imageId } = pair;
|
||||
|
||||
const imagePlaneModule = cornerstone.metaData.get(
|
||||
'imagePlaneModule',
|
||||
imageId
|
||||
);
|
||||
|
||||
const distance = distanceFromPointToPlane(point, imagePlaneModule);
|
||||
|
||||
if (distance < closest.distance) {
|
||||
closest.distance = distance;
|
||||
closest.sopInstanceUID = pair.sopInstanceUID;
|
||||
}
|
||||
});
|
||||
|
||||
return closest.sopInstanceUID;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number[3]} P - The point
|
||||
* @param {object} imagePlaneModule The cornerstone metadata object for the imagePlane
|
||||
*/
|
||||
function distanceFromPointToPlane(P, imagePlaneModule) {
|
||||
const {
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
imagePositionPatient: Q,
|
||||
} = imagePlaneModule;
|
||||
|
||||
let N = [];
|
||||
vec3.cross(N, rowCosines, columnCosines);
|
||||
|
||||
const [A, B, C] = N;
|
||||
|
||||
const D = -A * Q[0] - B * Q[1] - C * Q[2];
|
||||
|
||||
return Math.abs(A * P[0] + B * P[1] + C * P[2] + D); // Denominator is sqrt(A**2 + B**2 + C**2) which is 1 as its a normal vector
|
||||
}
|
||||
@@ -3,6 +3,100 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.6.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.3...@ohif/extension-dicom-segmentation@0.6.4) (2021-04-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.2...@ohif/extension-dicom-segmentation@0.6.3) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.1...@ohif/extension-dicom-segmentation@0.6.2) (2021-03-31)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.6.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.6.0...@ohif/extension-dicom-segmentation@0.6.1) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.5.4...@ohif/extension-dicom-segmentation@0.6.0) (2021-03-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **log:** add new log service ([14d6454](https://github.com/OHIF/Viewers/commit/14d6454eafaa2ccb50e133c2945c9558052ea27e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.5.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.5.3...@ohif/extension-dicom-segmentation@0.5.4) (2021-03-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.5.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.5.2...@ohif/extension-dicom-segmentation@0.5.3) (2021-02-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.5.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.5.1...@ohif/extension-dicom-segmentation@0.5.2) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.5.0...@ohif/extension-dicom-segmentation@0.5.1) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.4.7...@ohif/extension-dicom-segmentation@0.5.0) (2020-12-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* visualize overlapping segments in cornerstone ([#2185](https://github.com/OHIF/Viewers/issues/2185)) ([29fceac](https://github.com/OHIF/Viewers/commit/29fceacee97d51f1952a0f6b574c66596d32c201))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.4.6...@ohif/extension-dicom-segmentation@0.4.7) (2020-10-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.4.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.4.5...@ohif/extension-dicom-segmentation@0.4.6) (2020-09-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-segmentation",
|
||||
"version": "0.4.6",
|
||||
"version": "0.6.4",
|
||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"dcmjs": "0.16.3",
|
||||
"dcmjs": "0.18.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -24,7 +24,7 @@ const SegmentItem = ({
|
||||
onClick,
|
||||
itemClass,
|
||||
color,
|
||||
visible = true,
|
||||
visible,
|
||||
onVisibilityChange,
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
@@ -33,6 +33,15 @@ const SegmentItem = ({
|
||||
setIsVisible(visible);
|
||||
}, [visible]);
|
||||
|
||||
const onClickHandler = () => onClick(index);
|
||||
|
||||
const onVisibilityChangeHandler = event => {
|
||||
event.stopPropagation();
|
||||
const newVisibility = !isVisible;
|
||||
setIsVisible(newVisibility);
|
||||
onVisibilityChange(newVisibility, index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dcmseg-segment-item">
|
||||
<TableListItem
|
||||
@@ -42,7 +51,7 @@ const SegmentItem = ({
|
||||
itemClass={itemClass}
|
||||
itemMeta={<ColoredCircle color={color} />}
|
||||
itemMetaClass="segment-color-section"
|
||||
onItemClick={onClick}
|
||||
onItemClick={onClickHandler}
|
||||
>
|
||||
<div>
|
||||
<div className="segment-label" style={{ marginBottom: 4 }}>
|
||||
@@ -63,12 +72,7 @@ const SegmentItem = ({
|
||||
name={isVisible ? 'eye' : 'eye-closed'}
|
||||
width="20px"
|
||||
height="20px"
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
const newVisibility = !isVisible;
|
||||
setIsVisible(newVisibility);
|
||||
onVisibilityChange(newVisibility);
|
||||
}}
|
||||
onClick={onVisibilityChangeHandler}
|
||||
/>
|
||||
</div>
|
||||
{false && <div className="segment-info">{'...'}</div>}
|
||||
|
||||
+64
-5
@@ -6,6 +6,7 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel.disabled {
|
||||
@@ -14,19 +15,19 @@
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel h3 {
|
||||
padding-left: 20px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .segmentations {
|
||||
padding: 0 20px 20px 20px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .cog-icon {
|
||||
margin: 20px 20px 0 0;
|
||||
align-self: flex-end;
|
||||
color: var(--default-color);
|
||||
cursor: pointer;
|
||||
min-height: 23px;
|
||||
max-height: 23px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .cog-icon:hover {
|
||||
@@ -48,11 +49,11 @@
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .selector-form>div {
|
||||
.dcmseg-segmentation-panel .selector-form > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dcmseg-segmentation-panel .selector-form>div:first-of-type {
|
||||
.dcmseg-segmentation-panel .selector-form > div:first-of-type {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
@@ -64,3 +65,61 @@
|
||||
flex: unset;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.SegmentsSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-primary-color);
|
||||
font-size: 12px;
|
||||
margin-bottom: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.SegmentsSection .header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--ui-gray-dark);
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.SegmentsSection .count {
|
||||
font-size: 15px;
|
||||
color: var(--active-color);
|
||||
margin-left: 15px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--ui-gray-darker);
|
||||
text-align: center;
|
||||
min-width: 25px;
|
||||
}
|
||||
|
||||
.SegmentsSection .header .eye-icon,
|
||||
.SegmentsSection .header .angle-double-down,
|
||||
.SegmentsSection .header .angle-double-up {
|
||||
cursor: pointer;
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.SegmentsSection .header .eye-icon:hover,
|
||||
.SegmentsSection .header .angle-double-down:hover,
|
||||
.SegmentsSection .header .angle-double-up:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.SegmentsSection .header .eye-icon.expanded {
|
||||
color: var(--default-color);
|
||||
}
|
||||
|
||||
.SegmentsSection .header .icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.SegmentsSection .header .icons .angle-double-down,
|
||||
.SegmentsSection .header .icons .angle-double-up {
|
||||
margin-left: 15px;
|
||||
}
|
||||
+467
-352
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import moment from 'moment';
|
||||
import classNames from 'classnames';
|
||||
import { utils, log } from '@ohif/core';
|
||||
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
|
||||
import DICOMSegTempCrosshairsTool from '../../tools/DICOMSegTempCrosshairsTool';
|
||||
@@ -67,29 +68,104 @@ const SegmentationPanel = ({
|
||||
const [state, setState] = useState({
|
||||
brushRadius: DEFAULT_BRUSH_RADIUS,
|
||||
brushColor: 'rgba(221, 85, 85, 1)',
|
||||
selectedSegment: null,
|
||||
selectedSegmentation: null,
|
||||
showSegmentationSettings: false,
|
||||
brushStackState: null,
|
||||
labelmapList: [],
|
||||
selectedSegment: 0,
|
||||
selectedSegmentation: 0,
|
||||
showSettings: false,
|
||||
labelMapList: [],
|
||||
segmentList: [],
|
||||
cachedSegmentsProperties: [],
|
||||
segmentsHidden: [],
|
||||
segmentNumbers: [],
|
||||
isLoading: false,
|
||||
isDisabled: true,
|
||||
});
|
||||
|
||||
const getActiveViewport = () => viewports[activeIndex];
|
||||
|
||||
const getFirstImageId = () => {
|
||||
const { StudyInstanceUID, displaySetInstanceUID } = getActiveViewport();
|
||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||
return studyMetadata.getFirstImageId(displaySetInstanceUID);
|
||||
};
|
||||
|
||||
const getActiveLabelMaps3D = () => {
|
||||
const { labelmaps3D, activeLabelmapIndex } = getBrushStackState();
|
||||
return labelmaps3D[activeLabelmapIndex];
|
||||
};
|
||||
|
||||
const getActiveLabelMapIndex = () => {
|
||||
const { activeLabelmapIndex } = getBrushStackState();
|
||||
return activeLabelmapIndex;
|
||||
};
|
||||
|
||||
const getActiveSegmentIndex = () => {
|
||||
const { activeSegmentIndex } = getActiveLabelMaps3D();
|
||||
return activeSegmentIndex;
|
||||
};
|
||||
|
||||
const getActiveLabelMaps2D = () => {
|
||||
const { labelmaps2D } = getActiveLabelMaps3D();
|
||||
return labelmaps2D;
|
||||
};
|
||||
|
||||
const getCurrentDisplaySet = () => {
|
||||
const { StudyInstanceUID, displaySetInstanceUID } = getActiveViewport();
|
||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||
const allDisplaySets = studyMetadata.getDisplaySets();
|
||||
return allDisplaySets.find(
|
||||
ds => ds.displaySetInstanceUID === displaySetInstanceUID
|
||||
);
|
||||
};
|
||||
|
||||
const setActiveSegment = segmentIndex => {
|
||||
const activeSegmentIndex = getActiveSegmentIndex();
|
||||
const activeViewport = getActiveViewport();
|
||||
|
||||
if (segmentIndex === activeSegmentIndex) {
|
||||
log.info(`${activeSegmentIndex} is already the active segment`);
|
||||
return;
|
||||
}
|
||||
|
||||
const labelmap3D = getActiveLabelMaps3D();
|
||||
labelmap3D.activeSegmentIndex = segmentIndex;
|
||||
|
||||
/**
|
||||
* Activates the correct label map if clicked segment
|
||||
* does not belong to the active labelmap
|
||||
*/
|
||||
const { StudyInstanceUID } = activeViewport;
|
||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||
const allDisplaySets = studyMetadata.getDisplaySets();
|
||||
let selectedSegmentation;
|
||||
let newLabelmapIndex = getActiveLabelMapIndex();
|
||||
allDisplaySets.forEach(displaySet => {
|
||||
if (displaySet.labelmapSegments) {
|
||||
Object.keys(displaySet.labelmapSegments).forEach(labelmapIndex => {
|
||||
if (
|
||||
displaySet.labelmapSegments[labelmapIndex].includes(segmentIndex)
|
||||
) {
|
||||
newLabelmapIndex = labelmapIndex;
|
||||
selectedSegmentation =
|
||||
displaySet.hasOverlapping === true
|
||||
? displaySet.originLabelMapIndex
|
||||
: labelmapIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const brushStackState = getBrushStackState();
|
||||
brushStackState.activeLabelmapIndex = newLabelmapIndex;
|
||||
setState(state => ({ ...state, selectedSegmentation }));
|
||||
|
||||
refreshViewports();
|
||||
|
||||
return segmentIndex;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const labelmapModifiedHandler = event => {
|
||||
log.warn('Segmentation Panel: labelmap modified', event);
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const activeViewport = viewports[activeIndex];
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const firstImageId = studyMetadata.getFirstImageId(
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
updateState('brushStackState', module.state.series[firstImageId]);
|
||||
refreshSegmentations();
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -114,6 +190,11 @@ const SegmentationPanel = ({
|
||||
refreshSegmentations
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
'extensiondicomsegmentationsegloadingfailed',
|
||||
cleanSegmentationComboBox
|
||||
);
|
||||
|
||||
/*
|
||||
* These are specific to each element;
|
||||
* Need to iterate cornerstone-tools tracked enabled elements?
|
||||
@@ -131,6 +212,10 @@ const SegmentationPanel = ({
|
||||
'extensiondicomsegmentationsegloaded',
|
||||
refreshSegmentations
|
||||
);
|
||||
document.removeEventListener(
|
||||
'extensiondicomsegmentationsegloadingfailed',
|
||||
cleanSegmentationComboBox
|
||||
);
|
||||
cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
|
||||
enabledElement.removeEventListener(
|
||||
'cornerstonetoolslabelmapmodified',
|
||||
@@ -140,50 +225,49 @@ const SegmentationPanel = ({
|
||||
};
|
||||
}, [activeIndex, viewports]);
|
||||
|
||||
const refreshSegmentations = useCallback(() => {
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const activeViewport = viewports[activeIndex];
|
||||
const cleanSegmentationComboBox = () => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
segmentsHidden: [],
|
||||
segmentNumbers: [],
|
||||
labelMapList: [],
|
||||
segmentList: [],
|
||||
isDisabled: true,
|
||||
}));
|
||||
}
|
||||
|
||||
const refreshSegmentations = () => {
|
||||
const activeViewport = getActiveViewport();
|
||||
const isDisabled = !activeViewport || !activeViewport.StudyInstanceUID;
|
||||
if (!isDisabled) {
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const firstImageId = studyMetadata.getFirstImageId(
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
const brushStackState = module.state.series[firstImageId];
|
||||
const brushStackState = getBrushStackState();
|
||||
if (brushStackState) {
|
||||
const labelmap3D =
|
||||
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||
const labelmapList = getLabelmapList(
|
||||
brushStackState,
|
||||
firstImageId,
|
||||
activeViewport
|
||||
);
|
||||
const segmentList = getSegmentList(
|
||||
labelmap3D,
|
||||
firstImageId,
|
||||
brushStackState
|
||||
);
|
||||
const labelMapList = getLabelMapList();
|
||||
const {
|
||||
items: segmentList,
|
||||
numbers: segmentNumbers,
|
||||
segmentsHidden,
|
||||
} = getSegmentList();
|
||||
setState(state => ({
|
||||
...state,
|
||||
brushStackState,
|
||||
selectedSegmentation: brushStackState.activeLabelmapIndex,
|
||||
labelmapList,
|
||||
segmentsHidden,
|
||||
segmentNumbers,
|
||||
labelMapList,
|
||||
segmentList,
|
||||
isDisabled,
|
||||
}));
|
||||
} else {
|
||||
setState(state => ({
|
||||
...state,
|
||||
labelmapList: [],
|
||||
segmentsHidden: [],
|
||||
segmentNumbers: [],
|
||||
labelMapList: [],
|
||||
segmentList: [],
|
||||
isDisabled,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [viewports, activeIndex, state.isLoading]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshSegmentations();
|
||||
@@ -200,306 +284,288 @@ const SegmentationPanel = ({
|
||||
useEffect(() => {
|
||||
setState(state => ({
|
||||
...state,
|
||||
showSegmentationSettings: state.showSegmentationSettings && !isOpen,
|
||||
showSettings: state.showSettings && !isOpen,
|
||||
}));
|
||||
}, [isOpen]);
|
||||
|
||||
const getLabelmapList = useCallback(
|
||||
(brushStackState, firstImageId, activeViewport) => {
|
||||
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
||||
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
||||
activeViewport.StudyInstanceUID,
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
const getLabelMapList = () => {
|
||||
const activeViewport = getActiveViewport();
|
||||
|
||||
return referencedSegDisplaysets.map((displaySet, index) => {
|
||||
const { labelmapIndex, SeriesDate, SeriesTime } = displaySet;
|
||||
/* Get list of SEG labelmaps specific to active viewport (reference series) */
|
||||
const referencedSegDisplaysets = _getReferencedSegDisplaysets(
|
||||
activeViewport.StudyInstanceUID,
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
|
||||
/* Map to display representation */
|
||||
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||
const isActiveLabelmap =
|
||||
labelmapIndex === brushStackState.activeLabelmapIndex;
|
||||
const displayDate = date.format('ddd, MMM Do YYYY');
|
||||
const displayTime = date.format('h:mm:ss a');
|
||||
const displayDescription = displaySet.SeriesDescription;
|
||||
const filteredReferencedSegDisplaysets = referencedSegDisplaysets.filter(
|
||||
(segDisplay => segDisplay.loadError !== true && segDisplay.isLoaded));
|
||||
|
||||
return {
|
||||
value: labelmapIndex,
|
||||
title: displayDescription,
|
||||
description: displayDate,
|
||||
onClick: async () => {
|
||||
const activatedLabelmapIndex = await setActiveLabelmap(
|
||||
activeViewport,
|
||||
studies,
|
||||
displaySet,
|
||||
() => onSelectedSegmentationChange(),
|
||||
onDisplaySetLoadFailure
|
||||
);
|
||||
updateState('selectedSegmentation', activatedLabelmapIndex);
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[studies]
|
||||
);
|
||||
return filteredReferencedSegDisplaysets.map((displaySet, index) => {
|
||||
const {
|
||||
labelmapIndex,
|
||||
originLabelMapIndex,
|
||||
hasOverlapping,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
} = displaySet;
|
||||
|
||||
const getSegmentList = useCallback(
|
||||
(labelmap3D, firstImageId, brushStackState) => {
|
||||
/*
|
||||
* Newly created segments have no `meta`
|
||||
* So we instead build a list of all segment indexes in use
|
||||
* Then find any associated metadata
|
||||
*/
|
||||
const uniqueSegmentIndexes = labelmap3D.labelmaps2D
|
||||
.reduce((acc, labelmap2D) => {
|
||||
if (labelmap2D) {
|
||||
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
|
||||
/* Map to display representation */
|
||||
const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0];
|
||||
const date = moment(dateStr, 'YYYYMMDD:HHmmss');
|
||||
const displayDate = date.format('ddd, MMM Do YYYY, h:mm:ss a');
|
||||
const displayDescription = displaySet.SeriesDescription;
|
||||
|
||||
for (let i = 0; i < segmentIndexes.length; i++) {
|
||||
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
|
||||
acc.push(segmentIndexes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const colorLutTable =
|
||||
module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data;
|
||||
|
||||
const segmentList = [];
|
||||
for (let i = 0; i < uniqueSegmentIndexes.length; i++) {
|
||||
const segmentIndex = uniqueSegmentIndexes[i];
|
||||
|
||||
const color = colorLutTable[segmentIndex];
|
||||
let segmentLabel = '(unlabeled)';
|
||||
let segmentNumber = segmentIndex;
|
||||
|
||||
/* Meta */
|
||||
if (hasLabelmapMeta) {
|
||||
const segmentMeta = labelmap3D.metadata.data[segmentIndex];
|
||||
|
||||
if (segmentMeta) {
|
||||
segmentNumber = segmentMeta.SegmentNumber;
|
||||
segmentLabel = segmentMeta.SegmentLabel;
|
||||
}
|
||||
}
|
||||
|
||||
const sameSegment = state.selectedSegment === segmentNumber;
|
||||
const setCurrentSelectedSegment = () => {
|
||||
_setActiveSegment(
|
||||
firstImageId,
|
||||
segmentNumber,
|
||||
labelmap3D.activeSegmentIndex
|
||||
return {
|
||||
value: hasOverlapping === true ? originLabelMapIndex : labelmapIndex,
|
||||
title: displayDescription,
|
||||
description: displayDate,
|
||||
onClick: async () => {
|
||||
const activatedLabelmapIndex = await setActiveLabelmap(
|
||||
activeViewport,
|
||||
studies,
|
||||
displaySet,
|
||||
onSelectedSegmentationChange,
|
||||
onDisplaySetLoadFailure
|
||||
);
|
||||
updateState('selectedSegment', sameSegment ? null : segmentNumber);
|
||||
|
||||
const validIndexList = [];
|
||||
labelmap3D.labelmaps2D.forEach((labelMap2D, index) => {
|
||||
if (labelMap2D.segmentsOnLabelmap.includes(segmentNumber)) {
|
||||
validIndexList.push(index);
|
||||
}
|
||||
});
|
||||
const avg = array => array.reduce((a, b) => a + b) / array.length;
|
||||
const average = avg(validIndexList);
|
||||
const closest = validIndexList.reduce((prev, curr) => {
|
||||
return Math.abs(curr - average) < Math.abs(prev - average)
|
||||
? curr
|
||||
: prev;
|
||||
});
|
||||
|
||||
if (isCornerstone()) {
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
const element = enabledElements[activeIndex].element;
|
||||
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||
|
||||
if (!toolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = toolState.data[0].imageIds;
|
||||
const imageId = imageIds[closest];
|
||||
const frameIndex = imageIds.indexOf(imageId);
|
||||
|
||||
const SOPInstanceUID = cornerstone.metaData.get(
|
||||
'SOPInstanceUID',
|
||||
imageId
|
||||
);
|
||||
const StudyInstanceUID = cornerstone.metaData.get(
|
||||
'StudyInstanceUID',
|
||||
imageId
|
||||
);
|
||||
|
||||
DICOMSegTempCrosshairsTool.addCrosshair(
|
||||
element,
|
||||
imageId,
|
||||
segmentNumber
|
||||
);
|
||||
|
||||
onSegmentItemClick({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
activeViewportIndex: activeIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (isVTK()) {
|
||||
const activeViewport = viewports[activeIndex];
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
activeViewport.StudyInstanceUID
|
||||
);
|
||||
const allDisplaySets = studyMetadata.getDisplaySets();
|
||||
const currentDisplaySet = allDisplaySets.find(
|
||||
displaySet =>
|
||||
displaySet.displaySetInstanceUID ===
|
||||
activeViewport.displaySetInstanceUID
|
||||
);
|
||||
|
||||
const frame = labelmap3D.labelmaps2D[closest];
|
||||
|
||||
onSegmentItemClick({
|
||||
studies,
|
||||
StudyInstanceUID: currentDisplaySet.StudyInstanceUID,
|
||||
displaySetInstanceUID: currentDisplaySet.displaySetInstanceUID,
|
||||
SOPClassUID: viewports[activeIndex].sopClassUIDs[0],
|
||||
SOPInstanceUID: currentDisplaySet.SOPInstanceUID,
|
||||
segmentNumber,
|
||||
frameIndex: closest,
|
||||
frame,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isSegmentVisible = () => {
|
||||
return !labelmap3D.segmentsHidden[segmentIndex];
|
||||
};
|
||||
|
||||
const toggleSegmentVisibility = () => {
|
||||
const segmentsHidden = labelmap3D.segmentsHidden;
|
||||
segmentsHidden[segmentIndex] = !segmentsHidden[segmentIndex];
|
||||
return !segmentsHidden[segmentIndex];
|
||||
};
|
||||
|
||||
const cachedSegmentProperties =
|
||||
state.cachedSegmentsProperties[segmentNumber];
|
||||
let visible = isSegmentVisible();
|
||||
if (
|
||||
cachedSegmentProperties &&
|
||||
cachedSegmentProperties.visible !== visible
|
||||
) {
|
||||
toggleSegmentVisibility();
|
||||
}
|
||||
|
||||
segmentList.push(
|
||||
<SegmentItem
|
||||
key={segmentNumber}
|
||||
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
||||
onClick={() => setCurrentSelectedSegment()}
|
||||
label={segmentLabel}
|
||||
index={segmentNumber}
|
||||
color={color}
|
||||
visible={visible}
|
||||
onVisibilityChange={newVisibility => {
|
||||
if (isCornerstone()) {
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
const element = enabledElements[activeIndex].element;
|
||||
module.setters.toggleSegmentVisibility(
|
||||
element,
|
||||
segmentNumber,
|
||||
brushStackState.activeLabelmapIndex
|
||||
);
|
||||
}
|
||||
|
||||
if (isVTK()) {
|
||||
onSegmentVisibilityChange(segmentNumber, newVisibility);
|
||||
}
|
||||
|
||||
updateCachedSegmentsProperties(segmentNumber, {
|
||||
visible: newVisibility,
|
||||
});
|
||||
refreshViewports();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return segmentList;
|
||||
|
||||
/*
|
||||
* Let's iterate over segmentIndexes ^ above
|
||||
* If meta has a match, use it to show info
|
||||
* If now, add "no-meta" class
|
||||
* Show default name
|
||||
*/
|
||||
},
|
||||
[activeIndex, onSegmentItemClick, state.selectedSegment, state.isLoading]
|
||||
);
|
||||
|
||||
const updateCachedSegmentsProperties = (segmentNumber, properties) => {
|
||||
const segmentsProperties = state.cachedSegmentsProperties;
|
||||
const segmentProperties = state.cachedSegmentsProperties[segmentNumber];
|
||||
|
||||
segmentsProperties[segmentNumber] = segmentProperties
|
||||
? { ...segmentProperties, ...properties }
|
||||
: properties;
|
||||
|
||||
updateState('cachedSegmentsProperties', segmentsProperties);
|
||||
setState(state => ({
|
||||
...state,
|
||||
selectedSegmentation: activatedLabelmapIndex,
|
||||
}));
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateState('cachedSegmentsProperties', []);
|
||||
}, [activeContexts]);
|
||||
const setCurrentSelectedSegment = segmentNumber => {
|
||||
setActiveSegment(segmentNumber);
|
||||
|
||||
const updateState = (field, value) => {
|
||||
setState(state => ({ ...state, [field]: value }));
|
||||
const sameSegment = state.selectedSegment === segmentNumber;
|
||||
if (!sameSegment) {
|
||||
setState(state => ({ ...state, selectedSegment: segmentNumber }));
|
||||
}
|
||||
|
||||
const validIndexList = [];
|
||||
getActiveLabelMaps2D().forEach((labelMap2D, index) => {
|
||||
if (labelMap2D.segmentsOnLabelmap.includes(segmentNumber)) {
|
||||
validIndexList.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
const avg = array => array.reduce((a, b) => a + b) / array.length;
|
||||
const average = avg(validIndexList);
|
||||
const closest = validIndexList.reduce((prev, curr) => {
|
||||
return Math.abs(curr - average) < Math.abs(prev - average) ? curr : prev;
|
||||
});
|
||||
|
||||
if (isCornerstone()) {
|
||||
const element = getEnabledElement();
|
||||
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||
|
||||
if (!toolState) return;
|
||||
|
||||
const imageIds = toolState.data[0].imageIds;
|
||||
const imageId = imageIds[closest];
|
||||
const frameIndex = imageIds.indexOf(imageId);
|
||||
|
||||
const SOPInstanceUID = cornerstone.metaData.get(
|
||||
'SOPInstanceUID',
|
||||
imageId
|
||||
);
|
||||
const StudyInstanceUID = cornerstone.metaData.get(
|
||||
'StudyInstanceUID',
|
||||
imageId
|
||||
);
|
||||
|
||||
DICOMSegTempCrosshairsTool.addCrosshair(element, imageId, segmentNumber);
|
||||
|
||||
onSegmentItemClick({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
activeViewportIndex: activeIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (isVTK()) {
|
||||
const labelMaps3D = getActiveLabelMaps3D();
|
||||
const currentDisplaySet = getCurrentDisplaySet();
|
||||
const frame = labelMaps3D.labelmaps2D[closest];
|
||||
|
||||
onSegmentItemClick({
|
||||
studies,
|
||||
StudyInstanceUID: currentDisplaySet.StudyInstanceUID,
|
||||
displaySetInstanceUID: currentDisplaySet.displaySetInstanceUID,
|
||||
SOPClassUID: getActiveViewport().sopClassUIDs[0],
|
||||
SOPInstanceUID: currentDisplaySet.SOPInstanceUID,
|
||||
segmentNumber,
|
||||
frameIndex: closest,
|
||||
frame,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getColorLUTTable = () => {
|
||||
const { state } = cornerstoneTools.getModule('segmentation');
|
||||
const { colorLUTIndex } = getActiveLabelMaps3D();
|
||||
return state.colorLutTables[colorLUTIndex];
|
||||
};
|
||||
|
||||
const getEnabledElement = () => {
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
return enabledElements[activeIndex].element;
|
||||
};
|
||||
|
||||
const onSegmentVisibilityChangeHandler = (isVisible, segmentNumber) => {
|
||||
/** Get all labelmaps with this segmentNumber (overlapping segments) */
|
||||
const { labelmaps3D } = getBrushStackState();
|
||||
const possibleLabelMaps3D = labelmaps3D.filter(({ labelmaps2D }) => {
|
||||
return labelmaps2D.some(({ segmentsOnLabelmap }) =>
|
||||
segmentsOnLabelmap.includes(segmentNumber)
|
||||
);
|
||||
});
|
||||
|
||||
let segmentsHidden = [];
|
||||
possibleLabelMaps3D.forEach(labelmap3D => {
|
||||
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
|
||||
|
||||
segmentsHidden = [
|
||||
...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]),
|
||||
];
|
||||
});
|
||||
|
||||
setState(state => ({ ...state, segmentsHidden }));
|
||||
|
||||
refreshSegmentations();
|
||||
refreshViewports();
|
||||
|
||||
if (isVTK()) {
|
||||
onSegmentVisibilityChange(segmentNumber, isVisible);
|
||||
}
|
||||
};
|
||||
|
||||
const getSegmentList = () => {
|
||||
/*
|
||||
* Newly created segments have no `meta`
|
||||
* So we instead build a list of all segment indexes in use
|
||||
* Then find any associated metadata
|
||||
*/
|
||||
const uniqueSegmentIndexes = getActiveLabelMaps2D()
|
||||
.reduce((acc, labelmap2D) => {
|
||||
if (labelmap2D) {
|
||||
const segmentIndexes = labelmap2D.segmentsOnLabelmap;
|
||||
|
||||
for (let i = 0; i < segmentIndexes.length; i++) {
|
||||
if (!acc.includes(segmentIndexes[i]) && segmentIndexes[i] !== 0) {
|
||||
acc.push(segmentIndexes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
const labelmap3D = getActiveLabelMaps3D();
|
||||
const colorLutTable = getColorLUTTable();
|
||||
const hasLabelmapMeta = labelmap3D.metadata && labelmap3D.metadata.data;
|
||||
|
||||
const segmentList = [];
|
||||
const segmentNumbers = [];
|
||||
for (let i = 0; i < uniqueSegmentIndexes.length; i++) {
|
||||
const segmentIndex = uniqueSegmentIndexes[i];
|
||||
|
||||
const color = colorLutTable[segmentIndex];
|
||||
let segmentLabel = '(unlabeled)';
|
||||
let segmentNumber = segmentIndex;
|
||||
|
||||
/* Meta */
|
||||
if (hasLabelmapMeta) {
|
||||
const segmentMeta = labelmap3D.metadata.data[segmentIndex];
|
||||
if (segmentMeta) {
|
||||
segmentNumber = segmentMeta.SegmentNumber;
|
||||
segmentLabel = segmentMeta.SegmentLabel;
|
||||
}
|
||||
}
|
||||
|
||||
const sameSegment = state.selectedSegment === segmentNumber;
|
||||
|
||||
segmentNumbers.push(segmentNumber);
|
||||
segmentList.push(
|
||||
<SegmentItem
|
||||
key={segmentNumber}
|
||||
itemClass={`segment-item ${sameSegment && 'selected'}`}
|
||||
onClick={setCurrentSelectedSegment}
|
||||
label={segmentLabel}
|
||||
index={segmentNumber}
|
||||
color={color}
|
||||
visible={!labelmap3D.segmentsHidden[segmentIndex]}
|
||||
onVisibilityChange={onSegmentVisibilityChangeHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
items: segmentList,
|
||||
numbers: segmentNumbers,
|
||||
segmentsHidden: labelmap3D.segmentsHidden,
|
||||
};
|
||||
|
||||
/*
|
||||
* Let's iterate over segmentIndexes ^ above
|
||||
* If meta has a match, use it to show info
|
||||
* If now, add "no-meta" class
|
||||
* Show default name
|
||||
*/
|
||||
};
|
||||
|
||||
const updateBrushSize = evt => {
|
||||
const updatedRadius = Number(evt.target.value);
|
||||
|
||||
if (updatedRadius !== brushRadius) {
|
||||
updateState('brushRadius', updatedRadius);
|
||||
setState(state => ({ ...state, brushRadius: updatedRadius }));
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
module.setters.radius(updatedRadius);
|
||||
}
|
||||
};
|
||||
|
||||
const decrementSegment = event => {
|
||||
const activeSegmentIndex = getActiveSegmentIndex();
|
||||
event.preventDefault();
|
||||
if (labelmap3D.activeSegmentIndex > 1) {
|
||||
labelmap3D.activeSegmentIndex--;
|
||||
if (activeSegmentIndex > 1) {
|
||||
activeSegmentIndex--;
|
||||
}
|
||||
updateState('selectedSegment', labelmap3D.activeSegmentIndex);
|
||||
setState(state => ({ ...state, selectedSegment: activeSegmentIndex }));
|
||||
updateActiveSegmentColor();
|
||||
};
|
||||
|
||||
const incrementSegment = event => {
|
||||
const activeSegmentIndex = getActiveSegmentIndex();
|
||||
event.preventDefault();
|
||||
labelmap3D.activeSegmentIndex++;
|
||||
updateState('selectedSegment', labelmap3D.activeSegmentIndex);
|
||||
activeSegmentIndex++;
|
||||
setState(state => ({ ...state, selectedSegment: activeSegmentIndex }));
|
||||
updateActiveSegmentColor();
|
||||
};
|
||||
|
||||
const updateActiveSegmentColor = () => {
|
||||
const color = getActiveSegmentColor();
|
||||
updateState('brushColor', color);
|
||||
setState(state => ({ ...state, brushColor: color }));
|
||||
};
|
||||
|
||||
const getBrushStackState = () => {
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const firstImageId = getFirstImageId();
|
||||
const brushStackState = module.state.series[firstImageId];
|
||||
return brushStackState;
|
||||
};
|
||||
|
||||
const getActiveSegmentColor = () => {
|
||||
if (!state.brushStackState) {
|
||||
const brushStackState = getBrushStackState();
|
||||
if (!brushStackState) {
|
||||
return 'rgba(255, 255, 255, 1)';
|
||||
}
|
||||
|
||||
const module = cornerstoneTools.getModule('segmentation');
|
||||
const colorLutTable = module.state.colorLutTables[labelmap3D.colorLUTIndex];
|
||||
const colorLutTable = getColorLUTTable();
|
||||
const color = colorLutTable[labelmap3D.activeSegmentIndex];
|
||||
|
||||
return `rgba(${color.join(',')})`;
|
||||
};
|
||||
|
||||
@@ -517,65 +583,103 @@ const SegmentationPanel = ({
|
||||
refreshViewports();
|
||||
};
|
||||
|
||||
const onVisibilityChangeHandler = isVisible => {
|
||||
let segmentsHidden = [];
|
||||
state.segmentNumbers.forEach(segmentNumber => {
|
||||
if (isVTK()) {
|
||||
onSegmentVisibilityChange(segmentNumber, isVisible);
|
||||
}
|
||||
|
||||
/** Get all labelmaps with this segmentNumber (overlapping segments) */
|
||||
const { labelmaps3D } = getBrushStackState();
|
||||
const possibleLabelMaps3D = labelmaps3D.filter(({ labelmaps2D }) => {
|
||||
return labelmaps2D.some(({ segmentsOnLabelmap }) =>
|
||||
segmentsOnLabelmap.includes(segmentNumber)
|
||||
);
|
||||
});
|
||||
|
||||
possibleLabelMaps3D.forEach(labelmap3D => {
|
||||
labelmap3D.segmentsHidden[segmentNumber] = !isVisible;
|
||||
segmentsHidden = [
|
||||
...new Set([...segmentsHidden, ...labelmap3D.segmentsHidden]),
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
setState(state => ({ ...state, segmentsHidden }));
|
||||
|
||||
refreshSegmentations();
|
||||
refreshViewports();
|
||||
};
|
||||
|
||||
const disabledConfigurationFields = [
|
||||
'outlineAlpha',
|
||||
'shouldRenderInactiveLabelmaps',
|
||||
];
|
||||
if (state.showSegmentationSettings) {
|
||||
|
||||
const selectedSegmentationOption = state.labelMapList.find(
|
||||
i => i.value === state.selectedSegmentation
|
||||
);
|
||||
|
||||
if (state.showSettings) {
|
||||
return (
|
||||
<SegmentationSettings
|
||||
disabledFields={isVTK() ? disabledConfigurationFields : []}
|
||||
configuration={configuration}
|
||||
onBack={() => updateState('showSegmentationSettings', false)}
|
||||
onBack={() => setState(state => ({ ...state, showSettings: false }))}
|
||||
onChange={updateConfiguration}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className={`dcmseg-segmentation-panel ${state.isDisabled &&
|
||||
'disabled'}`}
|
||||
className={classNames('dcmseg-segmentation-panel', {
|
||||
disabled: state.isDisabled,
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
className="cog-icon"
|
||||
name="cog"
|
||||
width="25px"
|
||||
height="25px"
|
||||
onClick={() => updateState('showSegmentationSettings', true)}
|
||||
/>
|
||||
{false && (
|
||||
<form className="selector-form">
|
||||
<BrushColorSelector
|
||||
defaultColor={brushColor}
|
||||
defaultColor={state.brushColor}
|
||||
index={state.selectedSegment}
|
||||
onNext={incrementSegment}
|
||||
onPrev={decrementSegment}
|
||||
/>
|
||||
<BrushRadius
|
||||
value={brushRadius}
|
||||
value={state.brushRadius}
|
||||
onChange={updateBrushSize}
|
||||
min={configuration.minRadius}
|
||||
max={configuration.maxRadius}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
<Icon
|
||||
className="cog-icon"
|
||||
name="cog"
|
||||
width="25px"
|
||||
height="25px"
|
||||
onClick={() => setState(state => ({ ...state, showSettings: true }))}
|
||||
/>
|
||||
<h3>Segmentations</h3>
|
||||
<div className="segmentations">
|
||||
<SegmentationSelect
|
||||
value={state.labelmapList.find(
|
||||
i => i.value === state.selectedSegmentation
|
||||
)}
|
||||
value={selectedSegmentationOption}
|
||||
formatOptionLabel={SegmentationItem}
|
||||
options={state.labelmapList}
|
||||
options={state.labelMapList}
|
||||
/>
|
||||
</div>
|
||||
<ScrollableArea>
|
||||
<TableList
|
||||
customHeader={<SegmentsHeader count={state.segmentList.length} />}
|
||||
>
|
||||
{state.segmentList}
|
||||
</TableList>
|
||||
</ScrollableArea>
|
||||
<SegmentsSection
|
||||
count={state.segmentList.length}
|
||||
isVisible={
|
||||
state.segmentsHidden.filter(isHidden => isHidden === true).length <
|
||||
state.segmentNumbers.length
|
||||
}
|
||||
onVisibilityChange={onVisibilityChangeHandler}
|
||||
>
|
||||
<ScrollableArea>
|
||||
<TableList headless>{state.segmentList}</TableList>
|
||||
</ScrollableArea>
|
||||
</SegmentsSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -610,7 +714,7 @@ SegmentationPanel.propTypes = {
|
||||
SegmentationPanel.defaultProps = {};
|
||||
|
||||
/**
|
||||
* Returns SEG Displaysets that reference the target series, sorted by dateTime
|
||||
* Returns SEG DisplaySets that reference the target series, sorted by dateTime
|
||||
*
|
||||
* @param {string} StudyInstanceUID
|
||||
* @param {string} SeriesInstanceUID
|
||||
@@ -628,43 +732,54 @@ const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => {
|
||||
referencedDisplaysets.sort((a, b) => {
|
||||
const aNumber = Number(`${a.SeriesDate}${a.SeriesTime}`);
|
||||
const bNumber = Number(`${b.SeriesDate}${b.SeriesTime}`);
|
||||
return aNumber - bNumber;
|
||||
return bNumber - aNumber;
|
||||
});
|
||||
|
||||
return referencedDisplaysets;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} firstImageId
|
||||
* @param {*} activeSegmentIndex
|
||||
* @returns
|
||||
*/
|
||||
const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex) => {
|
||||
if (segmentIndex === activeSegmentIndex) {
|
||||
log.info(`${activeSegmentIndex} is already the active segment`);
|
||||
return;
|
||||
}
|
||||
const SegmentsSection = ({
|
||||
count,
|
||||
children,
|
||||
isVisible: defaultVisibility,
|
||||
onVisibilityChange,
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(defaultVisibility);
|
||||
|
||||
const { state } = cornerstoneTools.getModule('segmentation');
|
||||
const brushStackState = state.series[firstImageId];
|
||||
const onVisibilityChangeHandler = () => {
|
||||
const newVisibility = !isVisible;
|
||||
setIsVisible(newVisibility);
|
||||
onVisibilityChange(newVisibility);
|
||||
};
|
||||
|
||||
const labelmap3D =
|
||||
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
|
||||
labelmap3D.activeSegmentIndex = segmentIndex;
|
||||
useEffect(() => {
|
||||
setIsVisible(defaultVisibility);
|
||||
}, [defaultVisibility]);
|
||||
|
||||
refreshViewports();
|
||||
|
||||
return segmentIndex;
|
||||
};
|
||||
|
||||
const SegmentsHeader = ({ count }) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="tableListHeaderTitle">Segments</div>
|
||||
<div className="numberOfItems">{count}</div>
|
||||
</React.Fragment>
|
||||
<div className="SegmentsSection">
|
||||
<div className="header">
|
||||
<div>Segments</div>
|
||||
<div className="icons">
|
||||
<Icon
|
||||
className={`eye-icon ${isVisible && 'expanded'}`}
|
||||
name={isVisible ? 'eye' : 'eye-closed'}
|
||||
width="20px"
|
||||
height="20px"
|
||||
onClick={onVisibilityChangeHandler}
|
||||
/>
|
||||
<div className="count">{count}</div>
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
SegmentsSection.defaultProps = {
|
||||
onVisibilityChange: noop,
|
||||
};
|
||||
|
||||
export default SegmentationPanel;
|
||||
@@ -1,6 +1,11 @@
|
||||
import { MODULE_TYPES, utils } from '@ohif/core';
|
||||
import loadSegmentation from './loadSegmentation';
|
||||
import getSourceDisplaySet from './getSourceDisplaySet';
|
||||
import OHIF from '@ohif/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
|
||||
const { DicomLoaderService } = OHIF.utils;
|
||||
const { DicomMessage, DicomMetaDictionary } = dcmjs.data;
|
||||
|
||||
// TODO: Should probably use dcmjs for this
|
||||
const SOP_CLASS_UIDS = {
|
||||
@@ -46,11 +51,11 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
StudyInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
authorizationHeaders,
|
||||
metadata,
|
||||
isDerived: true,
|
||||
referencedDisplaySetUID: null, // Assigned when loaded.
|
||||
labelmapIndex: null, // Assigned when loaded.
|
||||
isLoaded: false,
|
||||
hasOverlapping: false,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesNumber,
|
||||
@@ -58,15 +63,109 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
||||
metadata,
|
||||
};
|
||||
|
||||
segDisplaySet.getSourceDisplaySet = function(studies) {
|
||||
return getSourceDisplaySet(studies, segDisplaySet);
|
||||
segDisplaySet.getSourceDisplaySet = function(studies, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
|
||||
return getSourceDisplaySet(studies, segDisplaySet, activateLabelMap, onDisplaySetLoadFailureHandler);
|
||||
};
|
||||
|
||||
segDisplaySet.load = function(referencedDisplaySet, studies) {
|
||||
return loadSegmentation(segDisplaySet, referencedDisplaySet, studies);
|
||||
segDisplaySet.load = async function(referencedDisplaySet, studies) {
|
||||
segDisplaySet.isLoading = true;
|
||||
segDisplaySet.isLoaded = true;
|
||||
const { StudyInstanceUID } = referencedDisplaySet;
|
||||
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||
segDisplaySet,
|
||||
studies
|
||||
);
|
||||
const dicomData = DicomMessage.readFile(segArrayBuffer);
|
||||
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
||||
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
const imageIds = _getImageIdsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
referencedDisplaySet.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const results = await _parseSeg(segArrayBuffer, imageIds);
|
||||
if (results === undefined) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
labelmapBufferArray,
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
segmentsOnFrameArray,
|
||||
} = results;
|
||||
let labelmapIndex;
|
||||
if (labelmapBufferArray.length > 1) {
|
||||
let labelmapIndexes = [];
|
||||
for (let i = 0; i < labelmapBufferArray.length; ++i) {
|
||||
labelmapIndexes.push(
|
||||
await loadSegmentation(
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
labelmapBufferArray[i],
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
segmentsOnFrameArray[i]
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Since overlapping segments have virtual labelmaps,
|
||||
* originLabelMapIndex is used in the panel to select the correct dropdown value.
|
||||
*/
|
||||
segDisplaySet.hasOverlapping = true;
|
||||
segDisplaySet.originLabelMapIndex = labelmapIndexes[0];
|
||||
labelmapIndex = labelmapIndexes[0];
|
||||
console.warn('Overlapping segments!');
|
||||
} else {
|
||||
labelmapIndex = await loadSegmentation(
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
labelmapBufferArray[0],
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
[]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
segDisplaySet.isLoading = false;
|
||||
return segDisplaySet;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function _parseSeg(arrayBuffer, imageIds) {
|
||||
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
cornerstone.metaData
|
||||
);
|
||||
}
|
||||
|
||||
function _getImageIdsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const displaySets = study.displaySets.filter(displaySet => {
|
||||
return displaySet.SeriesInstanceUID === SeriesInstanceUID;
|
||||
});
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||
);
|
||||
// TODO -> We could make check the instance list and see if any match?
|
||||
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
|
||||
// ^ Will that even happen?
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySets[0];
|
||||
|
||||
return referencedDisplaySet.images.map(image => image.getImageId());
|
||||
}
|
||||
@@ -3,10 +3,12 @@ import { classes } from '@ohif/core';
|
||||
|
||||
const { ImageSet } = classes;
|
||||
|
||||
export default function getSourceDisplaySet(studies, segDisplaySet) {
|
||||
export default function getSourceDisplaySet(studies, segDisplaySet, activateLabelMap = true, onDisplaySetLoadFailureHandler) {
|
||||
const referencedDisplaySet = _getReferencedDisplaySet(segDisplaySet, studies);
|
||||
|
||||
setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet);
|
||||
if (activateLabelMap) {
|
||||
setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet, undefined, onDisplaySetLoadFailureHandler);
|
||||
}
|
||||
|
||||
return referencedDisplaySet;
|
||||
}
|
||||
|
||||
@@ -25,12 +25,13 @@ export default {
|
||||
return toolbarModule;
|
||||
},
|
||||
getPanelModule({ commandsManager, api, servicesManager }) {
|
||||
const { UINotificationService } = servicesManager.services;
|
||||
const { UINotificationService, LoggerService } = servicesManager.services;
|
||||
|
||||
const ExtendedSegmentationPanel = props => {
|
||||
const { activeContexts } = api.hooks.useAppContext();
|
||||
|
||||
const onDisplaySetLoadFailureHandler = error => {
|
||||
LoggerService.error({ error, message: error.message });
|
||||
UINotificationService.show({
|
||||
title: 'DICOM Segmentation Loader',
|
||||
message: error.message,
|
||||
|
||||
@@ -1,92 +1,68 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import dcmjs from 'dcmjs';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
const { DicomLoaderService } = OHIF.utils;
|
||||
|
||||
export default async function loadSegmentation(
|
||||
imageIds,
|
||||
segDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies
|
||||
labelmapBuffer,
|
||||
segMetadata,
|
||||
segmentsOnFrame,
|
||||
labelmapSegments
|
||||
) {
|
||||
const { StudyInstanceUID } = referencedDisplaySet;
|
||||
const { setters } = cornerstoneTools.getModule('segmentation');
|
||||
|
||||
// Set here is loading is asynchronous.
|
||||
// If this function throws its set back to false.
|
||||
segDisplaySet.isLoaded = true;
|
||||
// TODO: Could define a color LUT based on colors in the SEG.
|
||||
const labelmapIndex = _getNextLabelmapIndex(imageIds[0]);
|
||||
const colorLUTIndex = _makeColorLUTAndGetIndex(segMetadata);
|
||||
|
||||
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||
segDisplaySet,
|
||||
studies
|
||||
setters.labelmap3DByFirstImageId(
|
||||
imageIds[0],
|
||||
labelmapBuffer,
|
||||
labelmapIndex,
|
||||
segMetadata,
|
||||
imageIds.length,
|
||||
segmentsOnFrame,
|
||||
colorLUTIndex
|
||||
);
|
||||
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
if (!segDisplaySet.labelmapSegments) {
|
||||
segDisplaySet.labelmapSegments = {};
|
||||
}
|
||||
|
||||
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
/**
|
||||
* Cache each labelmap segments.
|
||||
* This data is used to determine the active label map when a given segment is activated/clicked.
|
||||
*/
|
||||
segDisplaySet.labelmapSegments[labelmapIndex] = labelmapSegments.length
|
||||
? Array.from(
|
||||
new Set(labelmapSegments.filter(a => !!a).reduce((a, b) => a.concat(b)))
|
||||
)
|
||||
: [];
|
||||
segDisplaySet.labelmapIndex = labelmapIndex;
|
||||
|
||||
const imageIds = _getImageIdsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
referencedDisplaySet.SeriesInstanceUID
|
||||
);
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app that depends on segs to be loaded.
|
||||
*
|
||||
* Currently we are using a non-ideal implementation through a custom event to notify the segmentation panel
|
||||
* or other components that could rely on loaded segmentations that
|
||||
* the segments were loaded so that e.g. when the user opens the panel
|
||||
* before the segments are fully loaded, the panel can subscribe to this custom event
|
||||
* and update itself with the new segments.
|
||||
*
|
||||
* This limitation is due to the fact that the cs segmentation module is an object (which will be
|
||||
* updated after the segments are loaded) that React its not aware of its changes
|
||||
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||
*
|
||||
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||
* allows us to easily watch the module or the segmentations loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
console.log('Segmentation loaded.');
|
||||
const event = new CustomEvent('extensiondicomsegmentationsegloaded');
|
||||
document.dispatchEvent(event);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let results;
|
||||
|
||||
try {
|
||||
results = _parseSeg(segArrayBuffer, imageIds);
|
||||
} catch (error) {
|
||||
segDisplaySet.isLoaded = false;
|
||||
segDisplaySet.loadError = true;
|
||||
reject(error);
|
||||
}
|
||||
|
||||
const { labelmapBuffer, segMetadata, segmentsOnFrame } = results;
|
||||
const { setters } = cornerstoneTools.getModule('segmentation');
|
||||
|
||||
// TODO: Could define a color LUT based on colors in the SEG.
|
||||
const labelmapIndex = _getNextLabelmapIndex(imageIds[0]);
|
||||
const colorLUTIndex = _makeColorLUTAndGetIndex(segMetadata);
|
||||
|
||||
setters.labelmap3DByFirstImageId(
|
||||
imageIds[0],
|
||||
labelmapBuffer,
|
||||
labelmapIndex,
|
||||
segMetadata,
|
||||
imageIds.length,
|
||||
segmentsOnFrame,
|
||||
colorLUTIndex
|
||||
);
|
||||
|
||||
segDisplaySet.labelmapIndex = labelmapIndex;
|
||||
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app that depends on segs to be loaded.
|
||||
*
|
||||
* Currently we are using a non-ideal implementation through a custom event to notify the segmentation panel
|
||||
* or other components that could rely on loaded segmentations that
|
||||
* the segments were loaded so that e.g. when the user opens the panel
|
||||
* before the segments are fully loaded, the panel can subscribe to this custom event
|
||||
* and update itself with the new segments.
|
||||
*
|
||||
* This limitation is due to the fact that the cs segmentation module is an object (which will be
|
||||
* updated after the segments are loaded) that React its not aware of its changes
|
||||
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||
*
|
||||
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||
* allows us to easily watch the module or the segmentations loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
console.log('Segmentation loaded.');
|
||||
const event = new CustomEvent('extensiondicomsegmentationsegloaded');
|
||||
document.dispatchEvent(event);
|
||||
|
||||
resolve(labelmapIndex);
|
||||
});
|
||||
return labelmapIndex;
|
||||
}
|
||||
|
||||
function _getNextLabelmapIndex(firstImageId) {
|
||||
@@ -172,38 +148,3 @@ function _getNextColorLUTIndex() {
|
||||
|
||||
return colorLUTIndex;
|
||||
}
|
||||
|
||||
function _parseSeg(arrayBuffer, imageIds) {
|
||||
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
||||
imageIds,
|
||||
arrayBuffer,
|
||||
cornerstone.metaData
|
||||
);
|
||||
}
|
||||
|
||||
function _getImageIdsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const displaySets = study.displaySets.filter(displaySet => {
|
||||
return displaySet.SeriesInstanceUID === SeriesInstanceUID;
|
||||
});
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||
);
|
||||
// TODO -> We could make check the instance list and see if any match?
|
||||
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
|
||||
// ^ Will that even happen?
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySets[0];
|
||||
|
||||
return referencedDisplaySet.images.map(image => image.getImageId());
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export default async function setActiveLabelmap(
|
||||
displaySet,
|
||||
callback = () => {},
|
||||
onDisplaySetLoadFailure = err => {
|
||||
throw new Error(err.message);
|
||||
console.error(err.message);
|
||||
}
|
||||
) {
|
||||
const studyMetadata = studyMetadataManager.get(
|
||||
@@ -37,31 +37,48 @@ export default async function setActiveLabelmap(
|
||||
? brushStackState.activeLabelmapIndex
|
||||
: undefined;
|
||||
|
||||
if (displaySet.labelmapIndex === activeLabelmapIndex) {
|
||||
let labelmapIndex =
|
||||
displaySet.hasOverlapping === true
|
||||
? displaySet.originLabelMapIndex
|
||||
: displaySet.labelmapIndex;
|
||||
|
||||
if (labelmapIndex === activeLabelmapIndex) {
|
||||
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
|
||||
return displaySet.labelmapIndex;
|
||||
return labelmapIndex;
|
||||
}
|
||||
|
||||
if (displaySet.isLoading) {
|
||||
return activeLabelmapIndex;
|
||||
}
|
||||
|
||||
if (!displaySet.isLoaded) {
|
||||
const loadPromise = displaySet.load(referencedDisplaySet, studies);
|
||||
|
||||
loadPromise.catch(error => {
|
||||
try {
|
||||
await displaySet.load(referencedDisplaySet, studies);
|
||||
} catch (error) {
|
||||
displaySet.isLoaded = false;
|
||||
displaySet.isLoading = false;
|
||||
displaySet.loadError = true;
|
||||
onDisplaySetLoadFailure(error);
|
||||
|
||||
// Return old index.
|
||||
return activeLabelmapIndex;
|
||||
});
|
||||
const event = new CustomEvent('extensiondicomsegmentationsegloadingfailed');
|
||||
document.dispatchEvent(event);
|
||||
|
||||
await loadPromise;
|
||||
return activeLabelmapIndex;
|
||||
}
|
||||
}
|
||||
|
||||
labelmapIndex =
|
||||
displaySet.hasOverlapping === true
|
||||
? displaySet.originLabelMapIndex
|
||||
: displaySet.labelmapIndex;
|
||||
|
||||
// This might have just been created, so need to use the non-cached value.
|
||||
state = cornerstoneTools.getModule('segmentation').state;
|
||||
brushStackState = state.series[firstImageId];
|
||||
brushStackState.activeLabelmapIndex = displaySet.labelmapIndex;
|
||||
brushStackState.activeLabelmapIndex = labelmapIndex;
|
||||
|
||||
refreshViewports();
|
||||
callback();
|
||||
|
||||
return displaySet.labelmapIndex;
|
||||
return labelmapIndex;
|
||||
}
|
||||
@@ -3,6 +3,103 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.5...@ohif/extension-dicom-tag-browser@0.2.6) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.4...@ohif/extension-dicom-tag-browser@0.2.5) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.3...@ohif/extension-dicom-tag-browser@0.2.4) (2021-03-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.2...@ohif/extension-dicom-tag-browser@0.2.3) (2021-02-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.1...@ohif/extension-dicom-tag-browser@0.2.2) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.0...@ohif/extension-dicom-tag-browser@0.2.1) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.1.1...@ohif/extension-dicom-tag-browser@0.2.0) (2020-12-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* visualize overlapping segments in cornerstone ([#2185](https://github.com/OHIF/Viewers/issues/2185)) ([29fceac](https://github.com/OHIF/Viewers/commit/29fceacee97d51f1952a0f6b574c66596d32c201))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.1.0...@ohif/extension-dicom-tag-browser@0.1.1) (2020-12-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fallback to original dataset VR for XS ([f57725a](https://github.com/OHIF/Viewers/commit/f57725ac8bfd59ea46ab334a1823882afadefba1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.0.5...@ohif/extension-dicom-tag-browser@0.1.0) (2020-12-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add show scroll option to modal ([be494a7](https://github.com/OHIF/Viewers/commit/be494a7376d15777dbe598289c4ecdb9f48a6a48))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.0.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.0.4...@ohif/extension-dicom-tag-browser@0.0.5) (2020-12-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.0.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.0.3...@ohif/extension-dicom-tag-browser@0.0.4) (2020-10-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.0.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.0.2...@ohif/extension-dicom-tag-browser@0.0.3) (2020-09-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-tag-browser",
|
||||
"version": "0.0.3",
|
||||
"version": "0.2.6",
|
||||
"description": "OHIF extension for checking DICOM headers.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^2.6.0",
|
||||
"dcmjs": "0.16.0",
|
||||
"dcmjs": "0.18.4",
|
||||
"react": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.dicom-tag-browser-table-wrapper {
|
||||
height: 500px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.dicom-tag-browser-table tr {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
@@ -11,6 +16,20 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dicom-tag-browser-content {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.dicom-tag-browser-instance-range .range {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dicom-tag-browser-instance-range {
|
||||
padding: 20px 0 20px 0;
|
||||
}
|
||||
|
||||
.dicom-tag-browser-table td.dicom-tag-browser-table-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { classes } from '@ohif/core';
|
||||
import { classes, cornerstone as OHIFCornerstone } from '@ohif/core';
|
||||
import { Range } from '@ohif/ui';
|
||||
import dcmjs from 'dcmjs';
|
||||
import DicomBrowserSelect from './DicomBrowserSelect';
|
||||
import moment from 'moment';
|
||||
@@ -10,6 +11,8 @@ const { ImageSet } = classes;
|
||||
const { DicomMetaDictionary } = dcmjs.data;
|
||||
const { nameMap } = DicomMetaDictionary;
|
||||
|
||||
const { metadataProvider } = OHIFCornerstone;
|
||||
|
||||
const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
const [
|
||||
activeDisplaySetInstanceUID,
|
||||
@@ -17,6 +20,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
] = useState(displaySetInstanceUID);
|
||||
const [activeInstance, setActiveInstance] = useState(0);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [meta, setMeta] = useState('');
|
||||
const [instanceList, setInstanceList] = useState([]);
|
||||
const [displaySetList, setDisplaySetList] = useState([]);
|
||||
const [isImageStack, setIsImageStack] = useState(false);
|
||||
@@ -83,6 +87,7 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
}
|
||||
|
||||
setTags(getSortedTags(metadata));
|
||||
setMeta(metadata);
|
||||
setInstanceList(instanceList);
|
||||
setDisplaySetList(newDisplaySetList);
|
||||
setIsImageStack(isImageStack);
|
||||
@@ -95,47 +100,56 @@ const DicomTagBrowser = ({ displaySets, displaySetInstanceUID }) => {
|
||||
let instanceSelectList = null;
|
||||
|
||||
if (isImageStack) {
|
||||
const selectedInstanceValue = instanceList[activeInstance];
|
||||
|
||||
instanceSelectList = (
|
||||
<DicomBrowserSelect
|
||||
value={selectedInstanceValue}
|
||||
formatOptionLabel={DicomBrowserSelectItem}
|
||||
options={instanceList}
|
||||
/>
|
||||
<div className="dicom-tag-browser-instance-range">
|
||||
<Range
|
||||
showValue
|
||||
step={1}
|
||||
min={1}
|
||||
max={instanceList.length - 1}
|
||||
value={activeInstance}
|
||||
valueRenderer={value => <p>Instance Number: {value}</p>}
|
||||
onChange={({ target }) => {
|
||||
const instanceIndex = parseInt(target.value);
|
||||
setActiveInstance(instanceIndex);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="dicom-tag-browser-content">
|
||||
<DicomBrowserSelect
|
||||
value={selectedDisplaySetValue}
|
||||
formatOptionLabel={DicomBrowserSelectItem}
|
||||
options={displaySetList}
|
||||
/>
|
||||
{instanceSelectList}
|
||||
<DicomTagTable tags={tags}></DicomTagTable>
|
||||
<div className="dicom-tag-browser-table-wrapper">
|
||||
<DicomTagTable tags={tags} meta={meta}></DicomTagTable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function DicomTagTable({ tags }) {
|
||||
const rows = getFormattedRowsFromTags(tags);
|
||||
function DicomTagTable({ tags, meta }) {
|
||||
const rows = getFormattedRowsFromTags(tags, meta);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="dicom-tag-browser-table">
|
||||
<table className="dicom-tag-browser-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="dicom-tag-browser-table-left">Tag</th>
|
||||
<th className="dicom-tag-browser-table-left">Value Representation</th>
|
||||
<th className="dicom-tag-browser-table-left">Keyword</th>
|
||||
<th className="dicom-tag-browser-table-left">Value</th>
|
||||
</tr>
|
||||
{rows.map(row => {
|
||||
{rows.map((row, index) => {
|
||||
const className = row.className ? row.className : null;
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<tr className={className} key={`DICOMTagRow-${index}`}>
|
||||
<td>{row[0]}</td>
|
||||
<td className="dicom-tag-browser-table-center">{row[1]}</td>
|
||||
<td>{row[2]}</td>
|
||||
@@ -143,12 +157,12 @@ function DicomTagTable({ tags }) {
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</table>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function getFormattedRowsFromTags(tags) {
|
||||
function getFormattedRowsFromTags(tags, meta) {
|
||||
const rows = [];
|
||||
|
||||
tags.forEach(tagInfo => {
|
||||
@@ -175,6 +189,21 @@ function getFormattedRowsFromTags(tags) {
|
||||
rows.push(...formatedRowsFromTags);
|
||||
});
|
||||
} else {
|
||||
if (tagInfo.vr === 'xs') {
|
||||
try {
|
||||
const dataset = metadataProvider.getStudyDataset(
|
||||
meta.StudyInstanceUID
|
||||
);
|
||||
const tag = dcmjs.data.Tag.fromPString(tagInfo.tag).toCleanString();
|
||||
const originalTagInfo = dataset[tag];
|
||||
tagInfo.vr = originalTagInfo.vr;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to parse value representation for tag '${tagInfo.keyword}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
rows.push([
|
||||
`${tagInfo.tagIndent}${tagInfo.tag}`,
|
||||
tagInfo.vr,
|
||||
@@ -247,10 +276,11 @@ function getRows(metadata, depth = 0) {
|
||||
sequenceAsArray.forEach(item => {
|
||||
const sequenceRows = getRows(item, depth + 1);
|
||||
|
||||
// Sort the sequence group.
|
||||
_sortTagList(sequenceRows);
|
||||
|
||||
sequence.values.push(sequenceRows);
|
||||
if (sequenceRows.length) {
|
||||
// Sort the sequence group.
|
||||
_sortTagList(sequenceRows);
|
||||
sequence.values.push(sequenceRows);
|
||||
}
|
||||
});
|
||||
|
||||
continue;
|
||||
@@ -276,13 +306,12 @@ function getRows(metadata, depth = 0) {
|
||||
} else if (value.Alphabetic) {
|
||||
value = value.Alphabetic;
|
||||
} else {
|
||||
console.error('Unrecognised Value for element:');
|
||||
console.error(value);
|
||||
console.warn(`Unrecognised Value: ${value} for ${keyword}:`);
|
||||
console.warn(value);
|
||||
value = ' ';
|
||||
}
|
||||
} else {
|
||||
console.error('Unrecognised Value for element:');
|
||||
console.error(value);
|
||||
console.warn(`Unrecognised Value: ${value} for ${keyword}:`);
|
||||
value = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function getCommandsModule(servicesManager) {
|
||||
content: WrappedDicomTagBrowser,
|
||||
title: `DICOM Tag Browser`,
|
||||
fullscreen: true,
|
||||
noScroll: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,6 +3,256 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.5...@ohif/extension-vtk@1.11.6) (2021-04-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.4...@ohif/extension-vtk@1.11.5) (2021-04-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.3...@ohif/extension-vtk@1.11.4) (2021-03-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.2...@ohif/extension-vtk@1.11.3) (2021-03-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.1...@ohif/extension-vtk@1.11.2) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.11.0...@ohif/extension-vtk@1.11.1) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.11.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.10.6...@ohif/extension-vtk@1.11.0) (2021-03-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **log:** add new log service ([14d6454](https://github.com/OHIF/Viewers/commit/14d6454eafaa2ccb50e133c2945c9558052ea27e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.10.5...@ohif/extension-vtk@1.10.6) (2021-03-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.10.4...@ohif/extension-vtk@1.10.5) (2021-02-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.10.3...@ohif/extension-vtk@1.10.4) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.10.2...@ohif/extension-vtk@1.10.3) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.10.1...@ohif/extension-vtk@1.10.2) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.10.0...@ohif/extension-vtk@1.10.1) (2020-12-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* panel issues and mpr button (IDC2122-IDC2117) ([32022f5](https://github.com/OHIF/Viewers/commit/32022f51f2d24f53f4c98188980db038a6dfe76c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.9.4...@ohif/extension-vtk@1.10.0) (2020-12-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* visualize overlapping segments in cornerstone ([#2185](https://github.com/OHIF/Viewers/issues/2185)) ([29fceac](https://github.com/OHIF/Viewers/commit/29fceacee97d51f1952a0f6b574c66596d32c201))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.9.3...@ohif/extension-vtk@1.9.4) (2020-12-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reset VOI on mpr reset ([#2115](https://github.com/OHIF/Viewers/issues/2115)) ([0baf18d](https://github.com/OHIF/Viewers/commit/0baf18dcb2985378016236b735329346a851e14f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.9.2...@ohif/extension-vtk@1.9.3) (2020-12-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.9.1...@ohif/extension-vtk@1.9.2) (2020-12-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.9.0...@ohif/extension-vtk@1.9.1) (2020-12-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.8.0...@ohif/extension-vtk@1.9.0) (2020-12-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add error boundary and retry logic for network failures during dynamic imports ([#2145](https://github.com/OHIF/Viewers/issues/2145)) ([4c07904](https://github.com/OHIF/Viewers/commit/4c079044f6ae2381c6054d8d77414100152d1d19))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.8.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.16...@ohif/extension-vtk@1.8.0) (2020-11-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** Added de (German) translations ([2fff2d5](https://github.com/OHIF/Viewers/commit/2fff2d511fa1d36bdd6476da4ffa5684bf55374f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.15...@ohif/extension-vtk@1.7.16) (2020-11-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.14...@ohif/extension-vtk@1.7.15) (2020-11-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.13...@ohif/extension-vtk@1.7.14) (2020-10-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.12...@ohif/extension-vtk@1.7.13) (2020-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.11...@ohif/extension-vtk@1.7.12) (2020-10-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.10...@ohif/extension-vtk@1.7.11) (2020-10-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.9...@ohif/extension-vtk@1.7.10) (2020-10-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.8...@ohif/extension-vtk@1.7.9) (2020-10-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.7...@ohif/extension-vtk@1.7.8) (2020-09-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.7.6...@ohif/extension-vtk@1.7.7) (2020-09-17)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "1.7.7",
|
||||
"version": "1.11.6",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -35,7 +35,7 @@
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dcmjs": "0.16.3",
|
||||
"dcmjs": "0.18.4",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
@@ -50,11 +50,11 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-vtkjs-viewport": "^0.10.4"
|
||||
"react-vtkjs-viewport": "^0.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^2.10.5",
|
||||
"@ohif/ui": "^1.5.5",
|
||||
"@ohif/core": "^2.13.5",
|
||||
"@ohif/ui": "^1.9.1",
|
||||
"cornerstone-tools": "^4.20.1",
|
||||
"cornerstone-wado-image-loader": "^3.1.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
|
||||
@@ -138,6 +138,8 @@ class OHIFVTKViewport extends Component {
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) => {
|
||||
const { UINotificationService } = this.props.servicesManager.services;
|
||||
|
||||
const stack = OHIFVTKViewport.getCornerstoneStack(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
@@ -159,6 +161,18 @@ class OHIFVTKViewport extends Component {
|
||||
const { activeLabelmapIndex } = brushStackState;
|
||||
const labelmap3D = brushStackState.labelmaps3D[activeLabelmapIndex];
|
||||
|
||||
if (
|
||||
brushStackState.labelmaps3D.length > 1 &&
|
||||
this.props.viewportIndex === 0
|
||||
) {
|
||||
UINotificationService.show({
|
||||
title: 'Overlapping Segmentation Found',
|
||||
message:
|
||||
'Overlapping segmentations cannot be displayed when in MPR mode',
|
||||
type: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
this.segmentsDefaultProperties = labelmap3D.segmentsHidden.map(
|
||||
isHidden => {
|
||||
return { visible: !isHidden };
|
||||
@@ -296,52 +310,82 @@ class OHIFVTKViewport extends Component {
|
||||
seriesDescription: displaySet.seriesDescription,
|
||||
};
|
||||
|
||||
const {
|
||||
imageDataObject,
|
||||
labelmapDataObject,
|
||||
labelmapColorLUT,
|
||||
} = this.getViewportData(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
try {
|
||||
const {
|
||||
imageDataObject,
|
||||
labelmapDataObject,
|
||||
labelmapColorLUT,
|
||||
} = this.getViewportData(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
this.imageDataObject = imageDataObject;
|
||||
this.imageDataObject = imageDataObject;
|
||||
|
||||
/* TODO: Not currently used until we have drawing tools in vtkjs.
|
||||
if (!labelmap) {
|
||||
labelmap = createLabelMapImageData(data);
|
||||
} */
|
||||
/* TODO: Not currently used until we have drawing tools in vtkjs.
|
||||
if (!labelmap) {
|
||||
labelmap = createLabelMapImageData(data);
|
||||
} */
|
||||
|
||||
const volumeActor = this.getOrCreateVolume(
|
||||
imageDataObject,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
const volumeActor = this.getOrCreateVolume(
|
||||
imageDataObject,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
percentComplete: 0,
|
||||
dataDetails,
|
||||
},
|
||||
() => {
|
||||
this.loadProgressively(imageDataObject);
|
||||
this.setState(
|
||||
{
|
||||
percentComplete: 0,
|
||||
dataDetails,
|
||||
},
|
||||
() => {
|
||||
this.loadProgressively(imageDataObject);
|
||||
|
||||
// TODO: There must be a better way to do this.
|
||||
// We do this so that if all the data is available the react-vtkjs-viewport
|
||||
// Will render _something_ before the volumes are set and the volume
|
||||
// Construction that happens in react-vtkjs-viewport locks up the CPU.
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
volumes: [volumeActor],
|
||||
paintFilterLabelMapImageData: labelmapDataObject,
|
||||
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
|
||||
labelmapColorLUT,
|
||||
});
|
||||
}, 200);
|
||||
// TODO: There must be a better way to do this.
|
||||
// We do this so that if all the data is available the react-vtkjs-viewport
|
||||
// Will render _something_ before the volumes are set and the volume
|
||||
// Construction that happens in react-vtkjs-viewport locks up the CPU.
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
volumes: [volumeActor],
|
||||
paintFilterLabelMapImageData: labelmapDataObject,
|
||||
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
|
||||
labelmapColorLUT,
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
const errorTitle = 'Failed to load 2D MPR';
|
||||
console.error(errorTitle, error);
|
||||
const {
|
||||
UINotificationService,
|
||||
LoggerService,
|
||||
} = this.props.servicesManager.services;
|
||||
if (this.props.viewportIndex === 0) {
|
||||
const message = error.message.includes('buffer')
|
||||
? 'Dataset is too big to display in MPR'
|
||||
: error.message;
|
||||
LoggerService.error({ error, message });
|
||||
UINotificationService.show({
|
||||
title: errorTitle,
|
||||
message,
|
||||
type: 'error',
|
||||
autoClose: false,
|
||||
action: {
|
||||
label: 'Exit 2D MPR',
|
||||
onClick: ({ close }) => {
|
||||
// context: 'ACTIVE_VIEWPORT::VTK',
|
||||
close();
|
||||
this.props.commandsManager.runCommand('setCornerstoneLayout');
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
this.setState({ isLoaded: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -387,11 +431,15 @@ class OHIFVTKViewport extends Component {
|
||||
};
|
||||
|
||||
const onPixelDataInsertedErrorCallback = error => {
|
||||
const { UINotificationService } = this.props.servicesManager.services;
|
||||
const {
|
||||
UINotificationService,
|
||||
LoggerService,
|
||||
} = this.props.servicesManager.services;
|
||||
|
||||
if (!this.hasError) {
|
||||
if (this.props.viewportIndex === 0) {
|
||||
// Only show the notification from one viewport 1 in MPR2D.
|
||||
LoggerService.error({ error, message: error.message });
|
||||
UINotificationService.show({
|
||||
title: 'MPR Load Error',
|
||||
message: error.message,
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* We use this component to leverage "Code Splitting"
|
||||
*
|
||||
* Link: https://serverless-stack.com/chapters/code-splitting-in-create-react-app.html
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export default function asyncComponent(importComponent) {
|
||||
class AsyncComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
component: null,
|
||||
};
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// Add dynamically loaded component to state
|
||||
const { default: component } = await importComponent();
|
||||
|
||||
this.setState({
|
||||
component: component,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const C = this.state.component;
|
||||
|
||||
// Render the loaded component, or null
|
||||
return C ? <C {...this.props} /> : null;
|
||||
}
|
||||
}
|
||||
|
||||
return AsyncComponent;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import throttle from 'lodash.throttle';
|
||||
import {
|
||||
vtkInteractorStyleMPRCrosshairs,
|
||||
vtkInteractorStyleMPRWindowLevel,
|
||||
vtkInteractorStyleRotatableMPRCrosshairs,
|
||||
vtkSVGRotatableCrosshairsWidget,
|
||||
vtkInteractorStyleMPRRotate,
|
||||
vtkSVGCrosshairsWidget,
|
||||
} from 'react-vtkjs-viewport';
|
||||
import { getImageData } from 'react-vtkjs-viewport';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
@@ -11,13 +11,15 @@ import setMPRLayout from './utils/setMPRLayout.js';
|
||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
||||
import OHIFVTKViewport from './OHIFVTKViewport';
|
||||
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
|
||||
|
||||
const { BlendMode } = Constants;
|
||||
|
||||
const commandsModule = ({ commandsManager }) => {
|
||||
const commandsModule = ({ commandsManager, servicesManager }) => {
|
||||
const { UINotificationService, LoggerService } = servicesManager.services;
|
||||
|
||||
// TODO: Put this somewhere else
|
||||
let apis = {};
|
||||
let defaultVOI;
|
||||
|
||||
async function _getActiveViewportVTKApi(viewports) {
|
||||
const {
|
||||
@@ -119,6 +121,16 @@ const commandsModule = ({ commandsManager }) => {
|
||||
getVtkApis: ({ index }) => {
|
||||
return apis[index];
|
||||
},
|
||||
resetMPRView() {
|
||||
// Reset orientation
|
||||
apis.forEach(api => api.resetOrientation());
|
||||
|
||||
// Reset VOI
|
||||
if (defaultVOI) setVOI(defaultVOI);
|
||||
|
||||
// Reset the crosshairs
|
||||
apis[0].svgWidgets.rotatableCrosshairsWidget.resetCrosshairs(apis, 0);
|
||||
},
|
||||
axial: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
@@ -165,7 +177,7 @@ const commandsModule = ({ commandsManager }) => {
|
||||
segmentNumber,
|
||||
frameIndex,
|
||||
frame,
|
||||
done = () => { }
|
||||
done = () => {},
|
||||
}) => {
|
||||
let api = apis[viewports.activeViewportIndex];
|
||||
|
||||
@@ -180,10 +192,13 @@ const commandsModule = ({ commandsManager }) => {
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
|
||||
const imageDataObject = getImageData(
|
||||
stack.imageIds,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
let pixelIndex = 0;
|
||||
let x = 0;
|
||||
@@ -209,9 +224,12 @@ const commandsModule = ({ commandsManager }) => {
|
||||
y /= count;
|
||||
|
||||
const position = [x, y, frameIndex];
|
||||
const worldPos = _convertModelToWorldSpace(position, imageDataObject.vtkImageData);
|
||||
const worldPos = _convertModelToWorldSpace(
|
||||
position,
|
||||
imageDataObject.vtkImageData
|
||||
);
|
||||
|
||||
api.svgWidgets.crosshairsWidget.moveCrosshairs(worldPos, apis);
|
||||
api.svgWidgets.rotatableCrosshairsWidget.moveCrosshairs(worldPos, apis);
|
||||
done();
|
||||
},
|
||||
setSegmentationConfiguration: async ({
|
||||
@@ -254,21 +272,38 @@ const commandsModule = ({ commandsManager }) => {
|
||||
await Promise.all(promises);
|
||||
},
|
||||
enableRotateTool: () => {
|
||||
apis.forEach(api => {
|
||||
apis.forEach((api, apiIndex) => {
|
||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle });
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex, uid: api.uid },
|
||||
});
|
||||
});
|
||||
},
|
||||
enableCrosshairsTool: () => {
|
||||
apis.forEach((api, apiIndex) => {
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
const istyle = vtkInteractorStyleRotatableMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
configuration: {
|
||||
apis,
|
||||
apiIndex,
|
||||
uid: api.uid,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const rotatableCrosshairsWidget =
|
||||
apis[0].svgWidgets.rotatableCrosshairsWidget;
|
||||
|
||||
const referenceLines = rotatableCrosshairsWidget.getReferenceLines();
|
||||
|
||||
// Initilise crosshairs if not initialised.
|
||||
if (!referenceLines) {
|
||||
rotatableCrosshairsWidget.resetCrosshairs(apis, 0);
|
||||
}
|
||||
},
|
||||
enableLevelTool: () => {
|
||||
function updateVOI(apis, windowWidth, windowCenter) {
|
||||
@@ -291,10 +326,14 @@ const commandsModule = ({ commandsManager }) => {
|
||||
},
|
||||
};
|
||||
|
||||
apis.forEach(api => {
|
||||
apis.forEach((api, apiIndex) => {
|
||||
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle, callbacks });
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
callbacks,
|
||||
configuration: { apis, apiIndex, uid: api.uid },
|
||||
});
|
||||
});
|
||||
},
|
||||
setSlabThickness: ({ slabThickness }) => {
|
||||
@@ -353,6 +392,7 @@ const commandsModule = ({ commandsManager }) => {
|
||||
|
||||
// Get current VOI if cornerstone viewport.
|
||||
const cornerstoneVOI = getVOIFromCornerstoneViewport();
|
||||
defaultVOI = cornerstoneVOI;
|
||||
|
||||
const viewportProps = [
|
||||
{
|
||||
@@ -391,18 +431,61 @@ const commandsModule = ({ commandsManager }) => {
|
||||
// Add widgets and set default interactorStyle of each viewport.
|
||||
apis.forEach((api, apiIndex) => {
|
||||
api.addSVGWidget(
|
||||
vtkSVGCrosshairsWidget.newInstance(),
|
||||
'crosshairsWidget'
|
||||
vtkSVGRotatableCrosshairsWidget.newInstance(),
|
||||
'rotatableCrosshairsWidget'
|
||||
);
|
||||
|
||||
const uid = api.uid;
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
const istyle = vtkInteractorStyleRotatableMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex, uid },
|
||||
});
|
||||
|
||||
api.svgWidgets.rotatableCrosshairsWidget.setApiIndex(apiIndex);
|
||||
api.svgWidgets.rotatableCrosshairsWidget.setApis(apis);
|
||||
});
|
||||
|
||||
const firstApi = apis[0];
|
||||
|
||||
// Initialise crosshairs
|
||||
apis[0].svgWidgets.rotatableCrosshairsWidget.resetCrosshairs(apis, 0);
|
||||
|
||||
// Check if we have full WebGL 2 support
|
||||
const openGLRenderWindow = apis[0].genericRenderWindow.getOpenGLRenderWindow();
|
||||
|
||||
if (!openGLRenderWindow.getWebgl2()) {
|
||||
// Throw a warning if we don't have WebGL 2 support,
|
||||
// And the volume is too big to fit in a 2D texture
|
||||
|
||||
const openGLContext = openGLRenderWindow.getContext();
|
||||
const maxTextureSizeInBytes = openGLContext.getParameter(
|
||||
openGLContext.MAX_TEXTURE_SIZE
|
||||
);
|
||||
|
||||
const maxBufferLengthFloat32 =
|
||||
(maxTextureSizeInBytes * maxTextureSizeInBytes) / 4;
|
||||
|
||||
const dimensions = firstApi.volumes[0]
|
||||
.getMapper()
|
||||
.getInputData()
|
||||
.getDimensions();
|
||||
|
||||
const volumeLength = dimensions[0] * dimensions[1] * dimensions[2];
|
||||
|
||||
if (volumeLength > maxBufferLengthFloat32) {
|
||||
const message =
|
||||
'This volume is too large to fit in WebGL 1 textures and will display incorrectly. Please use a different browser to view this data';
|
||||
LoggerService.error({ message });
|
||||
UINotificationService.show({
|
||||
title: 'Browser does not support WebGL 2',
|
||||
message,
|
||||
type: 'error',
|
||||
autoClose: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -456,6 +539,10 @@ const commandsModule = ({ commandsManager }) => {
|
||||
commandFn: actions.enableLevelTool,
|
||||
options: {},
|
||||
},
|
||||
resetMPRView: {
|
||||
commandFn: actions.resetMPRView,
|
||||
options: {},
|
||||
},
|
||||
setBlendModeToComposite: {
|
||||
commandFn: actions.setBlendModeToComposite,
|
||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import { asyncComponent, retryImport } from '@ohif/ui';
|
||||
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import withCommandsManager from './withCommandsManager.js';
|
||||
@@ -8,7 +9,9 @@ import { version } from '../package.json';
|
||||
// import loadLocales from './loadLocales';
|
||||
|
||||
const OHIFVTKViewport = asyncComponent(() =>
|
||||
import(/* webpackChunkName: "OHIFVTKViewport" */ './OHIFVTKViewport.js')
|
||||
retryImport(() =>
|
||||
import(/* webpackChunkName: "OHIFVTKViewport" */ './OHIFVTKViewport.js')
|
||||
)
|
||||
);
|
||||
|
||||
const vtkExtension = {
|
||||
@@ -20,15 +23,19 @@ const vtkExtension = {
|
||||
|
||||
getViewportModule({ commandsManager, servicesManager }) {
|
||||
const ExtendedVTKViewport = props => (
|
||||
<OHIFVTKViewport {...props} servicesManager={servicesManager} />
|
||||
<OHIFVTKViewport
|
||||
{...props}
|
||||
servicesManager={servicesManager}
|
||||
commandsManager={commandsManager}
|
||||
/>
|
||||
);
|
||||
return withCommandsManager(ExtendedVTKViewport, commandsManager);
|
||||
},
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
},
|
||||
getCommandsModule({ commandsManager }) {
|
||||
return commandsModule({ commandsManager });
|
||||
getCommandsModule({ commandsManager, servicesManager }) {
|
||||
return commandsModule({ commandsManager, servicesManager });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"Rotate": "Drehen"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import Buttons from './Buttons.json';
|
||||
|
||||
export default {
|
||||
de: {
|
||||
Buttons,
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import en from './en';
|
||||
import es from './es';
|
||||
import de from './de';
|
||||
|
||||
export default {
|
||||
...en,
|
||||
...es
|
||||
...es,
|
||||
...de,
|
||||
};
|
||||
@@ -25,6 +25,16 @@ const definitions = [
|
||||
commandName: 'enableLevelTool',
|
||||
commandOptions: {},
|
||||
},
|
||||
{
|
||||
id: 'Reset',
|
||||
label: 'Reset',
|
||||
icon: 'reset',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'resetMPRView',
|
||||
commandOptions: {},
|
||||
},
|
||||
/*
|
||||
{
|
||||
id: 'Rotate',
|
||||
label: 'Rotate',
|
||||
@@ -34,6 +44,7 @@ const definitions = [
|
||||
commandName: 'enableRotateTool',
|
||||
commandOptions: {},
|
||||
},
|
||||
*/
|
||||
/*
|
||||
{
|
||||
id: 'setBlendModeToComposite',
|
||||
@@ -131,4 +142,4 @@ const definitions = [
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
}
|
||||
};
|
||||
@@ -28,6 +28,7 @@
|
||||
"test:unit": "jest --collectCoverage",
|
||||
"test:unit:ci": "lerna run test:unit:ci --parallel --stream",
|
||||
"test:e2e": "lerna run test:e2e --stream",
|
||||
"test:e2e:script-tag": "lerna run test:e2e:script-tag --stream",
|
||||
"test:e2e:ci": "lerna run test:e2e:ci --stream",
|
||||
"test:e2e:dist": "lerna run test:e2e:dist --stream",
|
||||
"test:e2e:serve": "lerna run test:e2e:serve --stream",
|
||||
|
||||
@@ -3,6 +3,155 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.13.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.4...@ohif/core@2.13.5) (2021-04-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.3...@ohif/core@2.13.4) (2021-04-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **wl:** fix metadata provider window level issues after dicomweb update (string vs number) ([8681036](https://github.com/OHIF/Viewers/commit/8681036b2133cfc325658691a256a86b5e36705f))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.2...@ohif/core@2.13.3) (2021-03-26)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.1...@ohif/core@2.13.2) (2021-03-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* IDC-2288: Update toolbar tabs after derived displayset loaded ([#2307](https://github.com/OHIF/Viewers/issues/2307)) ([6e723eb](https://github.com/OHIF/Viewers/commit/6e723ebced58852007ca553792c740976cfc4201))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.13.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.13.0...@ohif/core@2.13.1) (2021-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.13.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.12.5...@ohif/core@2.13.0) (2021-03-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **log:** add new log service ([14d6454](https://github.com/OHIF/Viewers/commit/14d6454eafaa2ccb50e133c2945c9558052ea27e))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.12.4...@ohif/core@2.12.5) (2021-03-03)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.12.3...@ohif/core@2.12.4) (2021-02-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.12.2...@ohif/core@2.12.3) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.12.1...@ohif/core@2.12.2) (2021-02-05)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.12.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.12.0...@ohif/core@2.12.1) (2021-01-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.12.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.11.1...@ohif/core@2.12.0) (2020-12-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* visualize overlapping segments in cornerstone ([#2185](https://github.com/OHIF/Viewers/issues/2185)) ([29fceac](https://github.com/OHIF/Viewers/commit/29fceacee97d51f1952a0f6b574c66596d32c201))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.11.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.11.0...@ohif/core@2.11.1) (2020-12-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fallback to original dataset VR for XS ([f57725a](https://github.com/OHIF/Viewers/commit/f57725ac8bfd59ea46ab334a1823882afadefba1))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.11.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.10.7...@ohif/core@2.11.0) (2020-12-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add show scroll option to modal ([be494a7](https://github.com/OHIF/Viewers/commit/be494a7376d15777dbe598289c4ecdb9f48a6a48))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.10.6...@ohif/core@2.10.7) (2020-10-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.10.5...@ohif/core@2.10.6) (2020-09-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use SeriesMetadata method instead of property in findMostRecentStructuredReport ([#2038](https://github.com/OHIF/Viewers/issues/2038)) ([91364db](https://github.com/OHIF/Viewers/commit/91364db8820efc86665db258d0044c16fb3e2c87)), closes [#1714](https://github.com/OHIF/Viewers/issues/1714)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.10.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.10.4...@ohif/core@2.10.5) (2020-09-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "2.10.5",
|
||||
"version": "2.13.5",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -39,7 +39,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "0.16.3",
|
||||
"dcmjs": "0.18.4",
|
||||
"dicomweb-client": "^0.6.0",
|
||||
"immer": "6.0.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
|
||||
@@ -13,7 +13,7 @@ const findMostRecentStructuredReport = studies => {
|
||||
// Skip series that may not have instances yet
|
||||
// This can happen if we have retrieved just the initial
|
||||
// details about the series via QIDO-RS, but not the full metadata
|
||||
if (!series.instances || !series.instances.length) {
|
||||
if (!series || series.getInstanceCount() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,10 +63,15 @@ export class HotkeysManager {
|
||||
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
} catch (error) {
|
||||
const { UINotificationService } = this._servicesManager.services;
|
||||
const {
|
||||
UINotificationService,
|
||||
LoggerService,
|
||||
} = this._servicesManager.services;
|
||||
const message = 'Erro while setting hotkeys';
|
||||
LoggerService.error({ error, message });
|
||||
UINotificationService.show({
|
||||
title: 'Hotkeys Manager',
|
||||
message: 'Erro while setting hotkeys',
|
||||
message,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import dicomParser from 'dicom-parser';
|
||||
import getPixelSpacingInformation from '../utils/metadataProvider/getPixelSpacingInformation';
|
||||
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
|
||||
import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData';
|
||||
import validNumber from '../utils/metadataProvider/validNumber';
|
||||
|
||||
class MetadataProvider {
|
||||
constructor() {
|
||||
@@ -20,6 +21,7 @@ class MetadataProvider {
|
||||
writable: false,
|
||||
value: new Map(),
|
||||
});
|
||||
this.datasets = {};
|
||||
}
|
||||
|
||||
async addInstance(dicomJSONDatasetOrP10ArrayBuffer, options = {}) {
|
||||
@@ -52,6 +54,7 @@ class MetadataProvider {
|
||||
SOPInstanceUID,
|
||||
} = naturalizedDataset;
|
||||
|
||||
this._getAndCacheStudyDataset(StudyInstanceUID, dicomJSONDataset);
|
||||
const study = this._getAndCacheStudy(StudyInstanceUID);
|
||||
const series = this._getAndCacheSeriesFromStudy(study, SeriesInstanceUID);
|
||||
const instance = this._getAndCacheInstanceFromStudy(series, SOPInstanceUID);
|
||||
@@ -71,6 +74,16 @@ class MetadataProvider {
|
||||
this.imageIdToUIDs.set(imageId, uids);
|
||||
}
|
||||
|
||||
_getAndCacheStudyDataset(StudyInstanceUID, dataset) {
|
||||
if (!this.datasets[StudyInstanceUID]) {
|
||||
this.datasets[StudyInstanceUID] = dataset;
|
||||
}
|
||||
}
|
||||
|
||||
getStudyDataset(StudyInstanceUID) {
|
||||
return this.datasets[StudyInstanceUID];
|
||||
}
|
||||
|
||||
_getAndCacheStudy(StudyInstanceUID) {
|
||||
const studies = this.studies;
|
||||
|
||||
@@ -83,6 +96,7 @@ class MetadataProvider {
|
||||
|
||||
return study;
|
||||
}
|
||||
|
||||
_getAndCacheSeriesFromStudy(study, SeriesInstanceUID) {
|
||||
let series = study.series.get(SeriesInstanceUID);
|
||||
|
||||
@@ -271,7 +285,7 @@ class MetadataProvider {
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
|
||||
const { WindowCenter, WindowWidth } = instance;
|
||||
let { WindowCenter, WindowWidth } = instance;
|
||||
|
||||
const windowCenter = Array.isArray(WindowCenter)
|
||||
? WindowCenter
|
||||
@@ -281,15 +295,17 @@ class MetadataProvider {
|
||||
: [WindowWidth];
|
||||
|
||||
metadata = {
|
||||
windowCenter,
|
||||
windowWidth,
|
||||
windowCenter: validNumber(windowCenter),
|
||||
windowWidth: validNumber(windowWidth),
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
|
||||
const rescaleSlope = validNumber(instance.RescaleSlope);
|
||||
const rescaleIntercept = validNumber(instance.RescaleIntercept);
|
||||
metadata = {
|
||||
rescaleIntercept: instance.RescaleIntercept,
|
||||
rescaleSlope: instance.RescaleSlope,
|
||||
rescaleIntercept,
|
||||
rescaleSlope,
|
||||
rescaleType: instance.RescaleType,
|
||||
};
|
||||
break;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SeriesMetadata } from './SeriesMetadata';
|
||||
import { api } from 'dicomweb-client';
|
||||
// - createStacks
|
||||
import { isImage } from '../../utils/isImage';
|
||||
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
|
||||
import { isDisplaySetReconstructable, isSpacingUniform } from '../../utils/isDisplaySetReconstructable';
|
||||
import errorHandler from '../../errorHandler';
|
||||
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||
|
||||
@@ -767,6 +767,16 @@ const isMultiFrame = instance => {
|
||||
return instance.getTagValue('NumberOfFrames') > 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a display set for a series.
|
||||
* Checks if a series is reconstructable to a 3D volume.
|
||||
* If reconstructable, the frames are sorted.
|
||||
*
|
||||
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
||||
*
|
||||
* @returns {Object} imageSet.
|
||||
*/
|
||||
const makeDisplaySet = (series, instances) => {
|
||||
const instance = instances[0];
|
||||
const imageSet = new ImageSet(instances);
|
||||
@@ -786,7 +796,7 @@ const makeDisplaySet = (series, instances) => {
|
||||
isMultiFrame: isMultiFrame(instance),
|
||||
});
|
||||
|
||||
// Sort the images in this series if needed
|
||||
// Sort the images in this series by instanceNumber
|
||||
const shallSort = true; //!OHIF.utils.ObjectPath.get(Meteor, 'settings.public.ui.sortSeriesByIncomingOrder');
|
||||
if (shallSort) {
|
||||
imageSet.sortBy((a, b) => {
|
||||
@@ -804,18 +814,32 @@ const makeDisplaySet = (series, instances) => {
|
||||
imageSet.getImage(0).getTagValue('InstanceNumber')
|
||||
);
|
||||
|
||||
const isReconstructable = isDisplaySetReconstructable(instances);
|
||||
|
||||
imageSet.isReconstructable = isReconstructable.value;
|
||||
const displayReconstructableInfo = isDisplaySetReconstructable(instances);
|
||||
imageSet.isReconstructable = displayReconstructableInfo.value;
|
||||
|
||||
let displaySpacingInfo = undefined;
|
||||
if (shallSort && imageSet.isReconstructable) {
|
||||
// sort images by image position
|
||||
imageSet.sortByImagePositionPatient();
|
||||
|
||||
// check if the spacing is uniform and update isReconstructable
|
||||
const datasetIs4D = displayReconstructableInfo.warningIssues.find
|
||||
(issue => issue === ReconstructionIssues.DATASET_4D);
|
||||
displaySpacingInfo = isSpacingUniform(imageSet.images, datasetIs4D);
|
||||
imageSet.isReconstructable = displaySpacingInfo.isUniform;
|
||||
|
||||
if (displaySpacingInfo.missingFrames) {
|
||||
// TODO -> This is currently unused, but may be used for reconstructing
|
||||
// Volumes with gaps later on.
|
||||
imageSet.missingFrames = displaySpacingInfo.missingFrames;
|
||||
}
|
||||
}
|
||||
|
||||
if (isReconstructable.missingFrames) {
|
||||
// TODO -> This is currently unused, but may be used for reconstructing
|
||||
// Volumes with gaps later on.
|
||||
imageSet.missingFrames = isReconstructable.missingFrames;
|
||||
if (!imageSet.displayReconstructableInfo) {
|
||||
// It is not reconstrabale Save type of warning
|
||||
imageSet.warningIssues = displaySpacingInfo ?
|
||||
displayReconstructableInfo.warningIssues.concat(displaySpacingInfo.warningIssues) :
|
||||
displayReconstructableInfo.warningIssues;
|
||||
}
|
||||
|
||||
return imageSet;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const ReconstructionIssues = {
|
||||
DATASET_4D: 'datasetis4D',
|
||||
VARYING_IMAGESDIMENSIONS: 'imagesdimensionsvarying',
|
||||
VARYING_IMAGESCOMPONENTS: 'imagescomponentsvarying',
|
||||
VARYING_IMAGESORIENTATION: 'imagesorientationvarying',
|
||||
MISSING_FRAMES: 'missingframes',
|
||||
IRREGULAR_SPACING: 'irregularspacing',
|
||||
MULTIFFRAMES: 'multiframe',
|
||||
};
|
||||
|
||||
export {ReconstructionIssues};
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
LoggerService,
|
||||
} from './services';
|
||||
|
||||
const OHIF = {
|
||||
@@ -60,6 +61,7 @@ const OHIF = {
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
LoggerService,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -93,6 +95,7 @@ export {
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
LoggerService,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -14,6 +14,7 @@ describe('Top level exports', () => {
|
||||
'UIModalService',
|
||||
'UIDialogService',
|
||||
'MeasurementService',
|
||||
'LoggerService',
|
||||
//
|
||||
'utils',
|
||||
'hotkeys',
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
const name = 'LoggerService';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
info: _info,
|
||||
error: _error,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const serviceImplementation = {
|
||||
_info: () => console.warn('info() NOT IMPLEMENTED'),
|
||||
_error: () => console.warn('error() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs an info
|
||||
*
|
||||
* @param {object} props { message, displayOnConsole }
|
||||
*/
|
||||
function _info({ message, displayOnConsole }) {
|
||||
return serviceImplementation._info({
|
||||
message,
|
||||
displayOnConsole,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error
|
||||
*
|
||||
* @param {object} props { error, stack, message, displayOnConsole }
|
||||
* @returns void
|
||||
*/
|
||||
function _error({ error, stack, message, displayOnConsole }) {
|
||||
return serviceImplementation._error({
|
||||
error,
|
||||
stack,
|
||||
message,
|
||||
displayOnConsole,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} {
|
||||
* info: infoImplementation,
|
||||
* error: errorImplementation,
|
||||
* }
|
||||
*/
|
||||
function setServiceImplementation({
|
||||
info: infoImplementation,
|
||||
error: errorImplementation,
|
||||
}) {
|
||||
if (infoImplementation) {
|
||||
serviceImplementation._info = infoImplementation;
|
||||
}
|
||||
if (errorImplementation) {
|
||||
serviceImplementation._error = errorImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
@@ -9,6 +9,8 @@
|
||||
* @property {boolean} [closeButton=true] Should the modal body render the close button.
|
||||
* @property {string} [title=null] Should the modal render the title independently of the body content.
|
||||
* @property {string} [customClassName=null] The custom class to style the modal.
|
||||
* @property {boolean} [showScrollbar=false] Show or hide scrollbar.
|
||||
* @property {boolean} [noScroll=false] Disable or not the scrollbar.
|
||||
*/
|
||||
|
||||
const name = 'UIModalService';
|
||||
@@ -28,7 +30,7 @@ const serviceImplementation = {
|
||||
/**
|
||||
* Show a new UI modal;
|
||||
*
|
||||
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, onClose, closeButton, title, customClassName }
|
||||
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, onClose, closeButton, title, customClassName, showScrollbar, noScroll }
|
||||
*/
|
||||
function _show({
|
||||
content = null,
|
||||
@@ -40,6 +42,8 @@ function _show({
|
||||
title = null,
|
||||
fullscreen = false,
|
||||
customClassName = null,
|
||||
showScrollbar = false,
|
||||
noScroll = false,
|
||||
}) {
|
||||
return serviceImplementation._show({
|
||||
content,
|
||||
@@ -51,6 +55,8 @@ function _show({
|
||||
title,
|
||||
fullscreen,
|
||||
customClassName,
|
||||
showScrollbar,
|
||||
noScroll,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* @property {string} [position="bottomRight"] -"topLeft" | "topCenter | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight"
|
||||
* @property {string} [type="info"] - "info" | "error" | "warning" | "success"
|
||||
* @property {boolean} [autoClose=true]
|
||||
* @property {object} [action=null]
|
||||
*/
|
||||
|
||||
const name = 'UINotificationService';
|
||||
@@ -34,7 +35,7 @@ const serviceImplementation = {
|
||||
* Create and show a new UI notification; returns the
|
||||
* ID of the created notification.
|
||||
*
|
||||
* @param {Notification} notification { title, message, duration, position, type, autoClose}
|
||||
* @param {Notification} notification { title, message, duration, position, type, autoClose, action}
|
||||
* @returns {number} id
|
||||
*/
|
||||
function _show({
|
||||
@@ -44,6 +45,7 @@ function _show({
|
||||
position = 'bottomRight',
|
||||
type = 'info',
|
||||
autoClose = true,
|
||||
action = null,
|
||||
}) {
|
||||
return serviceImplementation._show({
|
||||
title,
|
||||
@@ -52,6 +54,7 @@ function _show({
|
||||
position,
|
||||
type,
|
||||
autoClose,
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import UINotificationService from './UINotificationService';
|
||||
import UIModalService from './UIModalService';
|
||||
import UIDialogService from './UIDialogService';
|
||||
import MeasurementService from './MeasurementService';
|
||||
import LoggerService from './LoggerService';
|
||||
|
||||
export {
|
||||
UINotificationService,
|
||||
@@ -10,4 +11,5 @@ export {
|
||||
UIDialogService,
|
||||
ServicesManager,
|
||||
MeasurementService,
|
||||
LoggerService,
|
||||
};
|
||||
@@ -21,12 +21,12 @@ describe('Queue', () => {
|
||||
const mockedTimeout = jest.fn(timeout);
|
||||
const timer = queue.bind(mockedTimeout);
|
||||
const start = Date.now();
|
||||
timer(120).then(now => {
|
||||
timer(1200).then(now => {
|
||||
const elapsed = now - start;
|
||||
expect(elapsed >= 120 && elapsed < 240).toBe(true);
|
||||
expect(elapsed >= 1200 && elapsed < 2400).toBe(true);
|
||||
});
|
||||
const end = await timer(120);
|
||||
expect(end - start > 240).toBe(true);
|
||||
const end = await timer(1200);
|
||||
expect(end - start > 2400).toBe(true);
|
||||
expect(mockedTimeout).toBeCalledTimes(2);
|
||||
});
|
||||
it('should prevent task execution when queue limit is reached', async () => {
|
||||
@@ -34,15 +34,15 @@ describe('Queue', () => {
|
||||
const mockedTimeout = jest.fn(timeout);
|
||||
const timer = queue.bind(mockedTimeout);
|
||||
const start = Date.now();
|
||||
const promise = timer(120).then(time => time - start);
|
||||
const promise = timer(1200).then(time => time - start);
|
||||
try {
|
||||
await timer(120);
|
||||
await timer(1200);
|
||||
} catch (e) {
|
||||
expect(Date.now() - start < 120).toBe(true);
|
||||
expect(Date.now() - start < 1200).toBe(true);
|
||||
expect(e.message).toBe('Queue limit reached');
|
||||
}
|
||||
const elapsed = await promise;
|
||||
expect(elapsed >= 120 && elapsed < 240).toBe(true);
|
||||
expect(elapsed >= 1200 && elapsed < 2400).toBe(true);
|
||||
expect(mockedTimeout).toBeCalledTimes(1);
|
||||
});
|
||||
it('should safely bind tasks to the queue', async () => {
|
||||
@@ -51,16 +51,16 @@ describe('Queue', () => {
|
||||
const mockedTimeout = jest.fn(timeout);
|
||||
const timer = queue.bindSafe(mockedTimeout, mockedErrorHandler);
|
||||
const start = Date.now();
|
||||
const promise = timer(120).then(time => time - start);
|
||||
await timer(120);
|
||||
expect(Date.now() - start < 120).toBe(true);
|
||||
const promise = timer(1200).then(time => time - start);
|
||||
await timer(1200);
|
||||
expect(Date.now() - start < 1200).toBe(true);
|
||||
expect(mockedErrorHandler).toBeCalledTimes(1);
|
||||
expect(mockedErrorHandler).nthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ message: 'Queue limit reached' })
|
||||
);
|
||||
const elapsed = await promise;
|
||||
expect(elapsed >= 120 && elapsed < 240).toBe(true);
|
||||
expect(elapsed >= 1200 && elapsed < 2400).toBe(true);
|
||||
expect(mockedTimeout).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,14 @@
|
||||
import { ReconstructionIssues } from './../enums.js';
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a series is reconstructable to a 3D volume.
|
||||
*
|
||||
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
||||
* @param {Object[]} An array of `OHIFInstanceMetadata` objects.
|
||||
*
|
||||
* @returns {Object} value, warningIssues.
|
||||
*/
|
||||
export default function isDisplaySetReconstructable(instances) {
|
||||
function isDisplaySetReconstructable(instances) {
|
||||
if (!instances.length) {
|
||||
return { value: false };
|
||||
}
|
||||
@@ -23,31 +28,45 @@ export default function isDisplaySetReconstructable(instances) {
|
||||
}
|
||||
|
||||
if (isMultiframe) {
|
||||
return processMultiframe(instances[0]);
|
||||
return processMultiframe();
|
||||
} else {
|
||||
return processSingleframe(instances);
|
||||
}
|
||||
}
|
||||
|
||||
function processMultiframe(instance) {
|
||||
//TODO: deal with multriframe checks! return false for now as can't reconstruct.
|
||||
return { value: false };
|
||||
/**
|
||||
* Process reconstructable multiframes checks
|
||||
* TODO: deal with multriframe checks! return false for now as can't reconstruct.
|
||||
* *
|
||||
* @returns {Object} value and warningIssues.
|
||||
*/
|
||||
function processMultiframe() {
|
||||
const warningIssues = [ReconstructionIssues.MULTIFRAMES];
|
||||
return { value: false, warningIssues };
|
||||
}
|
||||
|
||||
/**
|
||||
* Process reconstructable single frame checks
|
||||
*
|
||||
* @param {Object[]} An array of `OHIFInstanceMetadata` objects.
|
||||
*
|
||||
* @returns {Object} value and warningIssues.
|
||||
*/
|
||||
function processSingleframe(instances) {
|
||||
const n = instances.length;
|
||||
const firstImage = instances[0].getData().metadata;
|
||||
const firstImageRows = firstImage.Rows;
|
||||
const firstImageColumns = firstImage.Columns;
|
||||
const firstImageSamplesPerPixel = firstImage.SamplesPerPixel;
|
||||
const firstImageOrientationPatient = firstImage.ImageOrientationPatient;
|
||||
const firstImagePositionPatient = firstImage.ImagePositionPatient;
|
||||
|
||||
const warningIssues = [];
|
||||
// Can't reconstruct if we:
|
||||
// -- Have a different dimensions within a displaySet.
|
||||
// -- Have a different number of components within a displaySet.
|
||||
// -- Have different orientations within a displaySet.
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i].getData().metadata;
|
||||
for (let ii = 1; ii < n; ++ii) {
|
||||
const instance = instances[ii].getData().metadata;
|
||||
const {
|
||||
Rows,
|
||||
Columns,
|
||||
@@ -55,67 +74,146 @@ function processSingleframe(instances) {
|
||||
ImageOrientationPatient,
|
||||
} = instance;
|
||||
|
||||
if (
|
||||
Rows !== firstImageRows ||
|
||||
Columns !== firstImageColumns ||
|
||||
SamplesPerPixel !== firstImageSamplesPerPixel ||
|
||||
!_isSameOrientation(ImageOrientationPatient, firstImageOrientationPatient)
|
||||
) {
|
||||
return { value: false };
|
||||
if (Rows !== firstImageRows || Columns !== firstImageColumns) {
|
||||
warningIssues.push(ReconstructionIssues.VARYING_IMAGESDIMENSIONS);
|
||||
} else if (SamplesPerPixel !== firstImageSamplesPerPixel) {
|
||||
warningIssues.push(ReconstructionIssues.VARYING_IMAGESCOMPONENTS);
|
||||
} else if (!_isSameArray(ImageOrientationPatient, firstImageOrientationPatient)) {
|
||||
warningIssues.push(ReconstructionIssues.VARYING_IMAGESORIENTATION);
|
||||
}
|
||||
|
||||
if (warningIssues.length !== 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// check if dataset is 4D
|
||||
if (_isDataset4D(instances)) {
|
||||
warningIssues.push(ReconstructionIssues.DATASET_4D);
|
||||
}
|
||||
|
||||
return { value: warningIssues.length === 0 ? true : false, warningIssues };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is the spacing is uniform.
|
||||
* The input metadata array has to be ordered by image position.
|
||||
*
|
||||
* @param {Object[]} An array of `OHIFInstanceMetadata` objects.
|
||||
* @param {boolean} is the dataset 4D.
|
||||
*
|
||||
* @returns {Object} isUniform, warningIssues and missingFrames
|
||||
*/
|
||||
function isSpacingUniform(instances, datasetIs4D) {
|
||||
const n = instances.length;
|
||||
const firstImage = instances[0].getData().metadata;
|
||||
const firstImagePositionPatient = firstImage.ImagePositionPatient;
|
||||
|
||||
const warningIssues = [];
|
||||
let missingFrames = 0;
|
||||
|
||||
// Check if frame spacing is approximately equal within a spacingTolerance.
|
||||
// If spacing is on a uniform grid but we are missing frames,
|
||||
// Allow reconstruction, but pass back the number of missing frames.
|
||||
if (instances.length > 2) {
|
||||
const lastIpp = instances[instances.length - 1].getData().metadata
|
||||
if (n > 2) {
|
||||
const lastIpp = instances[n - 1].getData().metadata
|
||||
.ImagePositionPatient;
|
||||
|
||||
// We can't reconstruct if we are missing ImagePositionPatient values
|
||||
if (!firstImagePositionPatient || !lastIpp) {
|
||||
return { value: false };
|
||||
}
|
||||
if (firstImagePositionPatient && lastIpp) {
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||
(n - 1);
|
||||
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||
(instances.length - 1);
|
||||
let previousImagePositionPatient = firstImagePositionPatient;
|
||||
|
||||
let previousImagePositionPatient = firstImagePositionPatient;
|
||||
for (let ii = 1; ii < n; ++ii) {
|
||||
const instance = instances[ii].getData().metadata;
|
||||
const { ImagePositionPatient } = instance;
|
||||
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i].getData().metadata;
|
||||
const { ImagePositionPatient } = instance;
|
||||
const spacingBetweenFrames = _getPerpendicularDistance(
|
||||
ImagePositionPatient,
|
||||
previousImagePositionPatient
|
||||
);
|
||||
|
||||
const spacingBetweenFrames = _getPerpendicularDistance(
|
||||
ImagePositionPatient,
|
||||
previousImagePositionPatient
|
||||
);
|
||||
const spacingIssue = _getSpacingIssue(
|
||||
spacingBetweenFrames,
|
||||
averageSpacingBetweenFrames
|
||||
);
|
||||
|
||||
if (spacingIssue) {
|
||||
const issue = spacingIssue.issue;
|
||||
|
||||
if (issue === reconstructionIssues.MISSING_FRAMES) {
|
||||
missingFrames += spacingIssue.missingFrames;
|
||||
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
|
||||
return { value: false };
|
||||
if (datasetIs4D && spacingBetweenFrames < 1.e-3) {
|
||||
// the dataset is 4D, if the distance is zero, means that we are
|
||||
// checking the 4th dimension. Do not return, since we want still to
|
||||
// check the 3rd dimension spacing.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
previousImagePositionPatient = ImagePositionPatient;
|
||||
const spacingIssue = _getSpacingIssue(
|
||||
spacingBetweenFrames,
|
||||
averageSpacingBetweenFrames
|
||||
);
|
||||
|
||||
if (spacingIssue) {
|
||||
const issue = spacingIssue.issue;
|
||||
|
||||
if (issue === ReconstructionIssues.MISSING_FRAMES) {
|
||||
missingFrames += spacingIssue.missingFrames;
|
||||
} else if (issue === ReconstructionIssues.IRREGULAR_SPACING) {
|
||||
warningIssues.push(issue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
previousImagePositionPatient = ImagePositionPatient;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { value: true, missingFrames };
|
||||
return { isUniform: warningIssues.length === 0 ? true : false, missingFrames, warningIssues };
|
||||
}
|
||||
|
||||
function _isSameOrientation(iop1, iop2) {
|
||||
|
||||
/**
|
||||
* Check if 4D dataset.
|
||||
*
|
||||
* Assuming that slices at different time have the same position, here we just check if
|
||||
* there are multiple slices for the same ImagePositionPatient and disable MPR.
|
||||
*
|
||||
* A better heuristic would be checking 4D tags, e.g. the presence of multiple TemporalPositionIdentifier values.
|
||||
* However, some studies (e.g. https://github.com/OHIF/Viewers/issues/2113) do not have such tags.
|
||||
*
|
||||
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
||||
*
|
||||
* @returns {boolean} dataset4D value.
|
||||
*/
|
||||
function _isDataset4D(instances) {
|
||||
const n = instances.length;
|
||||
for (let ii = 0; ii < n; ++ii) {
|
||||
const instanceMetadataControl = instances[ii].getData().metadata;
|
||||
if (
|
||||
!instanceMetadataControl ||
|
||||
instanceMetadataControl === undefined ||
|
||||
!instanceMetadataControl.ImagePositionPatient ||
|
||||
instanceMetadataControl.ImagePositionPatient === undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (let jj = ii + 1; jj < n; ++jj) {
|
||||
const instanceMetadata = instances[jj].getData().metadata;
|
||||
if (
|
||||
!instanceMetadata ||
|
||||
instanceMetadata === undefined ||
|
||||
!instanceMetadata.ImagePositionPatient ||
|
||||
instanceMetadata.ImagePositionPatient === undefined
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_isSameArray(instanceMetadataControl.ImagePositionPatient, instanceMetadata.ImagePositionPatient)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function _isSameArray(iop1, iop2) {
|
||||
if (iop1 === undefined || !iop2 === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -156,12 +254,12 @@ function _getSpacingIssue(spacing, averageSpacing) {
|
||||
|
||||
if (errorForEachSpacing < spacingTolerance * averageSpacing) {
|
||||
return {
|
||||
issue: reconstructionIssues.MISSING_FRAMES,
|
||||
issue: ReconstructionIssues.MISSING_FRAMES,
|
||||
missingFrames: numberOfSpacings - 1,
|
||||
};
|
||||
}
|
||||
|
||||
return { issue: reconstructionIssues.IRREGULAR_SPACING };
|
||||
return { issue: ReconstructionIssues.IRREGULAR_SPACING };
|
||||
}
|
||||
|
||||
function _getPerpendicularDistance(a, b) {
|
||||
@@ -173,7 +271,5 @@ function _getPerpendicularDistance(a, b) {
|
||||
}
|
||||
|
||||
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
|
||||
const reconstructionIssues = {
|
||||
MISSING_FRAMES: 'missingframes',
|
||||
IRREGULAR_SPACING: 'irregularspacing',
|
||||
};
|
||||
|
||||
export {isDisplaySetReconstructable, isSpacingUniform};
|
||||
@@ -45,13 +45,13 @@ import studyMetadataManager from './studyMetadataManager';
|
||||
* @param {string} referencedDisplaySet.studyInstanceUid
|
||||
* @param {Array} referencedDisplaySet.sopClassUids
|
||||
* @param {Study[]} studies Collection of studies
|
||||
* @param {object} logger
|
||||
* @param {object} snackbar
|
||||
* @returns void
|
||||
*/
|
||||
const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
||||
async function loadAndCacheDerivedDisplaySets(referencedDisplaySet, studies, logger, snackbar) {
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
||||
|
||||
const promises = [];
|
||||
|
||||
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||
|
||||
if (!studyMetadata) {
|
||||
@@ -80,11 +80,10 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
||||
});
|
||||
|
||||
// For each type, see if any are loaded, if not load the most recent.
|
||||
Object.keys(displaySetsPerModality).forEach(key => {
|
||||
await Promise.all(Object.keys(displaySetsPerModality).map(async (key) => {
|
||||
const displaySets = displaySetsPerModality[key];
|
||||
|
||||
const isLoaded = displaySets.some(displaySet => displaySet.isLoaded);
|
||||
|
||||
if (isLoaded) {
|
||||
return;
|
||||
}
|
||||
@@ -109,10 +108,27 @@ const loadAndCacheDerivedDisplaySets = (referencedDisplaySet, studies) => {
|
||||
|
||||
recentDisplaySet.isLoading = true;
|
||||
|
||||
promises.push(recentDisplaySet.load(referencedDisplaySet, studies));
|
||||
});
|
||||
try {
|
||||
await recentDisplaySet.load(referencedDisplaySet, studies);
|
||||
} catch (error) {
|
||||
logger.error({ error, message: error.message });
|
||||
snackbar.show({
|
||||
title: 'Error loading derived display set:',
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
error,
|
||||
autoClose: false,
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
return promises;
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app
|
||||
* that depends on derived display sets to be loaded.
|
||||
* (Implement pubsub for better tracking of derived display sets)
|
||||
*/
|
||||
const event = new CustomEvent('deriveddisplaysetsloadedandcached');
|
||||
document.dispatchEvent(event);
|
||||
};
|
||||
|
||||
export default loadAndCacheDerivedDisplaySets;
|
||||
@@ -0,0 +1,9 @@
|
||||
const validNumber = val => {
|
||||
if (Array.isArray(val)) {
|
||||
return val.map(v => (v !== undefined ? Number(v) : v));
|
||||
} else {
|
||||
return val !== undefined ? Number(val) : val;
|
||||
}
|
||||
};
|
||||
|
||||
export default validNumber;
|
||||
@@ -3,6 +3,23 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.53.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.8...@ohif/i18n@0.53.0) (2020-11-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add syncTranslations script ([b161016](https://github.com/OHIF/Viewers/commit/b161016edb8808ff0d17ea637820837dca1ca3ce))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **i18n:** Added de (German) translations ([2fff2d5](https://github.com/OHIF/Viewers/commit/2fff2d511fa1d36bdd6476da4ffa5684bf55374f))
|
||||
* **i18n:** Added Russian language localization. ([d5b8974](https://github.com/OHIF/Viewers/commit/d5b89748c92c2cf5a88f54ae491adc31a1679705))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.8](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.7...@ohif/i18n@0.52.8) (2020-04-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/i18n
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/i18n",
|
||||
"version": "0.52.8",
|
||||
"version": "0.53.0",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"No hotkeys found": "Nenhuma tecla de atalho está configurada para este aplicativo. As teclas de atalho podem ser configuradas no arquivo app-config.js do aplicativo."
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'ar': {
|
||||
UserPreferencesModal,
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Browser": "Browser",
|
||||
"Build Number": "Build Number",
|
||||
"Latest Master Commits": "Letzter Master Commit",
|
||||
"More details": "Mehr Details",
|
||||
"Name": "Name",
|
||||
"OHIF Viewer - About": "OHIF Viewer",
|
||||
"OS": "OS",
|
||||
"Report an issue": "Problem melden",
|
||||
"Repository URL": "Repository URL",
|
||||
"Value": "Wert",
|
||||
"Version Information": "Versionsinformation",
|
||||
"Visit the forum": "Das Forum besuchen"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Acquired": "Akquiriert",
|
||||
"Angle": "Winkel",
|
||||
"Axial": "Axial",
|
||||
"Bidirectional": "Bidirektional",
|
||||
"Brush": "Pinsel",
|
||||
"CINE": "CINE",
|
||||
"Cancel": "Abbruch",
|
||||
"Circle": "Kreis",
|
||||
"Clear": "Entfernen",
|
||||
"Coronal": "Coronal",
|
||||
"Crosshairs": "Kreuzlinien",
|
||||
"Download": "Herunterladen",
|
||||
"Ellipse": "Ellipse",
|
||||
"Elliptical": "Elliptisch",
|
||||
"Flip H": "Spiegeln H",
|
||||
"Flip V": "Spiegeln V",
|
||||
"Freehand": "Freihand",
|
||||
"Invert": "Invertieren",
|
||||
"Layout": "$t(Common:Layout)",
|
||||
"Length": "Länge",
|
||||
"Levels": "Levels",
|
||||
"Magnify": "Vergrössern",
|
||||
"Manual": "Manuell",
|
||||
"Measurements": "Messungen",
|
||||
"More": "$t(Common:More)",
|
||||
"Next": "$t(Common:Next)",
|
||||
"Pan": "Verschieben",
|
||||
"Play": "$t(Common:Play)",
|
||||
"Previous": "$t(Common:Previous)",
|
||||
"Probe": "Messung",
|
||||
"ROI Window": "ROI Fenster",
|
||||
"Rectangle": "Rechteck",
|
||||
"Reset": "$t(Common:Reset)",
|
||||
"Reset to Defaults": "Standardwerte",
|
||||
"Rotate Right": "Rechts drehen",
|
||||
"Sagittal": "Sagittal",
|
||||
"Save": "Speichern",
|
||||
"Stack Scroll": "Stapel",
|
||||
"Stop": "$t(Common:Stop)",
|
||||
"Themes": "Themes",
|
||||
"Zoom": "Zoom"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Next image": "$t(Common:Next) $t(Common:Image)",
|
||||
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
||||
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||
"Skip to first image": "Zum ersten $t(Common:Image)",
|
||||
"Skip to last image": "Zum letzten $t(Common:Image)",
|
||||
"fps": "fps"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Close": "Schliessen",
|
||||
"Image": "Bild",
|
||||
"Layout": "Layout",
|
||||
"Measurements": "Messungen",
|
||||
"More": "Mehr",
|
||||
"Next": "Nächstes",
|
||||
"Play": "Abspielen",
|
||||
"Previous": "Zurück",
|
||||
"Reset": "Zurücksetzen",
|
||||
"RowsPerPage": "Zeilen pro Seite",
|
||||
"Series": "Serie",
|
||||
"Show": "Zeigen",
|
||||
"Stop": "Stopp",
|
||||
"StudyDate": "Studien Datum"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"Clear dates": "Zeiträume löschen",
|
||||
"End Date": "Bis",
|
||||
"Start Date": "Ab"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"About": "Über",
|
||||
"Back to Viewer": "Zurück zum Viewer",
|
||||
"INVESTIGATIONAL USE ONLY": "NICHT ZUR BEFUNDUNG FREIGEGEBEN",
|
||||
"Options": "Optionen",
|
||||
"Preferences": "Einstellungen",
|
||||
"Study list": "Studienliste"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Criteria nonconformities": "Kriterien für Nichtkonformität",
|
||||
"Delete": "Löschen",
|
||||
"Description": "Beschreibung",
|
||||
"MAX": "MAX",
|
||||
"NonTargets": "NichtZiele",
|
||||
"Relabel": "Umbenennen",
|
||||
"Targets": "Ziele"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"AccessionNumber": "Accession #",
|
||||
"Empty": "Leer",
|
||||
"MRN": "MRN",
|
||||
"Modality": "Modalität",
|
||||
"PatientName": "Patientenname",
|
||||
"StudyDate": "Studiendatum",
|
||||
"StudyDescription": "Beschreibung",
|
||||
"StudyList": "Studienliste"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Cancel": "$t(Buttons:Cancel)",
|
||||
"No hotkeys found": "Keine Tastaturkürzel konfiguriert. Tastaturkürzel können in der app-config.js Datei der Anwendung konfiguriert werden.",
|
||||
"Reset to Defaults": "$t(Buttons:Reset to Defaults)",
|
||||
"ResetDefaultMessage": "Einstellungen erfolgreich zurückgesetzt. <br /> Sie müssen <strong>Speichern</strong> um dieses abzuschliessen.",
|
||||
"Save": "$t(Buttons:Save)",
|
||||
"SaveMessage": "Einstellungen gesichert",
|
||||
"User Preferences": "Benutzereinstellungen"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"emptyFilenameError": "Dateiname kann nicht leer sein.",
|
||||
"fileType": "Dateityp",
|
||||
"filename": "Dateiname",
|
||||
"formTitle": "Bitte geben Sie die Dimensionen, Dateiname und den gewünschten Dateityp an.",
|
||||
"imageHeight": "Bildhöhe (px)",
|
||||
"imagePreview": "Bild Vorschau",
|
||||
"imageWidth": "Bildbreite (px)",
|
||||
"keepAspectRatio": "Seitenverhältnis erbehalten",
|
||||
"loadingPreview": "Bildvorschau laden...",
|
||||
"minHeightError": "Die minimale Höhe beträgt 100 Pixel.",
|
||||
"minWidthError": "Die minimale Breiete beträgt 100 Pixel.",
|
||||
"showAnnotations": "Annotationen anzeigen"
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import AboutModal from './AboutModal.json';
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import DatePicker from './DatePicker.json';
|
||||
import Header from './Header.json';
|
||||
import MeasurementTable from './MeasurementTable.json';
|
||||
import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||
|
||||
export default {
|
||||
'de': {
|
||||
AboutModal,
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
DatePicker,
|
||||
Header,
|
||||
MeasurementTable,
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
ViewportDownloadForm,
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,19 +1,25 @@
|
||||
import AboutModal from './AboutModal.json';
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import DatePicker from './DatePicker.json';
|
||||
import Header from './Header.json';
|
||||
import MeasurementTable from './MeasurementTable.json';
|
||||
import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||
|
||||
export default {
|
||||
'es': {
|
||||
AboutModal,
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
DatePicker,
|
||||
Header,
|
||||
MeasurementTable,
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
ViewportDownloadForm,
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -1,15 +1,25 @@
|
||||
import AboutModal from './AboutModal.json';
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import DatePicker from './DatePicker.json';
|
||||
import Header from './Header.json';
|
||||
import MeasurementTable from './MeasurementTable.json';
|
||||
import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||
|
||||
export default {
|
||||
'fr': {
|
||||
AboutModal,
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
DatePicker,
|
||||
Header,
|
||||
MeasurementTable,
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
ViewportDownloadForm,
|
||||
}
|
||||
};
|
||||
Loaded 100 of 210 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user