Compare commits
195
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d426b78967 | ||
|
|
1d85f7a74e | ||
|
|
8833731ad3 | ||
|
|
bb7cce4716 | ||
|
|
4ad3ab93b0 | ||
|
|
3e074da64e | ||
|
|
d66bad664a | ||
|
|
fa47db4a54 | ||
|
|
e1c21a7ce1 | ||
|
|
c785a5e975 | ||
|
|
f133be75d1 | ||
|
|
b880663818 | ||
|
|
e160889605 | ||
|
|
2f0d1b651b | ||
|
|
d034006294 | ||
|
|
2f459a0f64 | ||
|
|
ee80e02661 | ||
|
|
4e53fbfe9d | ||
|
|
64dfbeab7a | ||
|
|
9c1112f768 | ||
|
|
9b3be9b0c0 | ||
|
|
6829542c00 | ||
|
|
846c9c1c54 | ||
|
|
85fed49eb1 | ||
|
|
298da03e2f | ||
|
|
141e1cf648 | ||
|
|
ea2ab1a25e | ||
|
|
3d0357c9c0 | ||
|
|
f68aaf973c | ||
|
|
f04d459a63 | ||
|
|
4cf9f2c9cb | ||
|
|
c0cc9752c6 | ||
|
|
2910d38668 | ||
|
|
6c950a9669 | ||
|
|
06075ecb51 | ||
|
|
509a7b7e57 | ||
|
|
4bbf24b250 | ||
|
|
ebc01a8ca2 | ||
|
|
4fd0c6dfcb | ||
|
|
06a4af06fa | ||
|
|
6cba41dd8b | ||
|
|
2f30e7a821 | ||
|
|
9a62c28b3f | ||
|
|
98a649b455 | ||
|
|
3529474a16 | ||
|
|
0c37a406d9 | ||
|
|
e8d56c11f0 | ||
|
|
a6b92a4505 | ||
|
|
a4c9a86772 | ||
|
|
73e440968c | ||
|
|
3f7eb8a5d3 | ||
|
|
7b2d44f2c1 | ||
|
|
225da81135 | ||
|
|
686d12da5c | ||
|
|
c22756051b | ||
|
|
8fdfcc0c7d | ||
|
|
44ab65c981 | ||
|
|
b9574b6efc | ||
|
|
96a8e26786 | ||
|
|
80f5b23622 | ||
|
|
2b4950e954 | ||
|
|
e16f14089d | ||
|
|
c3807a89ed | ||
|
|
049ef4a666 | ||
|
|
d5fa880a29 | ||
|
|
1b9a6db73f | ||
|
|
ed2692908e | ||
|
|
0c6c5a5d51 | ||
|
|
b449f8eb0e | ||
|
|
cf5160fb13 | ||
|
|
4c1c91c006 | ||
|
|
9ed39c149b | ||
|
|
b3f0fe029d | ||
|
|
a1b78afdb9 | ||
|
|
3d3cfb1b41 | ||
|
|
b812fe03d2 | ||
|
|
84f087bca4 | ||
|
|
a7e612eb88 | ||
|
|
9e3afda2f1 | ||
|
|
4d7a91a584 | ||
|
|
1dc320edcd | ||
|
|
1d02f5485f | ||
|
|
4c9a977920 | ||
|
|
b5db09c25e | ||
|
|
8dc4a5b82a | ||
|
|
44add9ce2f | ||
|
|
c19eceeede | ||
|
|
72bc5a5ae5 | ||
|
|
dfdf46dbdd | ||
|
|
571dff2c09 | ||
|
|
b9a66d4424 | ||
|
|
7ab2e3e60d | ||
|
|
a5d6bc6a51 | ||
|
|
df612ca4a0 | ||
|
|
a91ce57c43 | ||
|
|
342884f83e | ||
|
|
0fb39bad84 | ||
|
|
c74c80bb6f | ||
|
|
910d5c65d5 | ||
|
|
d0fb4843ab | ||
|
|
f488b857fe | ||
|
|
256e8b1116 | ||
|
|
168dd87747 | ||
|
|
13c41e6d86 | ||
|
|
7ab0bbb325 | ||
|
|
2213187b14 | ||
|
|
4c4d8a61b6 | ||
|
|
67f58f5f09 | ||
|
|
dfc35885bb | ||
|
|
e9c449d1db | ||
|
|
6dac50c7c6 | ||
|
|
9be54f6164 | ||
|
|
341cc5a4eb | ||
|
|
7b80171203 | ||
|
|
2e5a385e53 | ||
|
|
a3caeb381e | ||
|
|
ab0af86379 | ||
|
|
86426cb024 | ||
|
|
348840ef26 | ||
|
|
52ef934961 | ||
|
|
be995f93b6 | ||
|
|
5036615e00 | ||
|
|
4ee2b135a7 | ||
|
|
388fe04e09 | ||
|
|
2e215aed4c | ||
|
|
ffd27d6db4 | ||
|
|
6540e36818 | ||
|
|
a436436def | ||
|
|
d6d92afbda | ||
|
|
e1142910e4 | ||
|
|
067e6c769b | ||
|
|
d375d02a4b | ||
|
|
63594d36b0 | ||
|
|
6c160f517f | ||
|
|
23305cec9c | ||
|
|
36a741f0bc | ||
|
|
695ad732e1 | ||
|
|
73a3f07e2b | ||
|
|
4ea239a953 | ||
|
|
625d5d70a7 | ||
|
|
5c3d8b37b6 | ||
|
|
8bbbf3a889 | ||
|
|
d5bf72851a | ||
|
|
2914c83032 | ||
|
|
31e542dfd9 | ||
|
|
25599a2e2a | ||
|
|
1cf7015511 | ||
|
|
756f70cc54 | ||
|
|
ded2e61913 | ||
|
|
b58aa4575a | ||
|
|
f78abe2afa | ||
|
|
f48157117f | ||
|
|
6f8bd0dff8 | ||
|
|
943af89df2 | ||
|
|
cd2da349e5 | ||
|
|
14e86dcddd | ||
|
|
2c8b93656d | ||
|
|
6932468206 | ||
|
|
7bc9efb0f5 | ||
|
|
5d47cc43c1 | ||
|
|
e46a11a01e | ||
|
|
27a950d956 | ||
|
|
03d4c10adc | ||
|
|
7a0c369006 | ||
|
|
8c946a4742 | ||
|
|
30b9e44220 | ||
|
|
380abe2705 | ||
|
|
f4570c96c2 | ||
|
|
1fe951a71e | ||
|
|
ebe1af8d4f | ||
|
|
5cda5159da | ||
|
|
115a3ec0b0 | ||
|
|
ac82a111e7 | ||
|
|
a22fd67e13 | ||
|
|
91f0af058f | ||
|
|
5a1186388d | ||
|
|
2f0f92c095 | ||
|
|
5c686268ab | ||
|
|
974aabc9b2 | ||
|
|
17e3b49b06 | ||
|
|
e3c119489a | ||
|
|
17447baf44 | ||
|
|
daa3ddccdb | ||
|
|
c91b6dddd8 | ||
|
|
92548191e4 | ||
|
|
68fcaef19a | ||
|
|
ef611e4849 | ||
|
|
d128fd6bf0 | ||
|
|
3bdd434488 | ||
|
|
29bac8df7a | ||
|
|
319c67c389 | ||
|
|
b9836f6c71 | ||
|
|
6a6e2d7df3 | ||
|
|
2f8c408ed2 | ||
|
|
27910239e0 |
No files matched your search
+62
-28
@@ -13,14 +13,7 @@ version: 2.1
|
||||
##
|
||||
orbs:
|
||||
codecov: codecov/codecov@1.0.5
|
||||
cypress: cypress-io/cypress@1.11.0
|
||||
executors:
|
||||
# Custom executor to override Cypress config
|
||||
deploy-to-prod-executor:
|
||||
docker:
|
||||
- image: 'cypress/browsers:node10.16.0-chrome77'
|
||||
environment:
|
||||
CYPRESS_BASE_URL: https://ohif-staging.netlify.com/
|
||||
cypress: cypress-io/cypress@1.13.0
|
||||
|
||||
defaults: &defaults
|
||||
docker:
|
||||
@@ -335,12 +328,15 @@ workflows:
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*,cypress/integration/pwa/**/*'
|
||||
wait-on: 'http://localhost:3000'
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/videos
|
||||
requires:
|
||||
- UNIT_TESTS
|
||||
# E2E: script-tag
|
||||
@@ -357,16 +353,46 @@ workflows:
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build:package
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*'
|
||||
spec: 'cypress/integration/common/**/*,cypress/integration/script-tag/**/*'
|
||||
wait-on: 'http://localhost:3000'
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/videos
|
||||
requires:
|
||||
- UNIT_TESTS
|
||||
|
||||
PR_OPTIONAL_VISUAL_TESTS:
|
||||
jobs:
|
||||
- AWAIT_APPROVAL:
|
||||
type: approval
|
||||
# Update hub.docker.org
|
||||
- cypress/run:
|
||||
name: 'Generate Percy Snapshots'
|
||||
executor: cypress/browsers-chrome76
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
yarn: true
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
# start server --> verify running --> percy + chrome + cypress
|
||||
command: yarn run test:e2e:dist
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/videos
|
||||
requires:
|
||||
- AWAIT_APPROVAL
|
||||
|
||||
PR_OPTIONAL_DOCKER_PUBLISH:
|
||||
jobs:
|
||||
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
|
||||
@@ -419,25 +445,33 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
# Update base branch snapshots
|
||||
# and record a Cypress dashboard test run
|
||||
- cypress/run:
|
||||
name: 'Generate Percy Snapshots'
|
||||
executor: cypress/browsers-chrome76
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
yarn: true
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
# start server --> verify running --> percy + chrome + cypress
|
||||
command: yarn run test:e2e:dist
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/videos
|
||||
- store_test_results:
|
||||
path: platform/viewer/cypress/results
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- DOCKER_MASTER_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
#
|
||||
#
|
||||
# Potentially use to E2E test STAGING environment
|
||||
# before publishing to production
|
||||
# - cypress/run:
|
||||
# name: 'E2E: PWA'
|
||||
# executor: deploy-to-prod-executor
|
||||
# browser: chrome
|
||||
# working_directory: platform/viewer
|
||||
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
# yarn: true
|
||||
# record: false
|
||||
# no-workspace: true
|
||||
# store_artifacts: true
|
||||
# post-steps:
|
||||
# - store_artifacts:
|
||||
# path: platform/viewer/cypress/screenshots
|
||||
# - store_test_results:
|
||||
# path: cypress/results
|
||||
@@ -1,17 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
|
||||
if [ ! -z "$CLIENT_ID" ]
|
||||
if [ -n "$CLIENT_ID" ] || [ -n "$HEALTHCARE_API_ENDPOINT" ]
|
||||
then
|
||||
echo "Google Cloud Healthcare $CLIENT_ID has been provided: "
|
||||
echo $CLIENT_ID
|
||||
echo "Updating config..."
|
||||
# If CLIENT_ID is specified, use the google.js configuration with the modified ID
|
||||
if [ -n "$CLIENT_ID" ]
|
||||
then
|
||||
echo "Google Cloud Healthcare \$CLIENT_ID has been provided: "
|
||||
echo "$CLIENT_ID"
|
||||
echo "Updating config..."
|
||||
|
||||
# - Use SED to replace the CLIENT_ID that is currently in google.js
|
||||
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
|
||||
# - Use SED to replace the CLIENT_ID that is currently in google.js
|
||||
sed -i -e "s/YOURCLIENTID.apps.googleusercontent.com/$CLIENT_ID/g" /usr/share/nginx/html/google.js
|
||||
fi
|
||||
|
||||
# - Copy google.js to overwrite app-config.js
|
||||
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
|
||||
# If HEALTHCARE_API_ENDPOINT is specified, use the google.js configuration with the modified endpoint
|
||||
if [ -n "$HEALTHCARE_API_ENDPOINT" ]
|
||||
then
|
||||
echo "Google Cloud Healthcare \$HEALTHCARE_API_ENDPOINT has been provided: "
|
||||
echo "$HEALTHCARE_API_ENDPOINT"
|
||||
echo "Updating config..."
|
||||
|
||||
# - Use SED to replace the HEALTHCARE_API_ENDPOINT that is currently in google.js
|
||||
sed -i -e "s+https://healthcare.googleapis.com/v1beta1+$HEALTHCARE_API_ENDPOINT+g" /usr/share/nginx/html/google.js
|
||||
fi
|
||||
|
||||
# - Copy google.js to overwrite app-config.js
|
||||
cp /usr/share/nginx/html/google.js /usr/share/nginx/html/app-config.js
|
||||
fi
|
||||
|
||||
echo "Starting Nginx to serve the OHIF Viewer..."
|
||||
|
||||
File renamed without changes.
+3
-2
@@ -9,13 +9,14 @@ daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- 'Story :raised_hands:'
|
||||
- 'Bug: Verified :bug:'
|
||||
- 'Task: CI/Tooling 🤖'
|
||||
- 'Task: CI/Tooling :robot:'
|
||||
- 'Task: Docs 📖'
|
||||
- 'Task: Docs :book:'
|
||||
- 'Task: Refactor 🛠'
|
||||
- 'Task: Refactor :hammer_and_wrench:'
|
||||
- 'Task: Tests :microscope:'
|
||||
- 'PR: Awaiting Review 👀'
|
||||
- 'Triage :white_flag:'
|
||||
- 'Extension: Discussion'
|
||||
- 'Announcement 🎉'
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: 'Stale :baguette_bread:'
|
||||
|
||||
@@ -30,6 +30,7 @@ docker/dcm4che/dcm4che-arc
|
||||
|
||||
# Cypress test results
|
||||
videos/
|
||||
screenshots/
|
||||
|
||||
# Locize settings
|
||||
.locize
|
||||
Vendored
+4
-1
@@ -28,5 +28,8 @@
|
||||
],
|
||||
"prettier.disableLanguages": ["html"],
|
||||
"prettier.endOfLine": "lf",
|
||||
"workbench.colorCustomizations": {}
|
||||
"workbench.colorCustomizations": {},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
}
|
||||
}
|
||||
@@ -72,12 +72,17 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
/* Application */
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
|
||||
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
|
||||
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
|
||||
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
|
||||
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
|
||||
/* i18n */
|
||||
'process.env.USE_LOCIZE': JSON.stringify(process.env.USE_LOCIZE || ''),
|
||||
'process.env.LOCIZE_PROJECTID': JSON.stringify(process.env.LOCIZE_PROJECTID || ''),
|
||||
'process.env.LOCIZE_API_KEY': JSON.stringify(process.env.LOCIZE_API_KEY || ''),
|
||||
}),
|
||||
],
|
||||
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at danny.ri.brown+OHIFcoc@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -0,0 +1 @@
|
||||
See our contributing guidelines at [`https://docs.ohif.org`](https://docs.ohif.org/development/contributing.html)
|
||||
@@ -271,8 +271,8 @@ MIT © [OHIF](https://github.com/OHIF)
|
||||
<!-- Badges -->
|
||||
[lerna-image]: https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg
|
||||
[lerna-url]: https://lerna.js.org/
|
||||
[netlify-image]: https://api.netlify.com/api/v1/badges/a5d369ab-18a6-41c3-bcde-83805205ac7f/deploy-status
|
||||
[netlify-url]: https://app.netlify.com/sites/ohif/deploys
|
||||
[netlify-image]: https://api.netlify.com/api/v1/badges/32708787-c9b0-4634-b50f-7ca41952da77/deploy-status
|
||||
[netlify-url]: https://app.netlify.com/sites/ohif-dev/deploys
|
||||
[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square
|
||||
[circleci-image]: https://circleci.com/gh/OHIF/Viewers.svg?style=svg
|
||||
[circleci-url]: https://circleci.com/gh/OHIF/Viewers
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# OHIF Viewers
|
||||
|
||||
- [Our Process](our-process.md)
|
||||
- [Quick Start](quick-start.md)
|
||||
- Development
|
||||
- [Getting Started](development/getting-started.md)
|
||||
- [Contributing](development/contributing.md)
|
||||
@@ -31,7 +30,6 @@
|
||||
- [OHIF Maintained](extensions/index.md#maintained-extensions)
|
||||
- [Services](services/index.md)
|
||||
- [Default](services/default/index.md)
|
||||
- [Measurements](services/default/measurements.md)
|
||||
- [UI](services/ui/index.md)
|
||||
- [Dialog Service](services/ui/ui-dialog-service.md)
|
||||
- [Modal Service](services/ui/ui-modal-service.md)
|
||||
|
||||
@@ -193,7 +193,8 @@ today, create a GitHub issue!
|
||||
|
||||
The `ExtensionManager` is a class made available to us via the `@ohif/core`
|
||||
project (platform/core). Our application instantiates a single instance of it,
|
||||
and provides a `ServicesManager` and `CommandsManager`.
|
||||
and provides a `ServicesManager` and `CommandsManager` along with the application's
|
||||
configuration through the appConfig key (optional).
|
||||
|
||||
```js
|
||||
const commandsManager = new CommandsManager();
|
||||
@@ -201,6 +202,7 @@ const servicesManager = new ServicesManager();
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
appConfig
|
||||
});
|
||||
```
|
||||
|
||||
@@ -210,8 +212,8 @@ The `ExtensionManager` only has a few public members:
|
||||
- `registerExtensions` - Registers an array of extensions
|
||||
- `modules` - An object containing registered extensions by `MODULE_TYPE`
|
||||
|
||||
During registration, lifecycle hooks and modules have access to
|
||||
`ExtensionManager`'s `ServicesManager` and `CommandsManager`.
|
||||
During registration, lifecycle hooks and modules have access to the extension's config,
|
||||
the application's config and `ExtensionManager`'s `ServicesManager` and `CommandsManager` instances.
|
||||
|
||||
Our `@ohif/viewer` uses the `modules` member to access registered extensions at
|
||||
appropriate places in our application.
|
||||
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
from: 'right',
|
||||
// The target component to toggle open/close
|
||||
target: 'target-component-id',
|
||||
// UI Hint; If the target panel is in a "disabled" state
|
||||
isDisabled: studies => {
|
||||
return false;
|
||||
},
|
||||
// Overrides `defaultContext`, if specified
|
||||
context: ['ACTIVE_VIEWPORT:MAGIC'],
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ An extension can register a SOP Class Handler Module by defining a
|
||||
the other modules, as it doesn't provide a `1:1` schema for UI or provide it's
|
||||
own components. It instead defines:
|
||||
|
||||
- `sopClassUids`: an array of string SOP Class UIDs that the
|
||||
- `sopClassUIDs`: an array of string SOP Class UIDs that the
|
||||
`getDisplaySetFromSeries` method should be applied to.
|
||||
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
|
||||
display set
|
||||
@@ -15,13 +15,13 @@ A `displaySet` has the following shape:
|
||||
```js
|
||||
return {
|
||||
plugin: 'html',
|
||||
modality: 'SR',
|
||||
displaySetInstanceUid: 0,
|
||||
Modality: 'SR',
|
||||
displaySetInstanceUID: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
```
|
||||
@@ -50,7 +50,7 @@ export default {
|
||||
getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUids: Object.values(SOP_CLASS_UIDS),
|
||||
sopClassUIDs: Object.values(SOP_CLASS_UIDS),
|
||||
|
||||
/**
|
||||
* @param {object} series -
|
||||
@@ -63,12 +63,12 @@ getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: 0,
|
||||
displaySetInstanceUID: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ like user preferences and previous query paramaters. Because of this, the Viewer
|
||||
has been built to be highly configurable to work with almost any web accessible
|
||||
data source.
|
||||
|
||||

|
||||

|
||||
|
||||
To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files.
|
||||
These can be delivered to your end users however you would like:
|
||||
|
||||
@@ -43,7 +43,7 @@ translate to in practice?
|
||||
|
||||
```js
|
||||
// In the application
|
||||
const UINotificationService = createUINotificationService();
|
||||
import UINotificationService from '@ohif/core';
|
||||
const servicesManager = new ServicesManager();
|
||||
|
||||
servicesManager.registerService(UINotificationService);
|
||||
|
||||
@@ -1,3 +1,67 @@
|
||||
# Viewer: Configuration
|
||||
|
||||
...
|
||||
We maintain a number of common viewer application configurations at
|
||||
[`<root>/platform/viewer/public/configs`][config-dir]. How these values are
|
||||
passed to the viewer depend on how it's deployed, but the two most common paths
|
||||
are:
|
||||
|
||||
- `index.html` looks for `https://your-website.com/app-config.js` OR
|
||||
- `index.html` passes the values to `OHIF.installViewer()`
|
||||
|
||||
```js
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
whiteLabelling: {},
|
||||
extensions: [],
|
||||
showStudyList: true,
|
||||
filterQueryParam: false,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
qidoSupportsIncludeField: true,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
// Supported Keys: https://craig.is/killing/mice
|
||||
hotkeys: [
|
||||
{ commandName: 'rotateViewportCW', label: 'Rotate Right', keys: ['r'] },
|
||||
{ commandName: 'rotateViewportCCW', label: 'Rotate Left', keys: ['l'] },
|
||||
{ commandName: 'invertViewport', label: 'Invert', keys: ['i'] },
|
||||
{
|
||||
commandName: 'flipViewportVertical',
|
||||
label: 'Flip Horizontally',
|
||||
keys: ['h'],
|
||||
},
|
||||
{
|
||||
commandName: 'flipViewportHorizontal',
|
||||
label: 'Flip Vertically',
|
||||
keys: ['v'],
|
||||
},
|
||||
],
|
||||
/* Configuration passed to the bundled cornerstone extension
|
||||
*
|
||||
* The cornerstone extension is currently tightly coupled to the platform.
|
||||
* Until we're able to decouple it, this key will serve as a workaround to
|
||||
* pass it configuration.
|
||||
*/
|
||||
cornerstoneExtensionConfig: {
|
||||
/* Whether to show/hide annotation "handles" */
|
||||
hideHandles: true,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[config-dir]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,23 @@
|
||||
# Environment Variables
|
||||
|
||||
There are a number of environment variables we use at build time to influence the output application's behavior.
|
||||
|
||||
```bash
|
||||
# Application
|
||||
NODE_ENV=< production | development >
|
||||
DEBUG=< true | false >
|
||||
APP_CONFIG=< relative path to application configuration file >
|
||||
PUBLIC_URL=<>
|
||||
VERSION_NUMBER=<Set by CircleCI>
|
||||
BUILD_NUM=<Set by CircleCI>
|
||||
# i18n
|
||||
USE_LOCIZE=<false>
|
||||
LOCIZE_PROJECTID=<ProjectID to pull translations for>
|
||||
LOCIZE_API_KEY=<To enable Locize live editing of translations>
|
||||
```
|
||||
|
||||
## Setting Environment Variables
|
||||
|
||||
- `npx cross-env`
|
||||
- `.env` files
|
||||
- env variables on build machine, or for terminal session
|
||||
@@ -1,6 +1,7 @@
|
||||
# Lesion Tracker - Audit Trail
|
||||
|
||||
1. To view Audit Trails, select **View Audit Log** on the Configuration menu.
|
||||
2. To filter audit logs for a column or multiple columns, type or select in the related column or columns.
|
||||
2. To filter audit logs for a column or multiple columns, type or select in the
|
||||
related column or columns.
|
||||
|
||||

|
||||
@@ -2,50 +2,74 @@
|
||||
|
||||

|
||||
|
||||
1. Study List allows to sort each column. To sort studies, click the desired column header.
|
||||
2. Study List is filterable for each column and you can filter by more than one column by typing in the column field. To filter a column, type the text in the desired column header field and press Enter.
|
||||
3. Pagination is provided for the Study List at the bottom of the page. You can change the number of rows per page or the current page.
|
||||
1. Study List allows to sort each column. To sort studies, click the desired
|
||||
column header.
|
||||
2. Study List is filterable for each column and you can filter by more than one
|
||||
column by typing in the column field. To filter a column, type the text in
|
||||
the desired column header field and press Enter.
|
||||
3. Pagination is provided for the Study List at the bottom of the page. You can
|
||||
change the number of rows per page or the current page.
|
||||
|
||||
## Study List Context Menu
|
||||
|
||||
When you right-click on the study row, Study Context Menu will pop up. Study Context Menu includes operations at the study level.
|
||||
When you right-click on the study row, Study Context Menu will pop up. Study
|
||||
Context Menu includes operations at the study level.
|
||||
|
||||
There are a couple of ways to view a study:
|
||||
|
||||
* Simply double-click on the study row.
|
||||
* Select **View** option on the Study Context Menu, then you will be redirected to Viewer page.
|
||||
- Simply double-click on the study row.
|
||||
- Select **View** option on the Study Context Menu, then you will be redirected
|
||||
to Viewer page.
|
||||
|
||||

|
||||
|
||||
### Timepoint Association
|
||||
|
||||
1. To link a study with a timepoint, right-click on the study row and select **Associate** option on the Study Context Menu. You will see Study Association dialog.
|
||||
2. To link multiple studies at the same time, hold Shift key and select studies you would like to associate, then right-click and select Associate on the Study Context Menu
|
||||
1. To link a study with a timepoint, right-click on the study row and select
|
||||
**Associate** option on the Study Context Menu. You will see Study
|
||||
Association dialog.
|
||||
2. To link multiple studies at the same time, hold Shift key and select studies
|
||||
you would like to associate, then right-click and select Associate on the
|
||||
Study Context Menu
|
||||
|
||||

|
||||
|
||||
The Study Association dialog allows you to select and unselect Timepoint Type for the selected study or studies. Also, it will include all studies within 14 days of your selected studies, in case you forgot to select a study. After you identify timepoints for the selected study or studies, click **Save** to save the association.
|
||||
The Study Association dialog allows you to select and unselect Timepoint Type
|
||||
for the selected study or studies. Also, it will include all studies within 14
|
||||
days of your selected studies, in case you forgot to select a study. After you
|
||||
identify timepoints for the selected study or studies, click **Save** to save
|
||||
the association.
|
||||
|
||||

|
||||
|
||||
### Remove Timepoint Association
|
||||
To unlink a timepoint association with a study, right-click on the study row and select **Remove Association** option on the Study Context Menu. Remove Association will be disabled if the study is already dissociated.
|
||||
|
||||
To unlink a timepoint association with a study, right-click on the study row and
|
||||
select **Remove Association** option on the Study Context Menu. Remove
|
||||
Association will be disabled if the study is already dissociated.
|
||||
|
||||

|
||||
|
||||
### View Series Details
|
||||
To view details of related series, right-click on the study row and select **View Series Details** option on the Study Context Menu, then Series Details dialog will pop up. Series Details dialog gives a summary of the selected study.
|
||||
|
||||
To view details of related series, right-click on the study row and select
|
||||
**View Series Details** option on the Study Context Menu, then Series Details
|
||||
dialog will pop up. Series Details dialog gives a summary of the selected study.
|
||||
|
||||

|
||||
|
||||
### Anonymize (Under Development)
|
||||
|
||||
...
|
||||
|
||||
### Send (Under Development)
|
||||
|
||||
...
|
||||
|
||||
### Export (Under Development)
|
||||
|
||||
...
|
||||
|
||||
### Delete (Under Development)
|
||||
|
||||
...
|
||||
@@ -14,13 +14,13 @@ export default {
|
||||
preRegistration({
|
||||
servicesManager = {},
|
||||
commandsManager = {},
|
||||
appConfig = {},
|
||||
configuration = {},
|
||||
}) {},
|
||||
|
||||
/**
|
||||
* MODULE GETTERS
|
||||
*/
|
||||
|
||||
getViewportModule() {
|
||||
return '... react component ...';
|
||||
},
|
||||
@@ -63,7 +63,7 @@ const commandsModule = {
|
||||
*/
|
||||
const sopClassHandlerModule = {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUids: Object.values({
|
||||
sopClassUIDs: Object.values({
|
||||
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
||||
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
||||
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
|
||||
@@ -77,12 +77,12 @@ const sopClassHandlerModule = {
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: 0, //utils.guid(),
|
||||
displaySetInstanceUID: 0, //utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
@@ -97,6 +97,9 @@ const panelModule = {
|
||||
icon: 'th-list',
|
||||
label: 'Segments',
|
||||
target: 'segment-panel',
|
||||
isDisabled: studies => {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
],
|
||||
components: [
|
||||
|
||||
@@ -3,6 +3,129 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.4.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.2...@ohif/extension-cornerstone@2.4.3) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.4.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.1...@ohif/extension-cornerstone@2.4.2) (2020-03-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove Eraser and ROI Window ([6c950a9](https://github.com/OHIF/Viewers/commit/6c950a9669f7fbf3c46e48679fa26ee514824156))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.4.0...@ohif/extension-cornerstone@2.4.1) (2020-03-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bump react-cornerstone-viewport version to address critical issue ([#1473](https://github.com/OHIF/Viewers/issues/1473)) ([ee80e02](https://github.com/OHIF/Viewers/commit/ee80e026610442e94caf5e4e3e4d193220cd0ece))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.1...@ohif/extension-cornerstone@2.4.0) (2020-02-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.3.0...@ohif/extension-cornerstone@2.3.1) (2020-02-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Creating 2 commands to activate zoom tool and also to move between displaySets ([#1446](https://github.com/OHIF/Viewers/issues/1446)) ([06a4af0](https://github.com/OHIF/Viewers/commit/06a4af06faaecf6fa06ccd90cdfa879ee8d53053))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.2...@ohif/extension-cornerstone@2.3.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.1...@ohif/extension-cornerstone@2.2.2) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.0...@ohif/extension-cornerstone@2.2.1) (2019-12-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.1...@ohif/extension-cornerstone@2.2.0) (2019-12-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.1.0...@ohif/extension-cornerstone@2.1.1) (2019-12-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 add WwwcRegionTool to cornerstone tools initialization ([#1302](https://github.com/OHIF/Viewers/issues/1302)) ([d5bf728](https://github.com/OHIF/Viewers/commit/d5bf72851a32dff9fd3fc09332ea5250bc7e6114))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.2...@ohif/extension-cornerstone@2.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.1...@ohif/extension-cornerstone@2.0.2) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.0.0...@ohif/extension-cornerstone@2.0.1) (2019-12-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
@@ -85,6 +85,20 @@ Tools can be configured through extension configuration using the tools key:
|
||||
...
|
||||
```
|
||||
|
||||
## Annotate Tools Configuration
|
||||
|
||||
*We currently support one property for annotation tools.*
|
||||
|
||||
### Hide handles
|
||||
This extension configuration allows you to toggle on/off handle rendering for all annotate tools:
|
||||
|
||||
```js
|
||||
...
|
||||
cornerstoneExtensionConfig: {
|
||||
hideHandles: true,
|
||||
},
|
||||
...
|
||||
|
||||
## Resources
|
||||
|
||||
### Repositories
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("../../babel.config.js");
|
||||
@@ -0,0 +1,13 @@
|
||||
const base = require('../../jest.config.base.js');
|
||||
const pkg = require('./package');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
name: pkg.name,
|
||||
displayName: pkg.name,
|
||||
// rootDir: "../.."
|
||||
// testMatch: [
|
||||
// //`<rootDir>/platform/${pack.name}/**/*.spec.js`
|
||||
// "<rootDir>/platform/viewer/**/*.test.js"
|
||||
// ]
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.0.1",
|
||||
"version": "2.4.3",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -25,16 +25,18 @@
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
"start": "yarn run dev",
|
||||
"test:unit": "jest --watchAll",
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-tools": "4.12.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dcmjs": "^0.10.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -49,7 +51,6 @@
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"query-string": "^6.8.3",
|
||||
"react-cornerstone-viewport": "2.x.x"
|
||||
"react-cornerstone-viewport": "^2.3.6"
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,6 @@ import handleSegmentationStorage from './handleSegmentationStorage.js';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
// Metadata configuration
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
const SOP_CLASSES = {
|
||||
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
};
|
||||
@@ -58,34 +49,34 @@ class OHIFCornerstoneViewport extends Component {
|
||||
* Obtain the CornerstoneTools Stack for the specified display set.
|
||||
*
|
||||
* @param {Object[]} studies
|
||||
* @param {String} studyInstanceUid
|
||||
* @param {String} displaySetInstanceUid
|
||||
* @param {String} [sopInstanceUid]
|
||||
* @param {String} StudyInstanceUID
|
||||
* @param {String} displaySetInstanceUID
|
||||
* @param {String} [SOPInstanceUID]
|
||||
* @param {Number} [frameIndex=1]
|
||||
* @return {Object} CornerstoneTools Stack
|
||||
*/
|
||||
static getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex = 0
|
||||
) {
|
||||
if (!studies || !studies.length) {
|
||||
throw new Error('Studies not provided.');
|
||||
}
|
||||
|
||||
if (!studyInstanceUid) {
|
||||
if (!StudyInstanceUID) {
|
||||
throw new Error('StudyInstanceUID not provided.');
|
||||
}
|
||||
|
||||
if (!displaySetInstanceUid) {
|
||||
if (!displaySetInstanceUID) {
|
||||
throw new Error('StudyInstanceUID not provided.');
|
||||
}
|
||||
|
||||
// Create shortcut to displaySet
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
if (!study) {
|
||||
@@ -93,7 +84,7 @@ class OHIFCornerstoneViewport extends Component {
|
||||
}
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
return set.displaySetInstanceUID === displaySetInstanceUID;
|
||||
});
|
||||
|
||||
if (!displaySet) {
|
||||
@@ -107,17 +98,14 @@ class OHIFCornerstoneViewport extends Component {
|
||||
const stack = Object.assign({}, storedStack);
|
||||
stack.currentImageIdIndex = frameIndex;
|
||||
|
||||
if (sopInstanceUid) {
|
||||
if (SOPInstanceUID) {
|
||||
const index = stack.imageIds.findIndex(imageId => {
|
||||
const sopCommonModule = cornerstone.metaData.get(
|
||||
'sopCommonModule',
|
||||
const imageIdSOPInstanceUID = cornerstone.metaData.get(
|
||||
'SOPInstanceUID',
|
||||
imageId
|
||||
);
|
||||
if (!sopCommonModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
return sopCommonModule.sopInstanceUID === sopInstanceUid;
|
||||
return imageIdSOPInstanceUID === SOPInstanceUID;
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
@@ -134,39 +122,39 @@ class OHIFCornerstoneViewport extends Component {
|
||||
|
||||
getViewportData = async (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) => {
|
||||
let viewportData;
|
||||
|
||||
switch (sopClassUid) {
|
||||
switch (SOPClassUID) {
|
||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||
const specialCaseHandler =
|
||||
specialCaseHandlers[SOP_CLASSES.SEGMENTATION_STORAGE];
|
||||
|
||||
viewportData = await specialCaseHandler(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
break;
|
||||
default:
|
||||
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
viewportData = {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
stack,
|
||||
};
|
||||
|
||||
@@ -179,31 +167,31 @@ class OHIFCornerstoneViewport extends Component {
|
||||
setStateFromProps() {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUids,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
sopClassUIDs,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
} = displaySet;
|
||||
|
||||
if (!studyInstanceUid || !displaySetInstanceUid) {
|
||||
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sopClassUids && sopClassUids.length > 1) {
|
||||
if (sopClassUIDs && sopClassUIDs.length > 1) {
|
||||
console.warn(
|
||||
'More than one SOPClassUid in the same series is not yet supported.'
|
||||
'More than one SOPClassUID in the same series is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
const sopClassUid = sopClassUids && sopClassUids[0];
|
||||
const SOPClassUID = sopClassUIDs && sopClassUIDs[0];
|
||||
|
||||
this.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
).then(viewportData => {
|
||||
this.setState({
|
||||
@@ -221,9 +209,9 @@ class OHIFCornerstoneViewport extends Component {
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUid !==
|
||||
prevDisplaySet.displaySetInstanceUid ||
|
||||
displaySet.sopInstanceUid !== prevDisplaySet.sopInstanceUid ||
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
this.setStateFromProps();
|
||||
|
||||
@@ -2,6 +2,7 @@ import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
import setCornerstoneLayout from './utils/setCornerstoneLayout.js';
|
||||
import { getEnabledElement } from './state';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
const scroll = cornerstoneTools.import('util/scroll');
|
||||
@@ -73,10 +74,6 @@ const commandsModule = ({ servicesManager }) => {
|
||||
}
|
||||
cornerstoneTools.setToolActive(toolName, { mouseButtonMask: 1 });
|
||||
},
|
||||
updateViewportDisplaySet: ({ direction }) => {
|
||||
// TODO
|
||||
console.warn('updateDisplaySet: ', direction);
|
||||
},
|
||||
clearAnnotations: ({ viewports }) => {
|
||||
const element = getEnabledElement(viewports.activeViewportIndex);
|
||||
if (!element) {
|
||||
@@ -242,6 +239,22 @@ const commandsModule = ({ servicesManager }) => {
|
||||
cornerstoneTools.removeToolState(element, toolType, tool);
|
||||
cornerstone.updateImage(element);
|
||||
},
|
||||
setCornerstoneLayout: () => {
|
||||
setCornerstoneLayout();
|
||||
},
|
||||
setWindowLevel: ({ viewports, window, level }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
|
||||
viewport.voi = {
|
||||
windowWidth: Number(window),
|
||||
windowCenter: Number(level),
|
||||
};
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
@@ -330,24 +343,28 @@ const commandsModule = ({ servicesManager }) => {
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
// TODO: First/Last image
|
||||
// Next/Previous series/DisplaySet
|
||||
nextViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: 1 },
|
||||
},
|
||||
previousViewportDisplaySet: {
|
||||
commandFn: actions.updateViewportDisplaySet,
|
||||
storeContexts: [],
|
||||
options: { direction: -1 },
|
||||
},
|
||||
// TOOLS
|
||||
setToolActive: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setZoomTool: {
|
||||
commandFn: actions.setToolActive,
|
||||
storeContexts: [],
|
||||
options: { toolName: 'Zoom' },
|
||||
},
|
||||
setCornerstoneLayout: {
|
||||
commandFn: actions.setCornerstoneLayout,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
setWindowLevel: {
|
||||
commandFn: actions.setWindowLevel,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,25 +6,25 @@ import cornerstoneTools from 'cornerstone-tools';
|
||||
|
||||
const { StackManager, DicomLoaderService } = OHIF.utils;
|
||||
|
||||
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
function getDisplaySet(studies, StudyInstanceUID, displaySetInstanceUID) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
return set.displaySetInstanceUID === displaySetInstanceUID;
|
||||
});
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
function getDisplaySetsBySeries(studies, studyInstanceUid, seriesInstanceUid) {
|
||||
function getDisplaySetsBySeries(studies, StudyInstanceUID, SeriesInstanceUID) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
return study.displaySets.filter(set => {
|
||||
return set.seriesInstanceUid === seriesInstanceUid;
|
||||
return set.SeriesInstanceUID === SeriesInstanceUID;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ function parseSeg(arrayBuffer, imageIds) {
|
||||
function addSegMetadataToCornerstoneToolState(
|
||||
segMetadata,
|
||||
toolState,
|
||||
displaySetInstanceUid
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
|
||||
toolState
|
||||
@@ -48,22 +48,22 @@ function addSegMetadataToCornerstoneToolState(
|
||||
const brushModule = cornerstoneTools.store.modules.brush;
|
||||
|
||||
for (let i = 0; i < segMetadata.length; i++) {
|
||||
brushModule.setters.metadata(displaySetInstanceUid, i, segMetadata[i]);
|
||||
brushModule.setters.metadata(displaySetInstanceUID, i, segMetadata[i]);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSegmentationStorage(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
const displaySet = getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
const arrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||
@@ -77,16 +77,16 @@ async function handleSegmentationStorage(
|
||||
|
||||
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
|
||||
const seriesInstanceUid = dataset.ReferencedSeriesSequence.SeriesInstanceUID;
|
||||
const SeriesInstanceUID = dataset.ReferencedSeriesSequence.SeriesInstanceUID;
|
||||
const displaySets = getDisplaySetsBySeries(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
'More than one display set with the same seriesInstanceUid. This is not supported yet...'
|
||||
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ async function handleSegmentationStorage(
|
||||
stack.currentImageIdIndex = 0;
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
stack,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,29 +1,10 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { SimpleDialog } from '@ohif/ui';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import csTools from 'cornerstone-tools';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
import queryString from 'query-string';
|
||||
import { SimpleDialog } from '@ohif/ui';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
function fallbackMetaDataProvider(type, imageId) {
|
||||
if (!imageId.includes('wado?requestType=WADO')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If you call for an WADO-URI imageId and get no
|
||||
// metadata, try reformatting to WADO-RS imageId
|
||||
const qs = queryString.parse(imageId);
|
||||
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
|
||||
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
|
||||
qs.seriesUID
|
||||
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
||||
|
||||
return cornerstone.metaData.get(type, wadoRsImageId);
|
||||
}
|
||||
|
||||
// Add this fallback provider with a low priority so it is handled last
|
||||
cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -32,9 +13,9 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
const { UIDialogService, MeasurementService } = servicesManager.services;
|
||||
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
if (UIDialogService) {
|
||||
let dialogId = UIDialogService.create({
|
||||
centralize: true,
|
||||
@@ -57,16 +38,13 @@ export default function init({ servicesManager, configuration }) {
|
||||
};
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
const { StackManager } = OHIF.utils;
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
const metadataProvider = OHIF.cornerstone.metadataProvider;
|
||||
|
||||
// ~~ Set our MetadataProvider
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
metadataProvider.get.bind(metadataProvider),
|
||||
9999
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
|
||||
// ~~
|
||||
const defaultCsToolsConfig = csToolsConfig || {
|
||||
globalToolSyncEnabled: true,
|
||||
@@ -76,33 +54,41 @@ export default function init({ servicesManager, configuration }) {
|
||||
|
||||
initCornerstoneTools(defaultCsToolsConfig);
|
||||
|
||||
// ~~ Toooools 🙌
|
||||
const tools = [
|
||||
csTools.PanTool,
|
||||
csTools.ZoomTool,
|
||||
csTools.WwwcTool,
|
||||
csTools.MagnifyTool,
|
||||
csTools.StackScrollTool,
|
||||
csTools.StackScrollMouseWheelTool,
|
||||
// Touch
|
||||
csTools.PanMultiTouchTool,
|
||||
csTools.ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
csTools.ArrowAnnotateTool,
|
||||
csTools.EraserTool,
|
||||
csTools.BidirectionalTool,
|
||||
csTools.LengthTool,
|
||||
csTools.AngleTool,
|
||||
csTools.FreehandRoiTool,
|
||||
csTools.EllipticalRoiTool,
|
||||
csTools.DragProbeTool,
|
||||
csTools.RectangleRoiTool,
|
||||
// Segmentation
|
||||
csTools.BrushTool,
|
||||
];
|
||||
const toolsGroupedByType = {
|
||||
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
|
||||
annotations: [
|
||||
csTools.ArrowAnnotateTool,
|
||||
csTools.BidirectionalTool,
|
||||
csTools.LengthTool,
|
||||
csTools.AngleTool,
|
||||
csTools.FreehandRoiTool,
|
||||
csTools.EllipticalRoiTool,
|
||||
csTools.DragProbeTool,
|
||||
csTools.RectangleRoiTool,
|
||||
],
|
||||
segmentation: [csTools.BrushTool],
|
||||
other: [
|
||||
csTools.PanTool,
|
||||
csTools.ZoomTool,
|
||||
csTools.WwwcTool,
|
||||
csTools.WwwcRegionTool,
|
||||
csTools.MagnifyTool,
|
||||
csTools.StackScrollTool,
|
||||
csTools.StackScrollMouseWheelTool,
|
||||
csTools.OverlayTool,
|
||||
],
|
||||
};
|
||||
|
||||
let tools = [];
|
||||
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
|
||||
tools.push(...toolsGroupedByType[toolsGroup])
|
||||
);
|
||||
|
||||
/* Measurement Service */
|
||||
_connectToolsToMeasurementService(MeasurementService);
|
||||
|
||||
/* Add extension tools configuration here. */
|
||||
const extensionToolsConfiguration = {
|
||||
const internalToolsConfig = {
|
||||
ArrowAnnotate: {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) =>
|
||||
@@ -113,19 +99,60 @@ export default function init({ servicesManager, configuration }) {
|
||||
},
|
||||
};
|
||||
|
||||
const isEmpty = obj => Object.keys(obj).length < 1;
|
||||
if (!isEmpty(configuration.tools) || !isEmpty(extensionToolsConfiguration)) {
|
||||
/* Add tools with its custom props through extension configuration. */
|
||||
tools.forEach(tool => {
|
||||
const toolName = tool.name.replace('Tool', '');
|
||||
const configurationToolProps = configuration.tools[toolName] || {};
|
||||
const extensionToolProps = extensionToolsConfiguration[toolName];
|
||||
let props = merge(extensionToolProps, configurationToolProps);
|
||||
csTools.addTool(tool, props);
|
||||
});
|
||||
} else {
|
||||
tools.forEach(tool => csTools.addTool(tool));
|
||||
}
|
||||
/* Abstract tools configuration using extension configuration. */
|
||||
const parseToolProps = (props, tool) => {
|
||||
const { annotations } = toolsGroupedByType;
|
||||
// An alternative approach would be to remove the `drawHandlesOnHover` config
|
||||
// from the supported configuration properties in `cornerstone-tools`
|
||||
const toolsWithHideableHandles = annotations.filter(
|
||||
tool => !['RectangleRoiTool', 'EllipticalRoiTool'].includes(tool.name)
|
||||
);
|
||||
|
||||
let parsedProps = { ...props };
|
||||
|
||||
/**
|
||||
* drawHandles - Never/Always show handles
|
||||
* drawHandlesOnHover - Only show handles on handle hover (pointNearHandle)
|
||||
*
|
||||
* Does not apply to tools where handles aren't placed in predictable
|
||||
* locations.
|
||||
*/
|
||||
if (
|
||||
configuration.hideHandles !== false &&
|
||||
toolsWithHideableHandles.includes(tool)
|
||||
) {
|
||||
if (props.configuration) {
|
||||
parsedProps.configuration.drawHandlesOnHover = true;
|
||||
} else {
|
||||
parsedProps.configuration = { drawHandlesOnHover: true };
|
||||
}
|
||||
}
|
||||
|
||||
return parsedProps;
|
||||
};
|
||||
|
||||
/* Add tools with its custom props through extension configuration. */
|
||||
tools.forEach(tool => {
|
||||
const toolName = tool.name.replace('Tool', '');
|
||||
const externalToolsConfig = configuration.tools || {};
|
||||
const externalToolProps = externalToolsConfig[toolName] || {};
|
||||
const internalToolProps = internalToolsConfig[toolName] || {};
|
||||
const props = merge(
|
||||
internalToolProps,
|
||||
parseToolProps(externalToolProps, tool)
|
||||
);
|
||||
csTools.addTool(tool, props);
|
||||
});
|
||||
|
||||
// TODO -> We need a better way to do this with maybe global tool state setting all tools passive.
|
||||
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
|
||||
tools.forEach(tool => {
|
||||
if (tool.prototype instanceof BaseAnnotationTool) {
|
||||
// BaseAnnotationTool would likely come from csTools lib exports
|
||||
const toolName = new tool().name;
|
||||
csTools.setToolPassive(toolName); // there may be a better place to determine name; may not be on uninstantiated class
|
||||
}
|
||||
});
|
||||
|
||||
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
|
||||
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||
@@ -133,4 +160,118 @@ export default function init({ servicesManager, configuration }) {
|
||||
csTools.setToolActive('StackScrollMouseWheel', {}); // TODO: Empty options should not be required
|
||||
csTools.setToolActive('PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
|
||||
csTools.setToolActive('ZoomTouchPinch', {});
|
||||
csTools.setToolEnabled('Overlay', {});
|
||||
}
|
||||
|
||||
const _initMeasurementService = measurementService => {
|
||||
/* Initialization */
|
||||
const { toAnnotation, toMeasurement } = measurementServiceMappingsFactory(
|
||||
measurementService
|
||||
);
|
||||
const csToolsVer4MeasurementSource = measurementService.createSource(
|
||||
'CornerstoneTools',
|
||||
'4'
|
||||
);
|
||||
|
||||
/* Matching Criterias */
|
||||
const matchingCriteria = {
|
||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
};
|
||||
|
||||
/* Mappings */
|
||||
measurementService.addMapping(
|
||||
csToolsVer4MeasurementSource,
|
||||
'Length',
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
return csToolsVer4MeasurementSource;
|
||||
};
|
||||
|
||||
const _connectToolsToMeasurementService = measurementService => {
|
||||
const csToolsVer4MeasurementSource = _initMeasurementService(
|
||||
measurementService
|
||||
);
|
||||
const {
|
||||
id: sourceId,
|
||||
addOrUpdate,
|
||||
getAnnotation,
|
||||
} = csToolsVer4MeasurementSource;
|
||||
|
||||
/* Measurement Service Events */
|
||||
cornerstone.events.addEventListener(
|
||||
cornerstone.EVENTS.ELEMENT_ENABLED,
|
||||
event => {
|
||||
const {
|
||||
MEASUREMENT_ADDED,
|
||||
MEASUREMENT_UPDATED,
|
||||
} = measurementService.EVENTS;
|
||||
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_ADDED,
|
||||
({ source, measurement }) => {
|
||||
if (![sourceId].includes(source.id)) {
|
||||
const annotation = getAnnotation('Length', measurement.id);
|
||||
|
||||
console.log(
|
||||
'Measurement Service [Cornerstone]: Measurement added',
|
||||
measurement
|
||||
);
|
||||
console.log('Mapped annotation:', annotation);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_UPDATED,
|
||||
({ source, measurement }) => {
|
||||
if (![sourceId].includes(source.id)) {
|
||||
const annotation = getAnnotation('Length', measurement.id);
|
||||
|
||||
console.log(
|
||||
'Measurement Service [Cornerstone]: Measurement updated',
|
||||
measurement
|
||||
);
|
||||
console.log('Mapped annotation:', annotation);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const addOrUpdateMeasurement = csToolsAnnotation => {
|
||||
try {
|
||||
const { toolName, toolType, measurementData } = csToolsAnnotation;
|
||||
const csTool = toolName || measurementData.toolType || toolType;
|
||||
csToolsAnnotation.id = measurementData._measurementServiceId;
|
||||
const measurementServiceId = addOrUpdate(csTool, csToolsAnnotation);
|
||||
|
||||
if (!measurementData._measurementServiceId) {
|
||||
addMeasurementServiceId(measurementServiceId, csToolsAnnotation);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to add or update measurement:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const addMeasurementServiceId = (id, csToolsAnnotation) => {
|
||||
const { measurementData } = csToolsAnnotation;
|
||||
Object.assign(measurementData, { _measurementServiceId: id });
|
||||
};
|
||||
|
||||
[
|
||||
csTools.EVENTS.MEASUREMENT_ADDED,
|
||||
csTools.EVENTS.MEASUREMENT_MODIFIED,
|
||||
].forEach(csToolsEvtName => {
|
||||
event.detail.element.addEventListener(
|
||||
csToolsEvtName,
|
||||
({ detail: csToolsAnnotation }) => {
|
||||
console.log(`Cornerstone Element Event: ${csToolsEvtName}`);
|
||||
addOrUpdateMeasurement(csToolsAnnotation);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -215,15 +215,6 @@ const definitions = [
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Bidirectional' },
|
||||
},
|
||||
{
|
||||
id: 'Eraser',
|
||||
label: 'Eraser',
|
||||
icon: 'eraser',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'Eraser' },
|
||||
},
|
||||
{
|
||||
id: 'Download',
|
||||
label: 'Download',
|
||||
@@ -237,6 +228,15 @@ const definitions = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'Exit2DMPR',
|
||||
label: 'Exit 2D MPR',
|
||||
icon: 'times',
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'setCornerstoneLayout',
|
||||
context: 'ACTIVE_VIEWPORT::VTK',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
const SUPPORTED_TOOLS = [
|
||||
'Length',
|
||||
'EllipticalRoi',
|
||||
'RectangleRoi',
|
||||
'ArrowAnnotate',
|
||||
];
|
||||
|
||||
const measurementServiceMappingsFactory = measurementService => {
|
||||
/**
|
||||
* Maps measurement service format object to cornerstone annotation object.
|
||||
*
|
||||
* @param {Measurement} measurement The measurement instance
|
||||
* @param {string} definition The source definition
|
||||
* @return {Object} Cornerstone annotation data
|
||||
*/
|
||||
const toAnnotation = (measurement, definition) => {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
points,
|
||||
unit,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID,
|
||||
} = measurement;
|
||||
|
||||
return {
|
||||
toolName: definition,
|
||||
measurementData: {
|
||||
sopInstanceUid: SOPInstanceUID,
|
||||
frameOfReferenceUID: FrameOfReferenceUID,
|
||||
SeriesInstanceUID: referenceSeriesUID,
|
||||
unit,
|
||||
text: label,
|
||||
description,
|
||||
handles: _getHandlesFromPoints(points),
|
||||
_measurementServiceId: id,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
const toMeasurement = csToolsAnnotation => {
|
||||
const { element, measurementData } = csToolsAnnotation;
|
||||
const tool =
|
||||
csToolsAnnotation.toolType ||
|
||||
csToolsAnnotation.toolName ||
|
||||
measurementData.toolType;
|
||||
|
||||
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType(tool)) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const {
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SeriesInstanceUID,
|
||||
} = _getAttributes(element);
|
||||
|
||||
const points = [];
|
||||
points.push(measurementData.handles);
|
||||
|
||||
return {
|
||||
id: measurementData._measurementServiceId,
|
||||
SOPInstanceUID: SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
label: measurementData.text,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
area:
|
||||
measurementData.cachedStats &&
|
||||
measurementData.cachedStats
|
||||
.area /* TODO: Add concept names instead (descriptor) */,
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points: _getPointsFromHandles(measurementData.handles),
|
||||
};
|
||||
};
|
||||
|
||||
const _getAttributes = element => {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const imageId = enabledElement.image.imageId;
|
||||
const instance = cornerstone.metaData.get('instance', imageId);
|
||||
|
||||
return {
|
||||
SOPInstanceUID: instance.SOPInstanceUID,
|
||||
FrameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
||||
};
|
||||
};
|
||||
|
||||
const _getValueTypeFromToolType = toolType => {
|
||||
const { POLYLINE, ELLIPSE, POINT } = measurementService.VALUE_TYPES;
|
||||
|
||||
/* TODO: Relocate static value types */
|
||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||
Length: POLYLINE,
|
||||
EllipticalRoi: ELLIPSE,
|
||||
RectangleRoi: POLYLINE,
|
||||
ArrowAnnotate: POINT,
|
||||
};
|
||||
|
||||
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
|
||||
};
|
||||
|
||||
const _getPointsFromHandles = handles => {
|
||||
let points = [];
|
||||
Object.keys(handles).map(handle => {
|
||||
if (['start', 'end'].includes(handle)) {
|
||||
let point = {};
|
||||
if (handles[handle].x) point.x = handles[handle].x;
|
||||
if (handles[handle].y) point.y = handles[handle].y;
|
||||
points.push(point);
|
||||
}
|
||||
});
|
||||
return points;
|
||||
};
|
||||
|
||||
const _getHandlesFromPoints = points => {
|
||||
return points
|
||||
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
|
||||
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
|
||||
};
|
||||
|
||||
return {
|
||||
toAnnotation,
|
||||
toMeasurement,
|
||||
};
|
||||
};
|
||||
|
||||
export default measurementServiceMappingsFactory;
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import measurementServiceMappingsFactory from './measurementServiceMappingsFactory';
|
||||
|
||||
jest.mock('cornerstone-core', () => ({
|
||||
...jest.requireActual('cornerstone-core'),
|
||||
getEnabledElement: () => ({
|
||||
image: { imageId: 123 },
|
||||
}),
|
||||
metaData: {
|
||||
...jest.requireActual('cornerstone-core').metaData,
|
||||
get: () => ({
|
||||
SOPInstanceUID: '123',
|
||||
FrameOfReferenceUID: '123',
|
||||
SeriesInstanceUID: '123',
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('measurementServiceMappings.js', () => {
|
||||
let mappings;
|
||||
let handles;
|
||||
let points;
|
||||
let csToolsAnnotation;
|
||||
let measurement;
|
||||
let measurementServiceMock;
|
||||
let definition = 'Length';
|
||||
|
||||
beforeEach(() => {
|
||||
measurementServiceMock = {
|
||||
VALUE_TYPES: {
|
||||
POLYLINE: 'value_type::polyline',
|
||||
POINT: 'value_type::point',
|
||||
ELLIPSE: 'value_type::ellipse',
|
||||
MULTIPOINT: 'value_type::multipoint',
|
||||
CIRCLE: 'value_type::circle',
|
||||
},
|
||||
};
|
||||
mappings = measurementServiceMappingsFactory(measurementServiceMock);
|
||||
handles = { start: { x: 1, y: 2 }, end: { x: 1, y: 2 } };
|
||||
points = [{ x: 1, y: 2 }, { x: 1, y: 2 }];
|
||||
csToolsAnnotation = {
|
||||
toolName: definition,
|
||||
measurementData: {
|
||||
_measurementServiceId: 1,
|
||||
sopInstanceUid: '123',
|
||||
frameOfReferenceUID: '123',
|
||||
SeriesInstanceUID: '123',
|
||||
handles,
|
||||
text: 'Test',
|
||||
description: 'Test',
|
||||
unit: 'mm',
|
||||
},
|
||||
};
|
||||
measurement = {
|
||||
id: 1,
|
||||
SOPInstanceUID: '123',
|
||||
FrameOfReferenceUID: '123',
|
||||
referenceSeriesUID: '123',
|
||||
label: 'Test',
|
||||
description: 'Test',
|
||||
unit: 'mm',
|
||||
type: measurementServiceMock.VALUE_TYPES.POLYLINE,
|
||||
points: points,
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('toAnnotation()', () => {
|
||||
it('map measurement service format to annotation', async () => {
|
||||
const mappedMeasurement = await mappings.toAnnotation(
|
||||
measurement,
|
||||
definition
|
||||
);
|
||||
expect(mappedMeasurement).toEqual(csToolsAnnotation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toMeasurement()', () => {
|
||||
it('map annotation to measurement service format', async () => {
|
||||
const mappedAnnotation = await mappings.toMeasurement(csToolsAnnotation);
|
||||
expect(mappedAnnotation).toEqual(measurement);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { redux } from '@ohif/core';
|
||||
|
||||
const { setLayout } = redux.actions;
|
||||
|
||||
/**
|
||||
* Update the current layout with a simple Cornerstone one
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
const setCornerstoneLayout = () => {
|
||||
const layout = {
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
viewports: [{ plugin: 'cornerstone' }],
|
||||
};
|
||||
|
||||
const action = setLayout(layout);
|
||||
|
||||
window.store.dispatch(action);
|
||||
}
|
||||
|
||||
export default setCornerstoneLayout;
|
||||
@@ -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.
|
||||
|
||||
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.1.0...@ohif/extension-dicom-html@1.1.1) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.2...@ohif/extension-dicom-html@1.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.0.2",
|
||||
"version": "1.1.1",
|
||||
"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.6.1",
|
||||
"dcmjs": "^0.10.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -11,26 +11,26 @@ const SOP_CLASS_UIDS = {
|
||||
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
|
||||
};
|
||||
|
||||
const sopClassUids = Object.values(SOP_CLASS_UIDS);
|
||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
// TODO: Handle the case where there is more than one SOP Class Handler for the
|
||||
// same SOP Class
|
||||
const OHIFDicomHtmlSopClassHandler = {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUids,
|
||||
sopClassUIDs,
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
modality: 'SR',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
Modality: 'SR',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.50.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.6...@ohif/extension-dicom-microscopy@0.50.7) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-microscopy
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-microscopy@0.50.5...@ohif/extension-dicom-microscopy@0.50.6) (2019-09-26)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-microscopy",
|
||||
"version": "0.50.6",
|
||||
"version": "0.50.7",
|
||||
"description": "OHIF extension for Dicom Microscopy",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -8,7 +8,7 @@ const SOP_CLASS_UIDS = {
|
||||
|
||||
const DicomMicroscopySopClassHandler = {
|
||||
id: 'DicomMicroscopySopClassHandlerPlugin',
|
||||
sopClassUids: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE],
|
||||
sopClassUIDs: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE],
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
@@ -16,12 +16,12 @@ const DicomMicroscopySopClassHandler = {
|
||||
|
||||
return {
|
||||
plugin: 'microscopy',
|
||||
modality: 'SM',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
Modality: 'SM',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
dicomWebClient,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,8 +25,8 @@ class DicomMicroscopyViewport extends Component {
|
||||
const dicomWebClient = displaySet.dicomWebClient;
|
||||
|
||||
const searchInstanceOptions = {
|
||||
studyInstanceUID: displaySet.studyInstanceUid,
|
||||
seriesInstanceUID: displaySet.seriesInstanceUid,
|
||||
studyInstanceUID: displaySet.StudyInstanceUID,
|
||||
seriesInstanceUID: displaySet.SeriesInstanceUID,
|
||||
};
|
||||
|
||||
dicomWebClient
|
||||
@@ -34,18 +34,18 @@ class DicomMicroscopyViewport extends Component {
|
||||
.then(instances => {
|
||||
const promises = [];
|
||||
for (let i = 0; i < instances.length; i++) {
|
||||
const sopInstanceUID = instances[i]['00080018']['Value'][0];
|
||||
const sopInstanceUid = instances[i]['00080018']['Value'][0];
|
||||
const retrieveInstanceOptions = {
|
||||
studyInstanceUID: displaySet.studyInstanceUid,
|
||||
seriesInstanceUID: displaySet.seriesInstanceUid,
|
||||
sopInstanceUID,
|
||||
studyInstanceUID: displaySet.StudyInstanceUID,
|
||||
seriesInstanceUID: displaySet.SeriesInstanceUID,
|
||||
sopInstanceUid,
|
||||
};
|
||||
|
||||
const promise = dicomWebClient
|
||||
.retrieveInstanceMetadata(retrieveInstanceOptions)
|
||||
.then(metadata => {
|
||||
const imageType = metadata[0]['00080008']['Value'];
|
||||
if (imageType[2] === 'VOLUME') {
|
||||
const ImageType = metadata[0]['00080008']['Value'];
|
||||
if (ImageType[2] === 'VOLUME') {
|
||||
return metadata[0];
|
||||
}
|
||||
});
|
||||
@@ -56,13 +56,15 @@ class DicomMicroscopyViewport extends Component {
|
||||
.then(async metadata => {
|
||||
metadata = metadata.filter(m => m);
|
||||
|
||||
const { api } = await import(/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer');
|
||||
const { api } = await import(
|
||||
/* webpackChunkName: "dicom-microscopy-viewer" */ 'dicom-microscopy-viewer'
|
||||
);
|
||||
const microscopyViewer = api.VLWholeSlideMicroscopyImageViewer;
|
||||
|
||||
this.viewer = new microscopyViewer({
|
||||
client: dicomWebClient,
|
||||
metadata,
|
||||
retrieveRendered: false
|
||||
retrieveRendered: false,
|
||||
});
|
||||
|
||||
this.viewer.render({ container });
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.1...@ohif/extension-dicom-pdf@1.0.2) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-pdf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@1.0.0...@ohif/extension-dicom-pdf@1.0.1) (2019-12-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Update ohif pdf extension to optionally use pdfjs ([#1162](https://github.com/OHIF/Viewers/issues/1162)) ([31e542d](https://github.com/OHIF/Viewers/commit/31e542dfd973c3e982ede6d02d79bfe65175e792)), closes [#1049](https://github.com/OHIF/Viewers/issues/1049)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-pdf@0.50.4...@ohif/extension-dicom-pdf@1.0.0) (2019-09-27)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-pdf",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"description": "OHIF extension for Dicom PDF",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -37,7 +37,8 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"classnames": "^2.2.6",
|
||||
"lodash.isequal": "^4.5.0"
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"pdfjs-dist": "^2.2.228"
|
||||
},
|
||||
"gitHead": "a5baa9228c0eda0df880136bde4420d78e6f8706"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { connect } from 'react-redux';
|
||||
import OHIFDicomPDFViewport from './OHIFDicomPDFViewport';
|
||||
|
||||
const { setViewportActive } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { activeViewportIndex } = state.viewports;
|
||||
return { activeViewportIndex };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
const { viewportIndex } = ownProps;
|
||||
|
||||
return {
|
||||
setViewportActive: () => {
|
||||
dispatch(setViewportActive(viewportIndex));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedOHIFDicomPDFViewer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(OHIFDicomPDFViewport);
|
||||
|
||||
export default ConnectedOHIFDicomPDFViewer;
|
||||
@@ -0,0 +1,62 @@
|
||||
.DicomPDFViewport {
|
||||
--header-height: 50px;
|
||||
}
|
||||
|
||||
.DicomPDFViewport #toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
padding: 0.5em;
|
||||
border-bottom: var(--ui-border-thickness) solid var(--ui-border-color);
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
.DicomPDFViewport #canvas {
|
||||
height: calc(100% - var(--header-height));
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.DicomPDFViewport #text-layer {
|
||||
height: 100%;
|
||||
margin-top: var(--header-height);
|
||||
}
|
||||
|
||||
.DicomPDFViewport #pdf-canvas-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.DicomPDFViewport canvas, #text-layer {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.DicomPDFViewport #toolbar button {
|
||||
color: currentColor;
|
||||
background-color: transparent;
|
||||
font: inherit;
|
||||
border: var(--ui-border-thickness) solid var(--ui-border-color);
|
||||
border-radius: 3px;
|
||||
padding: 0.25em 0.5em;
|
||||
margin-right: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.DicomPDFViewport #text-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0.2;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.DicomPDFViewport #text-layer>span {
|
||||
color: transparent;
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
cursor: text;
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, createRef } from 'react';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import PDFJS from 'pdfjs-dist';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import TypedArrayProp from './TypedArrayProp';
|
||||
import './DicomPDFViewport.css';
|
||||
|
||||
import pdfjsBuild from 'pdfjs-dist/build/pdf';
|
||||
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
|
||||
pdfjsBuild.GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||
|
||||
// TODO: Should probably use dcmjs for this
|
||||
const SOP_CLASS_UIDS = {
|
||||
@@ -8,22 +16,94 @@ const SOP_CLASS_UIDS = {
|
||||
};
|
||||
|
||||
class DicomPDFViewport extends Component {
|
||||
state = {
|
||||
fileURL: null,
|
||||
error: null,
|
||||
};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
fileURL: null,
|
||||
error: null,
|
||||
currentPageIndex: 1,
|
||||
pdf: null,
|
||||
scale: 1,
|
||||
};
|
||||
|
||||
this.canvas = createRef();
|
||||
this.textLayer = createRef();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
byteArray: TypedArrayProp.uint8,
|
||||
useNative: PropTypes.bool,
|
||||
viewportData: PropTypes.object,
|
||||
activeViewportIndex: PropTypes.number,
|
||||
setViewportActive: PropTypes.func,
|
||||
viewportIndex: PropTypes.number,
|
||||
};
|
||||
|
||||
renderPDF = (dataSet, byteArray) => {
|
||||
static defaultProps = {
|
||||
useNative: false,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
const dataSet = this.parseByteArray(this.props.byteArray);
|
||||
const fileURL = this.getPDFFileUrl(dataSet, this.props.byteArray);
|
||||
|
||||
this.setState(state => ({ ...state, fileURL }));
|
||||
|
||||
if (!this.props.useNative) {
|
||||
const pdf = await PDFJS.getDocument(fileURL).promise;
|
||||
this.setState(state => ({ ...state, pdf }), () => this.updatePDFCanvas());
|
||||
}
|
||||
}
|
||||
|
||||
updatePDFCanvas = async () => {
|
||||
const { pdf, scale, currentPageIndex } = this.state;
|
||||
const context = this.canvas.getContext('2d');
|
||||
|
||||
const page = await pdf.getPage(currentPageIndex);
|
||||
let viewport = page.getViewport({ scale });
|
||||
|
||||
this.canvas.height = viewport.height;
|
||||
this.canvas.width = viewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
};
|
||||
|
||||
await page.render(renderContext);
|
||||
const textContent = await page.getTextContent();
|
||||
|
||||
this.textLayer.innerHTML = '';
|
||||
this.textLayer.style.height = viewport.height + 'px';
|
||||
this.textLayer.style.width = viewport.width + 'px';
|
||||
|
||||
PDFJS.renderTextLayer({
|
||||
textContent,
|
||||
container: this.textLayer,
|
||||
viewport,
|
||||
textDivs: [],
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { currentPageIndex, scale } = this.state;
|
||||
const newValidScale = prevState.scale !== scale && scale > 0;
|
||||
const newValidPageNumber =
|
||||
prevState.currentPageIndex !== currentPageIndex && currentPageIndex > 0;
|
||||
|
||||
if (newValidScale || newValidPageNumber) {
|
||||
this.updatePDFCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
getPDFFileUrl = (dataSet, byteArray) => {
|
||||
let pdfByteArray = byteArray;
|
||||
|
||||
if (dataSet) {
|
||||
const sopClassUid = dataSet.string('x00080016');
|
||||
const SOPClassUID = dataSet.string('x00080016');
|
||||
|
||||
if (sopClassUid !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
|
||||
if (SOPClassUID !== SOP_CLASS_UIDS.ENCAPSULATED_PDF) {
|
||||
throw new Error('This is not a DICOM-encapsulated PDF');
|
||||
}
|
||||
|
||||
@@ -36,50 +116,144 @@ class DicomPDFViewport extends Component {
|
||||
const PDF = new Blob([pdfByteArray], { type: 'application/pdf' });
|
||||
const fileURL = URL.createObjectURL(PDF);
|
||||
|
||||
this.setState({
|
||||
fileURL,
|
||||
});
|
||||
return fileURL;
|
||||
};
|
||||
|
||||
onPageChange = async event => {
|
||||
const { currentPageIndex, pdf } = this.state;
|
||||
let newPageIndex = currentPageIndex;
|
||||
|
||||
const action = event.target.getAttribute('data-pager');
|
||||
if (action === 'prev') {
|
||||
if (currentPageIndex === 1) {
|
||||
return;
|
||||
}
|
||||
newPageIndex -= 1;
|
||||
if (currentPageIndex < 0) {
|
||||
newPageIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'next') {
|
||||
if (currentPageIndex === pdf.numPages - 1) {
|
||||
return;
|
||||
}
|
||||
newPageIndex += 1;
|
||||
if (currentPageIndex > pdf.numPages - 1) {
|
||||
newPageIndex = pdf.numPages - 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState(state => ({ ...state, currentPageIndex: newPageIndex }));
|
||||
};
|
||||
|
||||
onZoomChange = () => {
|
||||
let newZoomValue = this.state.scale;
|
||||
|
||||
const action = event.target.getAttribute('data-pager');
|
||||
|
||||
if (action === '+') {
|
||||
newZoomValue += 0.25;
|
||||
}
|
||||
|
||||
if (action === '-') {
|
||||
newZoomValue -= 0.25;
|
||||
}
|
||||
|
||||
this.setState(state => ({ ...state, scale: newZoomValue }));
|
||||
};
|
||||
|
||||
parseByteArray = byteArray => {
|
||||
const options = {
|
||||
untilTag: '',
|
||||
};
|
||||
const options = { untilTag: '' };
|
||||
|
||||
let dataSet;
|
||||
|
||||
try {
|
||||
dataSet = dicomParser.parseDicom(byteArray, options);
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
error,
|
||||
});
|
||||
this.setState(state => ({ ...state, error }));
|
||||
}
|
||||
|
||||
return dataSet;
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const dataSet = this.parseByteArray(this.props.byteArray);
|
||||
setViewportActiveHandler = () => {
|
||||
const {
|
||||
setViewportActive,
|
||||
viewportIndex,
|
||||
activeViewportIndex,
|
||||
} = this.props;
|
||||
|
||||
this.renderPDF(dataSet, this.props.byteArray);
|
||||
}
|
||||
if (viewportIndex !== activeViewportIndex) {
|
||||
setViewportActive(viewportIndex);
|
||||
}
|
||||
};
|
||||
|
||||
downloadPDFCanvas = () => {
|
||||
const { fileURL } = this.state;
|
||||
const a = document.createElement('a');
|
||||
a.href = fileURL;
|
||||
a.download = fileURL.substr(fileURL.lastIndexOf('/') + 1);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { fileURL, pdf, error } = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'DicomPDFViewport'}
|
||||
onClick={this.setViewportActiveHandler}
|
||||
onScroll={this.setViewportActiveHandler}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
{this.state.fileURL && (
|
||||
{!this.props.useNative ? (
|
||||
<>
|
||||
<div id="toolbar">
|
||||
<div id="pager">
|
||||
{pdf && pdf.numPages > 1 && (
|
||||
<>
|
||||
<button data-pager="prev" onClick={this.onPageChange}>
|
||||
{`<`}
|
||||
</button>
|
||||
<button data-pager="next" onClick={this.onPageChange}>
|
||||
{`>`}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button data-pager="-" onClick={this.onZoomChange}>
|
||||
{`-`}
|
||||
</button>
|
||||
<button data-pager="+" onClick={this.onZoomChange}>
|
||||
{`+`}
|
||||
</button>
|
||||
<button onClick={this.downloadPDFCanvas}>Download</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="canvas">
|
||||
<div id="pdf-canvas-container">
|
||||
<canvas
|
||||
id="pdf-canvas"
|
||||
ref={canvas => (this.canvas = canvas)}
|
||||
/>
|
||||
<div
|
||||
id="text-layer"
|
||||
ref={textLayer => (this.textLayer = textLayer)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<object
|
||||
data={this.state.fileURL}
|
||||
aria-label="PDF Viewer"
|
||||
data={fileURL}
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
)}
|
||||
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
|
||||
{error && <h2>{JSON.stringify(error)}</h2>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,19 +8,19 @@ const SOP_CLASS_UIDS = {
|
||||
const OHIFDicomPDFSopClassHandler = {
|
||||
id: 'OHIFDicomPDFSopClassHandlerPlugin',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUids: [SOP_CLASS_UIDS.ENCAPSULATED_PDF],
|
||||
sopClassUIDs: [SOP_CLASS_UIDS.ENCAPSULATED_PDF],
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return {
|
||||
plugin: 'pdf',
|
||||
modality: 'DOC',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
Modality: 'DOC',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
SOPInstanceUID: instance.getSOPInstanceUID(),
|
||||
SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
authorizationHeaders: authorizationHeaders,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -11,6 +11,9 @@ class OHIFDicomPDFViewport extends Component {
|
||||
studies: PropTypes.object,
|
||||
displaySet: PropTypes.object,
|
||||
viewportIndex: PropTypes.number,
|
||||
viewportData: PropTypes.object,
|
||||
activeViewportIndex: PropTypes.number,
|
||||
setViewportActive: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
@@ -30,42 +33,37 @@ class OHIFDicomPDFViewport extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { displaySet, studies } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
authorizationHeaders,
|
||||
} = displaySet;
|
||||
|
||||
DicomLoaderService.findDicomDataPromise(displaySet, studies).then(
|
||||
data => {
|
||||
const byteArray = new Uint8Array(data);
|
||||
this.setState({
|
||||
byteArray: byteArray,
|
||||
});
|
||||
},
|
||||
data => this.setState({ byteArray: new Uint8Array(data) }),
|
||||
error => {
|
||||
this.setState({
|
||||
error,
|
||||
});
|
||||
|
||||
this.setState({ error });
|
||||
throw new Error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
setViewportActive,
|
||||
viewportIndex,
|
||||
activeViewportIndex,
|
||||
} = this.props;
|
||||
const { byteArray, error } = this.state;
|
||||
const { id, init, destroy } = OHIFDicomPDFViewport;
|
||||
const pluginProps = { id, init, destroy };
|
||||
|
||||
return (
|
||||
<OHIFComponentPlugin {...pluginProps}>
|
||||
{this.state.byteArray && (
|
||||
<DicomPDFViewport byteArray={this.state.byteArray} />
|
||||
{byteArray && (
|
||||
<DicomPDFViewport
|
||||
byteArray={byteArray}
|
||||
setViewportActive={setViewportActive}
|
||||
viewportIndex={viewportIndex}
|
||||
activeViewportIndex={activeViewportIndex}
|
||||
/>
|
||||
)}
|
||||
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
|
||||
{error && <h2>{JSON.stringify(error)}</h2>}
|
||||
</OHIFComponentPlugin>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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,16 +1,21 @@
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import OHIFDicomPDFSopClassHandler from './OHIFDicomPDFSopClassHandler.js';
|
||||
import OHIFDicomPDFViewport from './OHIFDicomPDFViewport.js';
|
||||
|
||||
const ConnectedOHIFDicomPDFViewer = asyncComponent(() =>
|
||||
import(
|
||||
/* webpackChunkName: "ConnectedOHIFDicomPDFViewer" */ './ConnectedOHIFDicomPDFViewer'
|
||||
)
|
||||
);
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'pdf',
|
||||
|
||||
getViewportModule() {
|
||||
return OHIFDicomPDFViewport;
|
||||
return ConnectedOHIFDicomPDFViewer;
|
||||
},
|
||||
getSopClassHandlerModule() {
|
||||
return OHIFDicomPDFSopClassHandler;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const merge = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './..');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'OHIFExtLesionTracker',
|
||||
libraryTarget: 'umd',
|
||||
libraryExport: 'default',
|
||||
filename: pkg.main,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-lesion-tracker@0.1.0...@ohif/extension-lesion-tracker@0.2.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Lesion tracker right panel ([#1428](https://github.com/OHIF/Viewers/issues/1428)) ([98a649b](https://github.com/OHIF/Viewers/commit/98a649b455ffc712938fc5035cdef40695e58440))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 0.1.0 (2020-02-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* lesion-tracker extension ([#1420](https://github.com/OHIF/Viewers/issues/1420)) ([73e4409](https://github.com/OHIF/Viewers/commit/73e440968ce4699d081a9c9f2d21dd68095b3056))
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Open Health Imaging Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,38 @@
|
||||
# @ohif/extension-lesion-tracker
|
||||
|
||||
This project is an OHIF extension that can be used with the Core OHIF Platform,
|
||||
either at runtime, or as an ES6 dependency, to create a medical image viewing
|
||||
application similar to that of the [legacy Lesion
|
||||
Tracker][legacy-lesion-tracker] viewer.
|
||||
|
||||
## About
|
||||
|
||||
...
|
||||
|
||||
## Scope
|
||||
|
||||
...
|
||||
|
||||
### Configuration
|
||||
|
||||
...
|
||||
|
||||
### Extensions
|
||||
|
||||
...
|
||||
|
||||
## Build & Deploy
|
||||
|
||||
...
|
||||
|
||||
## Funding/Support
|
||||
|
||||
...
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[legacy-lesion-tracker]: http://lesiontracker.ohif.org/studylist
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@ohif/extension-lesion-tracker",
|
||||
"version": "0.2.0",
|
||||
"description": "OHIF extension for Lesion Tracker",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||
"dev:lesion-tracker": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"classnames": "^2.2.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
.MeasurementComparisonTable {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .displayTexts {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .displayTexts .measurementDisplayText {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .warning-status {
|
||||
left: 0;
|
||||
top: 3px;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .warning-status .warning-border {
|
||||
padding: 2px 3px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .warning-status svg {
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .measurementTableHeaderItem {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .generate-report {
|
||||
background-color: #151a1f;
|
||||
margin-top: 2px;
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .generate-report button {
|
||||
font-size: 12px;
|
||||
color: black;
|
||||
background-color: var(--active-color);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MeasurementTable } from '@ohif/ui';
|
||||
|
||||
import './MeasurementComparisonTable.css';
|
||||
|
||||
const overallWarnings = {
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
};
|
||||
|
||||
const measurements = [
|
||||
{
|
||||
measurementId: '125',
|
||||
measurementNumber: '125',
|
||||
itemNumber: 1,
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '12.5 x 4.6' }],
|
||||
},
|
||||
{
|
||||
measurementId: '124',
|
||||
measurementNumber: '124',
|
||||
itemNumber: 2,
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '32.5 x 1.6' }],
|
||||
},
|
||||
{
|
||||
measurementId: '123',
|
||||
measurementNumber: '123',
|
||||
itemNumber: 3,
|
||||
hasWarnings: true,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '5.5 x 9.2' }],
|
||||
},
|
||||
];
|
||||
|
||||
const additionalFindings = [
|
||||
{
|
||||
measurementId: '122',
|
||||
measurementNumber: '122',
|
||||
itemNumber: 1,
|
||||
hasWarnings: true,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '23.5 x 9.2' }],
|
||||
},
|
||||
{
|
||||
measurementId: '121',
|
||||
measurementNumber: '121',
|
||||
itemNumber: 2,
|
||||
hasWarnings: true,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '11.2 x 9.2' }],
|
||||
},
|
||||
{
|
||||
measurementId: '120',
|
||||
measurementNumber: '120',
|
||||
itemNumber: 3,
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '2.9 x 9.2' }],
|
||||
},
|
||||
];
|
||||
|
||||
const currentCollections = [
|
||||
{
|
||||
selectorAction: () => { },
|
||||
maxMeasurements: 3,
|
||||
groupName: 'Measurements',
|
||||
measurements: measurements,
|
||||
},
|
||||
{
|
||||
selectorAction: () => { },
|
||||
groupName: 'Additional Findings',
|
||||
measurements: additionalFindings,
|
||||
},
|
||||
];
|
||||
|
||||
const comparisonColletions = [
|
||||
{
|
||||
selectorAction: () => { },
|
||||
maxMeasurements: 3,
|
||||
groupName: 'Measurements',
|
||||
measurements: measurements,
|
||||
},
|
||||
{
|
||||
selectorAction: () => { },
|
||||
groupName: 'Additional Findings',
|
||||
measurements: additionalFindings,
|
||||
},
|
||||
];
|
||||
|
||||
const comparisonCollections = currentCollections.map((group, index) => {
|
||||
return {
|
||||
...group,
|
||||
measurements: group.measurements.map((measurement, measurementIndex) => {
|
||||
const comparisonCollection = comparisonColletions[index].measurements;
|
||||
if (measurementIndex < comparisonCollection.length) {
|
||||
return {
|
||||
...measurement,
|
||||
data: [
|
||||
...measurement.data,
|
||||
...comparisonCollection[measurementIndex].data,
|
||||
],
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const comparisonTimepoints = [
|
||||
{
|
||||
key: 'Current',
|
||||
date: '10-Apr-18',
|
||||
},
|
||||
{
|
||||
key: 'Comparison',
|
||||
date: '15-Jun-18',
|
||||
}
|
||||
];
|
||||
|
||||
const MeasurementComparisonTable = () => {
|
||||
return (
|
||||
<div className="MeasurementComparisonTable">
|
||||
<MeasurementTable
|
||||
timepoints={comparisonTimepoints}
|
||||
overallWarnings={overallWarnings}
|
||||
measurementCollection={comparisonCollections}
|
||||
onRelabelClick={() => { }}
|
||||
onEditDescriptionClick={() => { }}
|
||||
/>
|
||||
<div className="generate-report">
|
||||
<button className="btn btn-primary">Generate Report</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MeasurementComparisonTable;
|
||||
@@ -0,0 +1,52 @@
|
||||
import MeasurementComparisonTable from './components/MeasurementComparisonTable';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'lesion-tracker',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getPanelModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'th-list',
|
||||
label: 'Measurements',
|
||||
target: 'lesion-tracker-panel',
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'lesion-tracker-panel',
|
||||
component: MeasurementComparisonTable,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER']
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
* @returns Object
|
||||
*/
|
||||
getToolbarModule() {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
* @returns Object
|
||||
*/
|
||||
getCommandsModule() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
@@ -3,6 +3,290 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.24](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.23...@ohif/extension-vtk@1.2.24) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.22...@ohif/extension-vtk@1.2.23) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.22](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.21...@ohif/extension-vtk@1.2.22) (2020-03-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.21](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.20...@ohif/extension-vtk@1.2.21) (2020-02-29)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.20](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.19...@ohif/extension-vtk@1.2.20) (2020-02-21)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.18...@ohif/extension-vtk@1.2.19) (2020-02-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.17...@ohif/extension-vtk@1.2.18) (2020-02-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.16...@ohif/extension-vtk@1.2.17) (2020-02-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.15...@ohif/extension-vtk@1.2.16) (2020-02-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.14...@ohif/extension-vtk@1.2.15) (2020-02-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.13...@ohif/extension-vtk@1.2.14) (2020-02-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.12...@ohif/extension-vtk@1.2.13) (2020-01-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Set VTK viewport as active by interaction ([#1139](https://github.com/OHIF/Viewers/issues/1139)) ([686d12d](https://github.com/OHIF/Viewers/commit/686d12da5c9d3d435b1e326c2a5caee36e2ed27c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.11...@ohif/extension-vtk@1.2.12) (2020-01-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.10...@ohif/extension-vtk@1.2.11) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.9...@ohif/extension-vtk@1.2.10) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.8...@ohif/extension-vtk@1.2.9) (2020-01-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.7...@ohif/extension-vtk@1.2.8) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.6...@ohif/extension-vtk@1.2.7) (2020-01-08)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.5...@ohif/extension-vtk@1.2.6) (2020-01-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.4...@ohif/extension-vtk@1.2.5) (2020-01-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.3...@ohif/extension-vtk@1.2.4) (2019-12-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.2...@ohif/extension-vtk@1.2.3) (2019-12-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.1...@ohif/extension-vtk@1.2.2) (2019-12-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.0...@ohif/extension-vtk@1.2.1) (2019-12-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.7...@ohif/extension-vtk@1.2.0) (2019-12-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.6...@ohif/extension-vtk@1.1.7) (2019-12-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.5...@ohif/extension-vtk@1.1.6) (2019-12-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.4...@ohif/extension-vtk@1.1.5) (2019-12-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.3...@ohif/extension-vtk@1.1.4) (2019-12-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.2...@ohif/extension-vtk@1.1.3) (2019-12-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.1...@ohif/extension-vtk@1.1.2) (2019-12-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.1.0...@ohif/extension-vtk@1.1.1) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.2...@ohif/extension-vtk@1.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.1...@ohif/extension-vtk@1.0.2) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.0.0...@ohif/extension-vtk@1.0.1) (2019-12-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "1.0.1",
|
||||
"version": "1.2.24",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dcmjs": "^0.10.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
@@ -49,14 +49,13 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-vtkjs-viewport": "^0.3.9"
|
||||
"react-vtkjs-viewport": "^0.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^2.0.1",
|
||||
"@ohif/ui": "^1.0.1",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"@ohif/core": "^2.5.3",
|
||||
"@ohif/ui": "^1.3.3",
|
||||
"cornerstone-tools": "4.12.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"gh-pages": "^2.0.1",
|
||||
"i18next": "^17.0.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import { View2D } from 'react-vtkjs-viewport';
|
||||
import { connect } from 'react-redux';
|
||||
import VTKViewport from './VTKViewport';
|
||||
|
||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
||||
|
||||
@@ -18,6 +18,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
const pluginDetails = viewportLayout.vtk || {};
|
||||
|
||||
return {
|
||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
||||
layout: state.viewports.layout,
|
||||
isActive,
|
||||
...pluginDetails,
|
||||
@@ -75,6 +76,6 @@ const ConnectedVTKViewport = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(View2D);
|
||||
)(VTKViewport);
|
||||
|
||||
export default ConnectedVTKViewport;
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
|
||||
|
||||
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||
import LoadingIndicator from './LoadingIndicator.js';
|
||||
import OHIF from '@ohif/core';
|
||||
@@ -13,15 +12,7 @@ import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
|
||||
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
// Metadata configuration
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
|
||||
cornerstone.metaData.addProvider(
|
||||
metadataProvider.provider.bind(metadataProvider)
|
||||
);
|
||||
|
||||
StackManager.setMetadataProvider(metadataProvider);
|
||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||
|
||||
const SOP_CLASSES = {
|
||||
SEGMENTATION_STORAGE: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
@@ -68,15 +59,20 @@ class OHIFVTKViewport extends Component {
|
||||
viewportData: PropTypes.shape({
|
||||
studies: PropTypes.array,
|
||||
displaySet: PropTypes.shape({
|
||||
studyInstanceUid: PropTypes.string,
|
||||
displaySetInstanceUid: PropTypes.string,
|
||||
sopClassUids: PropTypes.arrayOf(PropTypes.string),
|
||||
sopInstanceUid: PropTypes.string,
|
||||
StudyInstanceUID: PropTypes.string,
|
||||
displaySetInstanceUID: PropTypes.string,
|
||||
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
|
||||
SOPInstanceUID: PropTypes.string,
|
||||
frameIndex: PropTypes.number,
|
||||
}),
|
||||
}),
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
onScroll: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onScroll: () => {},
|
||||
};
|
||||
|
||||
static id = 'OHIFVTKViewport';
|
||||
@@ -92,18 +88,18 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
static getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) {
|
||||
// Create shortcut to displaySet
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
return set.displaySetInstanceUID === displaySetInstanceUID;
|
||||
});
|
||||
|
||||
// Get stack from Stack Manager
|
||||
@@ -114,17 +110,14 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
if (frameIndex !== undefined) {
|
||||
stack.currentImageIdIndex = frameIndex;
|
||||
} else if (sopInstanceUid) {
|
||||
} else if (SOPInstanceUID) {
|
||||
const index = stack.imageIds.findIndex(imageId => {
|
||||
const sopCommonModule = cornerstone.metaData.get(
|
||||
'sopCommonModule',
|
||||
const imageIdSOPInstanceUID = cornerstone.metaData.get(
|
||||
'SOPInstanceUID',
|
||||
imageId
|
||||
);
|
||||
if (!sopCommonModule) {
|
||||
return;
|
||||
}
|
||||
|
||||
return sopCommonModule.sopInstanceUID === sopInstanceUid;
|
||||
return imageIdSOPInstanceUID === SOPInstanceUID;
|
||||
});
|
||||
|
||||
if (index > -1) {
|
||||
@@ -139,31 +132,31 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
getViewportData = (
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) => {
|
||||
const stack = OHIFVTKViewport.getCornerstoneStack(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
let imageDataObject;
|
||||
let labelmapDataObject;
|
||||
|
||||
switch (sopClassUid) {
|
||||
switch (SOPClassUID) {
|
||||
case SOP_CLASSES.SEGMENTATION_STORAGE:
|
||||
throw new Error('Not yet implemented');
|
||||
/*
|
||||
const data = handleSegmentationStorage(
|
||||
stack.imageIds,
|
||||
displaySetInstanceUid
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
imageDataObject = data.referenceDataObject;
|
||||
@@ -177,7 +170,7 @@ class OHIFVTKViewport extends Component {
|
||||
});
|
||||
*/
|
||||
default:
|
||||
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
|
||||
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
|
||||
|
||||
return imageDataObject;
|
||||
}
|
||||
@@ -189,25 +182,31 @@ class OHIFVTKViewport extends Component {
|
||||
* @param {object} imageDataObject
|
||||
* @param {object} imageDataObject.vtkImageData
|
||||
* @param {object} imageDataObject.imageMetaData0
|
||||
* @param {number} [imageDataObject.imageMetaData0.windowWidth] - The volume's initial windowWidth
|
||||
* @param {number} [imageDataObject.imageMetaData0.windowCenter] - The volume's initial windowCenter
|
||||
* @param {string} imageDataObject.imageMetaData0.modality - CT, MR, PT, etc
|
||||
* @param {string} displaySetInstanceUid
|
||||
* @param {number} [imageDataObject.imageMetaData0.WindowWidth] - The volume's initial WindowWidth
|
||||
* @param {number} [imageDataObject.imageMetaData0.WindowCenter] - The volume's initial WindowCenter
|
||||
* @param {string} imageDataObject.imageMetaData0.Modality - CT, MR, PT, etc
|
||||
* @param {string} displaySetInstanceUID
|
||||
* @returns vtkVolumeActor
|
||||
* @memberof OHIFVTKViewport
|
||||
*/
|
||||
getOrCreateVolume(imageDataObject, displaySetInstanceUid) {
|
||||
if (volumeCache[displaySetInstanceUid]) {
|
||||
return volumeCache[displaySetInstanceUid];
|
||||
getOrCreateVolume(imageDataObject, displaySetInstanceUID) {
|
||||
if (volumeCache[displaySetInstanceUID]) {
|
||||
return volumeCache[displaySetInstanceUID];
|
||||
}
|
||||
|
||||
const { vtkImageData, imageMetaData0 } = imageDataObject;
|
||||
const { windowWidth, windowCenter, modality } = imageMetaData0;
|
||||
// TODO -> Should update react-vtkjs-viewport and react-cornerstone-viewports
|
||||
// internals to use naturalized DICOM JSON names.
|
||||
const {
|
||||
windowWidth: WindowWidth,
|
||||
windowCenter: WindowCenter,
|
||||
modality: Modality,
|
||||
} = imageMetaData0;
|
||||
|
||||
const { lower, upper } = _getRangeFromWindowLevels(
|
||||
windowWidth,
|
||||
windowCenter,
|
||||
modality
|
||||
WindowWidth,
|
||||
WindowCenter,
|
||||
Modality
|
||||
);
|
||||
const volumeActor = vtkVolume.newInstance();
|
||||
const volumeMapper = vtkVolumeMapper.newInstance();
|
||||
@@ -231,7 +230,7 @@ class OHIFVTKViewport extends Component {
|
||||
// TODO: maybe we should auto adjust samples to 1000.
|
||||
volumeMapper.setMaximumSamplesPerRay(4000);
|
||||
|
||||
volumeCache[displaySetInstanceUid] = volumeActor;
|
||||
volumeCache[displaySetInstanceUID] = volumeActor;
|
||||
|
||||
return volumeActor;
|
||||
}
|
||||
@@ -239,26 +238,26 @@ class OHIFVTKViewport extends Component {
|
||||
setStateFromProps() {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUids,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
sopClassUIDs,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
} = displaySet;
|
||||
|
||||
if (sopClassUids.length > 1) {
|
||||
if (sopClassUIDs.length > 1) {
|
||||
console.warn(
|
||||
'More than one SOPClassUid in the same series is not yet supported.'
|
||||
'More than one SOPClassUID in the same series is not yet supported.'
|
||||
);
|
||||
}
|
||||
|
||||
const sopClassUid = sopClassUids[0];
|
||||
const SOPClassUID = sopClassUIDs[0];
|
||||
const imageDataObject = this.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
sopClassUid,
|
||||
sopInstanceUid,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
@@ -272,7 +271,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
const volumeActor = this.getOrCreateVolume(
|
||||
imageDataObject,
|
||||
displaySetInstanceUid
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
this.setState({
|
||||
@@ -312,9 +311,9 @@ class OHIFVTKViewport extends Component {
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUid !==
|
||||
prevDisplaySet.displaySetInstanceUid ||
|
||||
displaySet.sopInstanceUid !== prevDisplaySet.sopInstanceUid ||
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
this.setStateFromProps();
|
||||
@@ -326,7 +325,7 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
const { isLoading, insertPixelDataPromises } = imageDataObject;
|
||||
|
||||
const numberOfFrames = insertPixelDataPromises.length;
|
||||
const NumberOfFrames = insertPixelDataPromises.length;
|
||||
|
||||
if (!isLoading) {
|
||||
this.setState({ isLoaded: true });
|
||||
@@ -336,7 +335,7 @@ class OHIFVTKViewport extends Component {
|
||||
insertPixelDataPromises.forEach(promise => {
|
||||
promise.then(numberProcessed => {
|
||||
const percentComplete = Math.floor(
|
||||
(numberProcessed * 100) / numberOfFrames
|
||||
(numberProcessed * 100) / NumberOfFrames
|
||||
);
|
||||
|
||||
if (percentComplete !== this.state.percentComplete) {
|
||||
@@ -368,8 +367,6 @@ class OHIFVTKViewport extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={style}>
|
||||
@@ -386,6 +383,7 @@ class OHIFVTKViewport extends Component {
|
||||
this.state.paintFilterBackgroundImageData
|
||||
}
|
||||
viewportIndex={this.props.viewportIndex}
|
||||
onScroll={this.props.onScroll}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -403,21 +401,21 @@ class OHIFVTKViewport extends Component {
|
||||
* @private
|
||||
* @param {number} [width] - the width of our window
|
||||
* @param {number} [center] - the center of our window
|
||||
* @param {string} [modality] - 'PT', 'CT', etc.
|
||||
* @param {string} [Modality] - 'PT', 'CT', etc.
|
||||
* @returns { lower, upper } - range
|
||||
*/
|
||||
function _getRangeFromWindowLevels(width, center, modality = undefined) {
|
||||
function _getRangeFromWindowLevels(width, center, Modality = undefined) {
|
||||
// For PET just set the range to 0-5 SUV
|
||||
if (Modality === 'PT') {
|
||||
return { lower: 0, upper: 5 };
|
||||
}
|
||||
|
||||
const levelsAreNotNumbers = isNaN(center) || isNaN(width);
|
||||
|
||||
if (levelsAreNotNumbers) {
|
||||
return { lower: 0, upper: 512 };
|
||||
}
|
||||
|
||||
// For PET just set the range to 0-5 SUV
|
||||
if (modality === 'PT') {
|
||||
return { lower: 0, upper: 5 };
|
||||
}
|
||||
|
||||
return {
|
||||
lower: center - width / 2.0,
|
||||
upper: center + width / 2.0,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.vtk-viewport-handler svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { View2D } from 'react-vtkjs-viewport';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './VTKViewport.css';
|
||||
|
||||
const VTKViewport = props => {
|
||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||
|
||||
const setViewportActiveHandler = useCallback(() => {
|
||||
const { setViewportActive, viewportIndex, activeViewportIndex } = props;
|
||||
|
||||
if (viewportIndex !== activeViewportIndex) {
|
||||
// set in Connected
|
||||
setViewportActive();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const handleScrollEvent = evt => {
|
||||
const vtkViewportApiReference = props.onScroll(props.viewportIndex) || {};
|
||||
const viewportUID = vtkViewportApiReference.uid;
|
||||
const viewportWasScrolled = viewportUID === evt.detail.uid;
|
||||
|
||||
if (viewportWasScrolled) {
|
||||
setViewportActiveHandler();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('vtkscrollevent', handleScrollEvent);
|
||||
return () =>
|
||||
window.removeEventListener('vtkscrollevent', handleScrollEvent);
|
||||
}, [props, props.onScroll, props.viewportIndex, setViewportActiveHandler]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="vtk-viewport-handler"
|
||||
style={style}
|
||||
onClick={setViewportActiveHandler}
|
||||
>
|
||||
<View2D {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
VTKViewport.propTypes = {
|
||||
setViewportActive: PropTypes.func.isRequired,
|
||||
viewportIndex: PropTypes.number.isRequired,
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
/* Receives viewportIndex */
|
||||
onScroll: PropTypes.func,
|
||||
};
|
||||
|
||||
VTKViewport.defaultProps = {
|
||||
onScroll: () => {},
|
||||
};
|
||||
|
||||
export default VTKViewport;
|
||||
@@ -70,12 +70,9 @@ const commandsModule = ({ commandsManager }) => {
|
||||
if (cornerstoneElement) {
|
||||
const imageId = cornerstoneElement.image.imageId;
|
||||
|
||||
const { modality } = cornerstone.metaData.get(
|
||||
'generalSeriesModule',
|
||||
imageId
|
||||
);
|
||||
const Modality = cornerstone.metaData.get('Modality', imageId);
|
||||
|
||||
if (modality !== 'PT') {
|
||||
if (Modality !== 'PT') {
|
||||
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
|
||||
|
||||
return {
|
||||
@@ -103,6 +100,9 @@ const commandsModule = ({ commandsManager }) => {
|
||||
}
|
||||
|
||||
const actions = {
|
||||
getVtkApis: ({ index }) => {
|
||||
return apis[index];
|
||||
},
|
||||
axial: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
@@ -234,7 +234,7 @@ const commandsModule = ({ commandsManager }) => {
|
||||
},
|
||||
},
|
||||
{
|
||||
// Sagital
|
||||
// Sagittal
|
||||
orientation: {
|
||||
sliceNormal: [1, 0, 0],
|
||||
viewUp: [0, 0, 1],
|
||||
@@ -266,11 +266,12 @@ const commandsModule = ({ commandsManager }) => {
|
||||
'crosshairsWidget'
|
||||
);
|
||||
|
||||
const uid = api.uid;
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
configuration: { apis, apiIndex, uid },
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -296,55 +297,45 @@ const commandsModule = ({ commandsManager }) => {
|
||||
},
|
||||
enableRotateTool: {
|
||||
commandFn: actions.enableRotateTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableCrosshairsTool: {
|
||||
commandFn: actions.enableCrosshairsTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableLevelTool: {
|
||||
commandFn: actions.enableLevelTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setBlendModeToComposite: {
|
||||
commandFn: actions.setBlendModeToComposite,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||
},
|
||||
setBlendModeToMaximumIntensity: {
|
||||
commandFn: actions.setBlendModeToMaximumIntensity,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToMinimumIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToAverageIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
|
||||
},
|
||||
setSlabThickness: {
|
||||
// TODO: How do we pass in a function argument?
|
||||
commandFn: actions.setSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
increaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: 3,
|
||||
},
|
||||
},
|
||||
decreaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: -3,
|
||||
},
|
||||
@@ -355,6 +346,10 @@ const commandsModule = ({ commandsManager }) => {
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
getVtkApiForViewportIndex: {
|
||||
commandFn: actions.getVtkApis,
|
||||
context: 'VIEWER',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import * as dcmjs from "dcmjs";
|
||||
import * as dcmjs from 'dcmjs';
|
||||
|
||||
import OHIF from "@ohif/core";
|
||||
import { api } from "dicomweb-client";
|
||||
import OHIF from '@ohif/core';
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
function getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
function getDisplaySet(studies, StudyInstanceUID, displaySetInstanceUID) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
return set.displaySetInstanceUID === displaySetInstanceUID;
|
||||
});
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
function getDisplaySetsBySeries(studies, studyInstanceUid, seriesInstanceUid) {
|
||||
function getDisplaySetsBySeries(studies, StudyInstanceUID, SeriesInstanceUID) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
return study.displaySets.filter(set => {
|
||||
return set.seriesInstanceUid === seriesInstanceUid;
|
||||
return set.SeriesInstanceUID === SeriesInstanceUID;
|
||||
});
|
||||
}
|
||||
|
||||
function getCornerstoneStack(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
function getCornerstoneStack(studies, StudyInstanceUID, displaySetInstanceUID) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
// Create shortcut to displaySet
|
||||
const displaySet = getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
// Get stack from Stack Manager
|
||||
@@ -52,19 +52,19 @@ function getCornerstoneStack(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
function retrieveDicomData(
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
sopInstanceUid,
|
||||
wadoRoot
|
||||
) {
|
||||
const config = {
|
||||
url: wadoRoot,
|
||||
headers: DICOMWeb.getAuthorizationHeader()
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID
|
||||
sopInstanceUid,
|
||||
};
|
||||
|
||||
return dicomWeb.retrieveInstance(options);
|
||||
@@ -72,30 +72,26 @@ function retrieveDicomData(
|
||||
|
||||
async function handleSegmentationStorage(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
const displaySet = getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
// TODO: This is terrible but we need to use WADO-RS or we can't retrieve the SEG
|
||||
// from google cloud
|
||||
const wadoRoot = displaySet.images[0].getData().wadoRoot;
|
||||
|
||||
const StudyInstanceUID = displaySet.images[0].getStudyInstanceUID();
|
||||
const SeriesInstanceUID = displaySet.images[0].getSeriesInstanceUID();
|
||||
const SOPInstanceUID = displaySet.images[0].getSOPInstanceUID();
|
||||
|
||||
const arrayBuffer = await retrieveDicomData(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
displaySet.images[0].getStudyInstanceUID(),
|
||||
displaySet.images[0].getSeriesInstanceUID(),
|
||||
displaySet.images[0].getSOPInstanceUID(),
|
||||
wadoRoot
|
||||
);
|
||||
|
||||
@@ -108,16 +104,16 @@ async function handleSegmentationStorage(
|
||||
|
||||
dataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
|
||||
const seriesInstanceUid = dataset.ReferencedSeriesSequence.SeriesInstanceUID;
|
||||
const SeriesInstanceUID = dataset.ReferencedSeriesSequence.SeriesInstanceUID;
|
||||
const displaySets = getDisplaySetsBySeries(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
"More than one display set with the same seriesInstanceUid. This is not supported yet..."
|
||||
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,7 +121,7 @@ async function handleSegmentationStorage(
|
||||
const imageIds = referenceDisplaySet.images.map(image => image.getImageId());
|
||||
|
||||
if (!results) {
|
||||
throw new Error("Fractional segmentations are not supported");
|
||||
throw new Error('Fractional segmentations are not supported');
|
||||
}
|
||||
|
||||
const cachedStack = StackManager.findOrCreateStack(
|
||||
@@ -137,13 +133,13 @@ async function handleSegmentationStorage(
|
||||
|
||||
return {
|
||||
referenceDataObject,
|
||||
labelmapDataObject
|
||||
labelmapDataObject,
|
||||
};
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
stack
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
stack,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import commandsModule from './commandsModule.js';
|
||||
import toolbarModule from './toolbarModule.js';
|
||||
import withCommandsManager from './withCommandsManager.js';
|
||||
// This feels weird
|
||||
// import loadLocales from './loadLocales';
|
||||
|
||||
@@ -14,8 +15,8 @@ const vtkExtension = {
|
||||
*/
|
||||
id: 'vtk',
|
||||
|
||||
getViewportModule() {
|
||||
return OHIFVTKViewport;
|
||||
getViewportModule({ commandsManager }) {
|
||||
return withCommandsManager(OHIFVTKViewport, commandsManager);
|
||||
},
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ToolbarButton } from '@ohif/ui';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
let isVisible = true;
|
||||
|
||||
const _isDisplaySetReconstructable = (viewportSpecificData = {}, activeViewportIndex) => {
|
||||
if (!viewportSpecificData[activeViewportIndex]) {
|
||||
return false;
|
||||
};
|
||||
|
||||
const { displaySetInstanceUID, StudyInstanceUID } = viewportSpecificData[
|
||||
activeViewportIndex
|
||||
];
|
||||
|
||||
const studies = studyMetadataManager.all();
|
||||
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
if (!study) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const displaySet = study._displaySets.find(set => set.displaySetInstanceUID === displaySetInstanceUID);
|
||||
|
||||
if (!displaySet) {
|
||||
return false;
|
||||
};
|
||||
|
||||
return displaySet.isReconstructable;
|
||||
};
|
||||
|
||||
function VTKMPRToolbarButton({
|
||||
parentContext,
|
||||
toolbarClickCallback,
|
||||
button,
|
||||
activeButtons,
|
||||
isActive,
|
||||
className,
|
||||
}) {
|
||||
const { id, label, icon } = button;
|
||||
const { viewportSpecificData, activeViewportIndex } = useSelector(state => {
|
||||
const { viewports = {} } = state;
|
||||
const { viewportSpecificData, activeViewportIndex } = viewports;
|
||||
|
||||
return {
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
}
|
||||
});
|
||||
|
||||
isVisible = _isDisplaySetReconstructable(
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{isVisible && (
|
||||
<ToolbarButton
|
||||
key={id}
|
||||
label={label}
|
||||
icon={icon}
|
||||
onClick={evt => toolbarClickCallback(button, evt)}
|
||||
isActive={isActive}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
VTKMPRToolbarButton.propTypes = {
|
||||
parentContext: PropTypes.object.isRequired,
|
||||
toolbarClickCallback: PropTypes.func.isRequired,
|
||||
button: PropTypes.object.isRequired,
|
||||
activeButtons: PropTypes.array.isRequired,
|
||||
isActive: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default VTKMPRToolbarButton;
|
||||
@@ -1,4 +1,5 @@
|
||||
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
|
||||
import VTKMPRToolbarButton from './toolbarComponents/VTKMPRToolbarButton';
|
||||
|
||||
const TOOLBAR_BUTTON_TYPES = {
|
||||
COMMAND: 'command',
|
||||
@@ -115,9 +116,19 @@ const definitions = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2DMPR',
|
||||
label: '2D MPR',
|
||||
icon: 'cube',
|
||||
//
|
||||
CustomComponent: VTKMPRToolbarButton,
|
||||
type: TOOLBAR_BUTTON_TYPES.COMMAND,
|
||||
commandName: 'mpr2d',
|
||||
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function withCommandsManager(Component, commandsManager = {}) {
|
||||
return class WithCommandsManager extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Component
|
||||
{...this.props}
|
||||
onScroll={viewportIndex =>
|
||||
commandsManager.runCommand('getVtkApiForViewportIndex', {
|
||||
index: viewportIndex,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
+2
-1
@@ -10,7 +10,8 @@ module.exports = {
|
||||
// https://jestjs.io/docs/en/configuration#projects-array-string-projectconfig
|
||||
projects: [
|
||||
// "<rootDir>/extensions/*/jest.config.js",
|
||||
"<rootDir>/platform/*/jest.config.js"
|
||||
"<rootDir>/platform/*/jest.config.js",
|
||||
"<rootDir>/extensions/*/jest.config.js"
|
||||
],
|
||||
coverageDirectory: "<rootDir>/coverage/"
|
||||
};
|
||||
+3
-1
@@ -13,11 +13,13 @@
|
||||
"scripts": {
|
||||
"cm": "npx git-cz",
|
||||
"build": "lerna run build:viewer --stream",
|
||||
"build:lt": "lerna run build:viewer:lesion-tracker --stream",
|
||||
"build:ci": "lerna run build:viewer:ci --stream",
|
||||
"build:demo": "lerna run build:viewer:demo --stream",
|
||||
"build:package": "lerna run build:viewer:package --stream",
|
||||
"build:package-all": "lerna run build:package --parallel --stream",
|
||||
"dev": "lerna run dev:viewer --stream",
|
||||
"dev:lt": "lerna run dev:viewer:lesion-tracker --stream",
|
||||
"dev:project": ".scripts/dev.sh",
|
||||
"dev:orthanc": "lerna run dev:orthanc --stream",
|
||||
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
|
||||
@@ -59,6 +61,7 @@
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^3.2.0",
|
||||
"cypress-file-upload": "^3.5.3",
|
||||
"dotenv": "^8.1.0",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-prettier": "^6.4.0",
|
||||
@@ -72,7 +75,6 @@
|
||||
"eslint-plugin-react": "7.x",
|
||||
"eslint-plugin-react-hooks": "1.5.0",
|
||||
"extract-css-chunks-webpack-plugin": "^4.5.4",
|
||||
"favicons-webpack-plugin": "^1.0.2",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^3.0.0",
|
||||
"jest": "^24.8.0",
|
||||
|
||||
@@ -3,6 +3,204 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.5.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.2...@ohif/core@2.5.3) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.1...@ohif/core@2.5.2) (2020-03-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.5.0...@ohif/core@2.5.1) (2020-02-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 💡 Update log to warn ([#1454](https://github.com/OHIF/Viewers/issues/1454)) ([509a7b7](https://github.com/OHIF/Viewers/commit/509a7b7e57834a0653add98cc320b2a5dbefd51d)), closes [#1451](https://github.com/OHIF/Viewers/issues/1451)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.1...@ohif/core@2.5.0) (2020-02-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* [#1342](https://github.com/OHIF/Viewers/issues/1342) - Window level tab ([#1429](https://github.com/OHIF/Viewers/issues/1429)) ([ebc01a8](https://github.com/OHIF/Viewers/commit/ebc01a8ca238d5a3437b44d81f75aa8a5e8d0574))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.4.0...@ohif/core@2.4.1) (2020-02-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Combined Hotkeys for special characters ([#1233](https://github.com/OHIF/Viewers/issues/1233)) ([2f30e7a](https://github.com/OHIF/Viewers/commit/2f30e7a821a238144c49c56f37d8e5565540b4bd))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.9...@ohif/core@2.4.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.8...@ohif/core@2.3.9) (2020-02-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.7...@ohif/core@2.3.8) (2020-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove trash data from redux storage after updates ([#1358](https://github.com/OHIF/Viewers/issues/1358)) ([7b2d44f](https://github.com/OHIF/Viewers/commit/7b2d44f2c18241ea521b8d3652aee32e36eaddb8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.6...@ohif/core@2.3.7) (2020-01-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Set VTK viewport as active by interaction ([#1139](https://github.com/OHIF/Viewers/issues/1139)) ([686d12d](https://github.com/OHIF/Viewers/commit/686d12da5c9d3d435b1e326c2a5caee36e2ed27c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.5...@ohif/core@2.3.6) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.4...@ohif/core@2.3.5) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.4](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.3...@ohif/core@2.3.4) (2020-01-27)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.3](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.2...@ohif/core@2.3.3) (2020-01-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.1...@ohif/core@2.3.2) (2020-01-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.0...@ohif/core@2.3.1) (2019-12-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 1241: Make Plugin switch part of ToolbarModule ([#1322](https://github.com/OHIF/Viewers/issues/1322)) ([6540e36](https://github.com/OHIF/Viewers/commit/6540e36818944ac2eccc696186366ae495b33a04)), closes [#1241](https://github.com/OHIF/Viewers/issues/1241)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.1...@ohif/core@2.3.0) (2019-12-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Configuration so viewer tools can nix handles ([#1304](https://github.com/OHIF/Viewers/issues/1304)) ([63594d3](https://github.com/OHIF/Viewers/commit/63594d36b0bdba59f0901095aed70b75fb05172d)), closes [#1223](https://github.com/OHIF/Viewers/issues/1223)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.2.0...@ohif/core@2.2.1) (2019-12-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.2.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.1...@ohif/core@2.2.0) (2019-12-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Expose extension config to modules ([#1279](https://github.com/OHIF/Viewers/issues/1279)) ([4ea239a](https://github.com/OHIF/Viewers/commit/4ea239a9535ef297e23387c186e537ab273744ea)), closes [#1268](https://github.com/OHIF/Viewers/issues/1268)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.1.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.1.0...@ohif/core@2.1.1) (2019-12-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.2...@ohif/core@2.1.0) (2019-12-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 DICOM SR STOW on MeasurementAPI ([#954](https://github.com/OHIF/Viewers/issues/954)) ([ebe1af8](https://github.com/OHIF/Viewers/commit/ebe1af8d4f75d2483eba869655906d7829bd9666)), closes [#758](https://github.com/OHIF/Viewers/issues/758)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.1...@ohif/core@2.0.2) (2019-12-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@2.0.0...@ohif/core@2.0.1) (2019-12-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
@@ -66,8 +66,8 @@ import { studies } from '@ohif/core';
|
||||
|
||||
const studiesMetadata = await studies.retrieveStudiesMetadata(
|
||||
server, // Object
|
||||
studyInstanceUids, // Array
|
||||
seriesInstanceUids // Array (optional)
|
||||
studyInstanceUIDs, // Array
|
||||
seriesInstanceUIDs // Array (optional)
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "2.0.1",
|
||||
"version": "2.5.3",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -32,13 +32,14 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-tools": "4.12.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dicom-parser": "^1.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "^0.8.3",
|
||||
"dicomweb-client": "^0.5.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import log from '../log';
|
||||
import studies from '../studies';
|
||||
import utils from '../utils';
|
||||
import {
|
||||
retrieveMeasurementFromSR,
|
||||
stowSRFromMeasurements,
|
||||
} from './handleStructuredReport';
|
||||
import findMostRecentStructuredReport from './utils/findMostRecentStructuredReport';
|
||||
|
||||
/**
|
||||
*
|
||||
* @typedef serverType
|
||||
* @property {string} type - type of the server
|
||||
* @property {string} wadoRoot - server wado root url
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
|
||||
*
|
||||
* @param {serverType} server
|
||||
* @returns {Promise} Should resolve with OHIF measurementData object
|
||||
*/
|
||||
const retrieveMeasurements = server => {
|
||||
log.info('[DICOMSR] retrieveMeasurements');
|
||||
|
||||
if (!server || server.type !== 'dicomWeb') {
|
||||
log.error('[DICOMSR] DicomWeb server is required!');
|
||||
return Promise.reject({});
|
||||
}
|
||||
|
||||
const serverUrl = server.wadoRoot;
|
||||
const studies = utils.studyMetadataManager.all();
|
||||
|
||||
const latestSeries = findMostRecentStructuredReport(studies);
|
||||
|
||||
if (!latestSeries) return Promise.resolve({});
|
||||
|
||||
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to be registered into MeasurementAPI to store measurements into DICOM Structured Reports
|
||||
*
|
||||
* @param {Object} measurementData - OHIF measurementData object
|
||||
* @param {Object} filter
|
||||
* @param {serverType} server
|
||||
* @returns {Object} With message to be displayed on success
|
||||
*/
|
||||
const storeMeasurements = async (measurementData, filter, server) => {
|
||||
log.info('[DICOMSR] storeMeasurements');
|
||||
|
||||
if (!server || server.type !== 'dicomWeb') {
|
||||
log.error('[DICOMSR] DicomWeb server is required!');
|
||||
return Promise.reject({});
|
||||
}
|
||||
|
||||
const serverUrl = server.wadoRoot;
|
||||
const firstMeasurementKey = Object.keys(measurementData)[0];
|
||||
const firstMeasurement = measurementData[firstMeasurementKey][0];
|
||||
const StudyInstanceUID =
|
||||
firstMeasurement && firstMeasurement.StudyInstanceUID;
|
||||
|
||||
try {
|
||||
await stowSRFromMeasurements(measurementData, serverUrl);
|
||||
if (StudyInstanceUID) {
|
||||
studies.deleteStudyMetadataPromise(StudyInstanceUID);
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Measurements saved successfully',
|
||||
};
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`[DICOMSR] Error while saving the measurements: ${error.message}`
|
||||
);
|
||||
throw new Error('Error while saving the measurements.');
|
||||
}
|
||||
};
|
||||
|
||||
export { retrieveMeasurements, storeMeasurements };
|
||||
@@ -0,0 +1,84 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import { api } from 'dicomweb-client';
|
||||
|
||||
import DICOMWeb from '../DICOMWeb';
|
||||
import parseDicomStructuredReport from './parseDicomStructuredReport';
|
||||
import parseMeasurementsData from './parseMeasurementsData';
|
||||
import getAllDisplaySets from './utils/getAllDisplaySets';
|
||||
|
||||
const VERSION_NAME = 'dcmjs-0.0';
|
||||
const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
||||
|
||||
/**
|
||||
* Function to retrieve measurements from DICOM Structured Reports coming from determined server
|
||||
*
|
||||
* @param {Array} series - List of all series metaData loaded
|
||||
* @param {Array} studies - List of all studies metaData loaded
|
||||
* @param {string} serverUrl - Server URL to be used on request
|
||||
* @returns {Object} MeasurementData
|
||||
*/
|
||||
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
||||
const config = {
|
||||
url: serverUrl,
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
|
||||
const instance = series.getFirstInstance();
|
||||
const options = {
|
||||
studyInstanceUID: instance.getStudyInstanceUID(),
|
||||
seriesInstanceUID: instance.getSeriesInstanceUID(),
|
||||
sopInstanceUID: instance.getSOPInstanceUID(),
|
||||
};
|
||||
|
||||
const part10SRArrayBuffer = await dicomWeb.retrieveInstance(options);
|
||||
const displaySets = getAllDisplaySets(studies);
|
||||
const measurementsData = parseDicomStructuredReport(
|
||||
part10SRArrayBuffer,
|
||||
displaySets
|
||||
);
|
||||
|
||||
return measurementsData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to store measurements to DICOM Structured Reports in determined server
|
||||
*
|
||||
* @param {Object} measurements - OHIF measurementData object
|
||||
* @param {string} serverUrl - Server URL to be used on request
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const stowSRFromMeasurements = async (measurements, serverUrl) => {
|
||||
const { dataset } = parseMeasurementsData(measurements);
|
||||
const { DicomMetaDictionary, DicomDict } = dcmjs.data;
|
||||
const meta = {
|
||||
FileMetaInformationVersion: dataset._meta.FileMetaInformationVersion.Value,
|
||||
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
||||
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
||||
TransferSyntaxUID: TRANSFER_SYNTAX_UID,
|
||||
ImplementationClassUID: DicomMetaDictionary.uid(),
|
||||
ImplementationVersionName: VERSION_NAME,
|
||||
};
|
||||
|
||||
const denaturalized = DicomMetaDictionary.denaturalizeDataset(meta);
|
||||
const dicomDict = new DicomDict(denaturalized);
|
||||
|
||||
dicomDict.dict = DicomMetaDictionary.denaturalizeDataset(dataset);
|
||||
|
||||
const part10Buffer = dicomDict.write();
|
||||
|
||||
const config = {
|
||||
url: serverUrl,
|
||||
headers: DICOMWeb.getAuthorizationHeader(),
|
||||
};
|
||||
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
datasets: [part10Buffer],
|
||||
};
|
||||
|
||||
await dicomWeb.storeInstances(options);
|
||||
};
|
||||
|
||||
export { retrieveMeasurementFromSR, stowSRFromMeasurements };
|
||||
@@ -0,0 +1,10 @@
|
||||
import { retrieveMeasurements, storeMeasurements } from './dataExchange';
|
||||
import isToolSupported from './utils/isToolSupported';
|
||||
|
||||
const DICOMSR = {
|
||||
retrieveMeasurements,
|
||||
storeMeasurements,
|
||||
isToolSupported,
|
||||
};
|
||||
|
||||
export default DICOMSR;
|
||||
@@ -0,0 +1,98 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
|
||||
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
||||
|
||||
/**
|
||||
* Function to parse the part10 array buffer that comes from a DICOM Structured report into measurementData
|
||||
* measurementData format is a viewer specific format to be stored into the redux and consumed by other components
|
||||
* (e.g. measurement table)
|
||||
*
|
||||
* @param {ArrayBuffer} part10SRArrayBuffer
|
||||
* @param {Array} displaySets
|
||||
* @returns
|
||||
*/
|
||||
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
||||
// Get the dicom data as an Object
|
||||
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
|
||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
|
||||
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
|
||||
const storedMeasurementByToolType = MeasurementReport.generateToolState(
|
||||
dataset
|
||||
);
|
||||
const measurementData = {};
|
||||
let measurementNumber = 0;
|
||||
|
||||
Object.keys(storedMeasurementByToolType).forEach(toolName => {
|
||||
const measurements = storedMeasurementByToolType[toolName];
|
||||
measurementData[toolName] = [];
|
||||
|
||||
measurements.forEach(measurement => {
|
||||
const instanceMetadata = findInstanceMetadataBySopInstanceUID(
|
||||
displaySets,
|
||||
measurement.sopInstanceUid
|
||||
);
|
||||
|
||||
const { _study: study, _series: series } = instanceMetadata;
|
||||
const { StudyInstanceUID, PatientID } = study;
|
||||
const { SeriesInstanceUID } = series;
|
||||
const { sopInstanceUid, frameIndex } = measurement;
|
||||
const imagePath = getImagePath(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
sopInstanceUid,
|
||||
frameIndex
|
||||
);
|
||||
|
||||
const imageId = instanceMetadata.getImageId();
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: We need the currentTimepointID set into the viewer
|
||||
const currentTimepointId = 'TimepointId';
|
||||
|
||||
const toolData = Object.assign({}, measurement, {
|
||||
imageId,
|
||||
imagePath,
|
||||
SOPInstanceUID: sopInstanceUid,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
PatientID,
|
||||
measurementNumber: ++measurementNumber,
|
||||
timepointId: currentTimepointId,
|
||||
toolType: toolName,
|
||||
_id: imageId + measurementNumber,
|
||||
});
|
||||
|
||||
measurementData[toolName].push(toolData);
|
||||
});
|
||||
});
|
||||
|
||||
return measurementData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to create imagePath with all imageData related
|
||||
*
|
||||
* @param {string} StudyInstanceUID
|
||||
* @param {string} SeriesInstanceUID
|
||||
* @param {string} SOPInstanceUID
|
||||
* @param {string} frameIndex
|
||||
* @returns
|
||||
*/
|
||||
const getImagePath = (
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
) => {
|
||||
return [StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, frameIndex].join(
|
||||
'_'
|
||||
);
|
||||
};
|
||||
|
||||
export default parseDicomStructuredReport;
|
||||
@@ -0,0 +1,57 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
import log from '../log';
|
||||
import measurements from '../measurements';
|
||||
import isToolSupported from './utils/isToolSupported';
|
||||
|
||||
/**
|
||||
* Function to parse OHIF viewer measurementData into a dcmjs MeasurementReport
|
||||
*
|
||||
* @param {Object} measurementsData - OHIF measurementData object
|
||||
* @returns {Object} Dataset: measurement report from dcmjs
|
||||
*/
|
||||
const parseMeasurementsData = measurementsData => {
|
||||
const { MeasurementReport } = dcmjs.adapters.Cornerstone;
|
||||
const { getImageIdForImagePath } = measurements;
|
||||
|
||||
const toolState = {};
|
||||
const unsupportedTools = [];
|
||||
|
||||
Object.keys(measurementsData).forEach(measurementType => {
|
||||
const annotations = measurementsData[measurementType];
|
||||
|
||||
annotations.forEach(annotation => {
|
||||
const { toolType, imagePath } = annotation;
|
||||
|
||||
if (isToolSupported(toolType)) {
|
||||
const imageId = getImageIdForImagePath(imagePath);
|
||||
toolState[imageId] = toolState[imageId] || {};
|
||||
toolState[imageId][toolType] = toolState[imageId][toolType] || {
|
||||
data: [],
|
||||
};
|
||||
|
||||
toolState[imageId][toolType].data.push(annotation);
|
||||
} else {
|
||||
unsupportedTools.push(toolType);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (unsupportedTools.length > 0) {
|
||||
log.warn(
|
||||
`[DICOMSR] Tooltypes not supported: ${unsupportedTools.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const report = MeasurementReport.generateReport(
|
||||
toolState,
|
||||
cornerstone.metaData
|
||||
);
|
||||
|
||||
return {
|
||||
dataset: report.dataset,
|
||||
};
|
||||
};
|
||||
|
||||
export default parseMeasurementsData;
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Should Find the requested instance metadata into the displaySets and return
|
||||
*
|
||||
* @param {Array} displaySets - List of displaySets
|
||||
* @param {string} SOPInstanceUID - sopInstanceUID to look for
|
||||
* @returns {Object} - instance metadata found
|
||||
*/
|
||||
const findInstanceMetadataBySopInstanceUID = (displaySets, SOPInstanceUID) => {
|
||||
let instanceFound;
|
||||
|
||||
displaySets.find(displaySet => {
|
||||
if (!displaySet.images) return false;
|
||||
|
||||
instanceFound = displaySet.images.find(
|
||||
instanceMetadata =>
|
||||
instanceMetadata.getSOPInstanceUID() === SOPInstanceUID
|
||||
);
|
||||
|
||||
return !!instanceFound;
|
||||
});
|
||||
|
||||
return instanceFound;
|
||||
};
|
||||
|
||||
export default findInstanceMetadataBySopInstanceUID;
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Should find the most recent Structured Report metadata
|
||||
*
|
||||
* @param {Array} studies
|
||||
* @returns {Object} Series
|
||||
*/
|
||||
const findMostRecentStructuredReport = studies => {
|
||||
let mostRecentStructuredReport;
|
||||
|
||||
studies.forEach(study => {
|
||||
const allSeries = study.getSeries ? study.getSeries() : [];
|
||||
allSeries.forEach(series => {
|
||||
if (isStructuredReportSeries(series)) {
|
||||
if (
|
||||
!mostRecentStructuredReport ||
|
||||
compareSeriesDate(series, mostRecentStructuredReport)
|
||||
) {
|
||||
mostRecentStructuredReport = series;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return mostRecentStructuredReport;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if series sopClassUID matches with the supported Structured Reports sopClassUID
|
||||
*
|
||||
* @param {Object} series - Series metadata
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isStructuredReportSeries = series => {
|
||||
const supportedSopClassUIDs = [
|
||||
'1.2.840.10008.5.1.4.1.1.88.22',
|
||||
'1.2.840.10008.5.1.4.1.1.11.1',
|
||||
];
|
||||
|
||||
const firstInstance = series.getFirstInstance();
|
||||
const SOPClassUID = firstInstance.getData().metadata.SOPClassUID;
|
||||
|
||||
return supportedSopClassUIDs.includes(SOPClassUID);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checkes if series1 is newer than series2
|
||||
*
|
||||
* @param {Object} series1 - Series Metadata 1
|
||||
* @param {Object} series2 - Series Metadata 2
|
||||
* @returns {boolean} true/false if series1 is newer than series2
|
||||
*/
|
||||
const compareSeriesDate = (series1, series2) => {
|
||||
return (
|
||||
series1._data.SeriesDate > series2._data.SeriesDate ||
|
||||
(series1._data.SeriesDate === series2._data.SeriesDate &&
|
||||
series1._data.SeriesTime > series2._data.SeriesTime)
|
||||
);
|
||||
};
|
||||
|
||||
export default findMostRecentStructuredReport;
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Retrieve a list of all displaySets of all studies
|
||||
*
|
||||
* @param {Object} studies - List of studies loaded into the viewer
|
||||
* @returns {Object} List of DisplaySets
|
||||
*/
|
||||
const getAllDisplaySets = studies => {
|
||||
let allDisplaySets = [];
|
||||
|
||||
studies.forEach(study => {
|
||||
if (study.getDisplaySets) {
|
||||
allDisplaySets = allDisplaySets.concat(study.getDisplaySets());
|
||||
}
|
||||
});
|
||||
|
||||
return allDisplaySets;
|
||||
};
|
||||
|
||||
export default getAllDisplaySets;
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as dcmjs from 'dcmjs';
|
||||
|
||||
/**
|
||||
* Checks if dcmjs has support to determined tool
|
||||
*
|
||||
* @param {string} toolName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isToolSupported = toolName => {
|
||||
const adapter = dcmjs.adapters.Cornerstone;
|
||||
return !!adapter[toolName];
|
||||
};
|
||||
|
||||
export default isToolSupported;
|
||||
@@ -1,23 +1,23 @@
|
||||
export default function getModalities(modality, modalitiesInStudy) {
|
||||
if (!modality && !modalitiesInStudy) {
|
||||
export default function getModalities(Modality, ModalitiesInStudy) {
|
||||
if (!Modality && !ModalitiesInStudy) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const modalities = modality || {
|
||||
const modalities = Modality || {
|
||||
vr: 'CS',
|
||||
Value: [],
|
||||
};
|
||||
|
||||
if (modalitiesInStudy) {
|
||||
if (modalities.vr && modalities.vr === modalitiesInStudy.vr) {
|
||||
for (let i = 0; i < modalitiesInStudy.Value.length; i++) {
|
||||
const value = modalitiesInStudy.Value[i];
|
||||
if (ModalitiesInStudy) {
|
||||
if (modalities.vr && modalities.vr === ModalitiesInStudy.vr) {
|
||||
for (let i = 0; i < ModalitiesInStudy.Value.length; i++) {
|
||||
const value = ModalitiesInStudy.Value[i];
|
||||
if (modalities.Value.indexOf(value) === -1) {
|
||||
modalities.Value.push(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return modalitiesInStudy;
|
||||
return ModalitiesInStudy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
import getModalities from './getModalities';
|
||||
|
||||
describe('getModalities', () => {
|
||||
test('should return an empty object when modality and modalitiesInStudy are not present', () => {
|
||||
const modality = null;
|
||||
const modalitiesInStudy = null;
|
||||
test('should return an empty object when Modality and ModalitiesInStudy are not present', () => {
|
||||
const Modality = null;
|
||||
const ModalitiesInStudy = null;
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual({});
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual({});
|
||||
});
|
||||
|
||||
test('should return an empty object when modality and modalitiesInStudy are not present', () => {
|
||||
const modality = null;
|
||||
const modalitiesInStudy = null;
|
||||
test('should return an empty object when Modality and ModalitiesInStudy are not present', () => {
|
||||
const Modality = null;
|
||||
const ModalitiesInStudy = null;
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual({});
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual({});
|
||||
});
|
||||
|
||||
test('should return modalities in Study when modality is not defined', () => {
|
||||
const modality = null;
|
||||
test('should return modalities in Study when Modality is not defined', () => {
|
||||
const Modality = null;
|
||||
|
||||
const modalitiesInStudy = {
|
||||
const ModalitiesInStudy = {
|
||||
Value: ['MOCKED_VALUE'],
|
||||
vr: 'MOCKED_VALUE',
|
||||
};
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual(
|
||||
modalitiesInStudy
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(
|
||||
ModalitiesInStudy
|
||||
);
|
||||
});
|
||||
|
||||
test('should return only the modalitues that exists in modalitiesInStudy', () => {
|
||||
const modality = {
|
||||
test('should return only the modalitues that exists in ModalitiesInStudy', () => {
|
||||
const Modality = {
|
||||
Value: ['DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
const modalitiesInStudy = {
|
||||
const ModalitiesInStudy = {
|
||||
Value: ['DESIRED_VALUE', 'NOT_DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual(modality);
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(Modality);
|
||||
});
|
||||
|
||||
test('should return the seek modality when the desired modality does not exist in modalitiesInStudy', () => {
|
||||
const modality = {
|
||||
test('should return the seek Modality when the desired Modality does not exist in ModalitiesInStudy', () => {
|
||||
const Modality = {
|
||||
Value: ['DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
const modalitiesInStudy = {
|
||||
const ModalitiesInStudy = {
|
||||
Value: ['NOT_DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual(modality);
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(Modality);
|
||||
});
|
||||
|
||||
test('should return the seek modality when the desired modality does not exist in modalitiesInStudy VR', () => {
|
||||
const modality = {
|
||||
test('should return the seek Modality when the desired Modality does not exist in ModalitiesInStudy VR', () => {
|
||||
const Modality = {
|
||||
Value: ['DESIRED_VALUE'],
|
||||
vr: 'DESIRED_VR',
|
||||
};
|
||||
|
||||
const modalitiesInStudy = {
|
||||
const ModalitiesInStudy = {
|
||||
Value: ['NOT_DESIRED_VALUE'],
|
||||
vr: 'ANOTHER_VR',
|
||||
};
|
||||
|
||||
expect(getModalities(modality, modalitiesInStudy)).toEqual(
|
||||
modalitiesInStudy
|
||||
expect(getModalities(Modality, ModalitiesInStudy)).toEqual(
|
||||
ModalitiesInStudy
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
};
|
||||
@@ -161,7 +161,7 @@ export class CommandsManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const { commandFn, storeContexts } = definition;
|
||||
const { commandFn, storeContexts = [] } = definition;
|
||||
const definitionOptions = definition.options;
|
||||
|
||||
let commandParams = {};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import hotkeys from './hotkeys';
|
||||
import hotkeys from './../utils/hotkeys';
|
||||
import log from './../log.js';
|
||||
|
||||
/**
|
||||
@@ -11,7 +11,7 @@ import log from './../log.js';
|
||||
*/
|
||||
|
||||
export class HotkeysManager {
|
||||
constructor(commandsManager) {
|
||||
constructor(commandsManager, servicesManager) {
|
||||
this.hotkeyDefinitions = {};
|
||||
this.hotkeyDefaults = [];
|
||||
this.isEnabled = true;
|
||||
@@ -22,9 +22,19 @@ export class HotkeysManager {
|
||||
);
|
||||
}
|
||||
|
||||
this._servicesManager = servicesManager;
|
||||
this._commandsManager = commandsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exposes Mousetrap.js's `.record` method, added by the record plugin.
|
||||
*
|
||||
* @param {*} event
|
||||
*/
|
||||
record(event) {
|
||||
return hotkeys.record(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables all hotkeys. Hotkeys added while disabled will not listen for
|
||||
* input.
|
||||
@@ -45,38 +55,57 @@ export class HotkeysManager {
|
||||
/**
|
||||
* Registers a list of hotkeydefinitions.
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
setHotkeys(hotkeyDefinitions = []) {
|
||||
try {
|
||||
const definitions = this._getValidDefinitions(hotkeyDefinitions);
|
||||
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
} catch (error) {
|
||||
const { UINotificationService } = this._servicesManager.services;
|
||||
UINotificationService.show({
|
||||
title: 'Hotkeys Manager',
|
||||
message: 'Erro while setting hotkeys',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default hotkey bindings. These
|
||||
* values are used in `this.restoreDefaultBindings`.
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
setDefaultHotKeys(hotkeyDefinitions) {
|
||||
setDefaultHotKeys(hotkeyDefinitions = []) {
|
||||
const definitions = this._getValidDefinitions(hotkeyDefinitions);
|
||||
|
||||
this.hotkeyDefaults = definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take hotkey definitions that can be an array or object and make sure that it
|
||||
* returns an array of hotkeys
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions=[]] Contains hotkeys definitions
|
||||
*/
|
||||
_getValidDefinitions(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
|
||||
this.hotkeyDefaults = definitions;
|
||||
return definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* It parses given object containing hotkeyDefinition to array like.
|
||||
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @param {HotkeyDefinition[] | Object} [hotkeyDefinitions={}] Contains hotkeys definitions
|
||||
* @returns {HotkeyDefinition[]}
|
||||
*/
|
||||
_parseToArrayLike(hotkeyDefinitionsObj) {
|
||||
_parseToArrayLike(hotkeyDefinitionsObj = {}) {
|
||||
const copy = { ...hotkeyDefinitionsObj };
|
||||
return Object.entries(copy).map(entryValue =>
|
||||
this._parseToHotKeyObj(entryValue[0], entryValue[1])
|
||||
@@ -127,11 +156,13 @@ export class HotkeysManager {
|
||||
if (previouslyRegisteredDefinition) {
|
||||
const previouslyRegisteredKeys = previouslyRegisteredDefinition.keys;
|
||||
this._unbindHotkeys(commandName, previouslyRegisteredKeys);
|
||||
log.info(`Unbinding ${commandName} from ${previouslyRegisteredKeys}`);
|
||||
}
|
||||
|
||||
// Set definition & bind
|
||||
this.hotkeyDefinitions[commandName] = { keys, label };
|
||||
this._bindHotkeys(commandName, keys);
|
||||
log.info(`Binding ${commandName} to ${keys}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,12 +198,11 @@ export class HotkeysManager {
|
||||
}
|
||||
|
||||
const isKeyArray = keys instanceof Array;
|
||||
if (isKeyArray) {
|
||||
keys.forEach(key => this._bindHotkeys(commandName, key));
|
||||
return;
|
||||
}
|
||||
const combinedKeys = isKeyArray ? keys.join('+') : keys;
|
||||
|
||||
hotkeys.bind(keys, evt => {
|
||||
hotkeys.bind(combinedKeys, evt => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this._commandsManager.runCommand(commandName, { evt });
|
||||
});
|
||||
}
|
||||
@@ -193,7 +223,8 @@ export class HotkeysManager {
|
||||
|
||||
const isKeyArray = keys instanceof Array;
|
||||
if (isKeyArray) {
|
||||
keys.forEach(key => this._unbindHotkeys(commandName, key));
|
||||
const combinedKeys = keys.join('+');
|
||||
this._unbindHotkeys(commandName, combinedKeys);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import CommandsManager from './CommandsManager.js';
|
||||
import HotkeysManager from './HotkeysManager.js';
|
||||
import hotkeys from './hotkeys';
|
||||
import hotkeys from './../utils/hotkeys';
|
||||
import log from './../log.js';
|
||||
|
||||
jest.mock('./CommandsManager.js');
|
||||
jest.mock('./hotkeys');
|
||||
jest.mock('./../utils/hotkeys');
|
||||
jest.mock('./../log.js');
|
||||
|
||||
describe('HotkeysManager', () => {
|
||||
@@ -60,7 +60,10 @@ describe('HotkeysManager', () => {
|
||||
});
|
||||
|
||||
describe('enable()', () => {
|
||||
beforeEach(() => hotkeys.unpause.mockClear());
|
||||
beforeEach(() => {
|
||||
hotkeys.unpause = jest.fn();
|
||||
hotkeys.unpause.mockClear();
|
||||
});
|
||||
|
||||
it('sets isEnabled property to true', () => {
|
||||
hotkeysManager.disable();
|
||||
@@ -139,20 +142,18 @@ describe('HotkeysManager', () => {
|
||||
expectedHotkeyDefinition
|
||||
);
|
||||
});
|
||||
it('calls hotkeys.bind for all keys in array', () => {
|
||||
const definition = { commandName: 'dance', keys: ['h', 'e', 'l', 'o'] };
|
||||
it('calls hotkeys.bind for the group of keys', () => {
|
||||
const definition = { commandName: 'dance', keys: ['shift', 'e'] };
|
||||
|
||||
hotkeysManager.registerHotkeys(definition);
|
||||
|
||||
expect(hotkeys.bind.mock.calls.length).toBe(definition.keys.length);
|
||||
definition.keys.forEach((key, i) =>
|
||||
expect(hotkeys.bind.mock.calls[i][0]).toBe(key)
|
||||
);
|
||||
expect(hotkeys.bind.mock.calls.length).toBe(1);
|
||||
expect(hotkeys.bind.mock.calls[0][0]).toBe('shift+e');
|
||||
});
|
||||
it('calls hotkeys.unbind if commandName was previously registered, for each previously registered set of keys', () => {
|
||||
const firstDefinition = {
|
||||
commandName: 'dance',
|
||||
keys: ['h', 'e', 'l', 'o'],
|
||||
keys: ['alt', 'e'],
|
||||
};
|
||||
const secondDefinition = { commandName: 'dance', keys: 'a' };
|
||||
|
||||
@@ -161,12 +162,8 @@ describe('HotkeysManager', () => {
|
||||
// Second call
|
||||
hotkeysManager.registerHotkeys(secondDefinition);
|
||||
|
||||
expect(hotkeys.unbind.mock.calls.length).toBe(
|
||||
firstDefinition.keys.length
|
||||
);
|
||||
firstDefinition.keys.forEach((key, i) =>
|
||||
expect(hotkeys.unbind.mock.calls[i][0]).toBe(key)
|
||||
);
|
||||
expect(hotkeys.unbind.mock.calls.length).toBe(1);
|
||||
expect(hotkeys.unbind.mock.calls[0][0]).toBe('alt+e');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,412 +1,508 @@
|
||||
import { parsingUtils } from '../lib/parsingUtils';
|
||||
|
||||
const FUNCTION = 'function';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import queryString from 'query-string';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import { getFallbackTagFromInstance } from '../utils/metadataProvider/metadataProviderFallbackTags';
|
||||
import fetchPaletteColorLookupTableData from '../utils/metadataProvider/fetchPaletteColorLookupTableData';
|
||||
import fetchOverlayData from '../utils/metadataProvider/fetchOverlayData';
|
||||
|
||||
class MetadataProvider {
|
||||
constructor() {
|
||||
// Define the main "metadataLookup" private property as an immutable property.
|
||||
Object.defineProperty(this, 'metadataLookup', {
|
||||
Object.defineProperty(this, 'studies', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
value: new Map(),
|
||||
});
|
||||
|
||||
// Local reference to provider function bound to current instance.
|
||||
Object.defineProperty(this, '_provider', {
|
||||
Object.defineProperty(this, 'imageIdToUIDs', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: null,
|
||||
writable: false,
|
||||
value: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cornerstone Metadata provider to store image meta data
|
||||
* Data from instances, series, and studies are associated with
|
||||
* imageIds to facilitate usage of this information by Cornerstone's Tools
|
||||
*
|
||||
* e.g. the imagePlane metadata object contains instance information about
|
||||
* row/column pixel spacing, patient position, and patient orientation. It
|
||||
* is used in CornerstoneTools to position reference lines and orientation markers.
|
||||
*
|
||||
* @param {String} imageId The Cornerstone ImageId
|
||||
* @param {Object} data An object containing instance, series, and study metadata
|
||||
*/
|
||||
addMetadata(imageId, data) {
|
||||
const instanceMetadata = data.instance;
|
||||
const seriesMetadata = data.series;
|
||||
const studyMetadata = data.study;
|
||||
const numImages = data.numImages;
|
||||
const metadata = {};
|
||||
async addInstance(dicomJSONDatasetOrP10ArrayBuffer, options = {}) {
|
||||
let dicomJSONDataset;
|
||||
|
||||
metadata.frameNumber = data.frameNumber;
|
||||
// If Arraybuffer, parse to DICOMJSON before naturalizing.
|
||||
if (dicomJSONDatasetOrP10ArrayBuffer instanceof ArrayBuffer) {
|
||||
const dicomData = DicomMessage.readFile(dicomJSONDatasetOrP10ArrayBuffer);
|
||||
|
||||
metadata.study = {
|
||||
accessionNumber: studyMetadata.accessionNumber,
|
||||
patientId: studyMetadata.patientId,
|
||||
studyInstanceUid: studyMetadata.studyInstanceUid,
|
||||
studyDate: studyMetadata.studyDate,
|
||||
studyTime: studyMetadata.studyTime,
|
||||
studyDescription: studyMetadata.studyDescription,
|
||||
institutionName: studyMetadata.institutionName,
|
||||
patientHistory: studyMetadata.patientHistory,
|
||||
};
|
||||
|
||||
metadata.series = {
|
||||
seriesDescription: seriesMetadata.seriesDescription,
|
||||
seriesNumber: seriesMetadata.seriesNumber,
|
||||
seriesDate: seriesMetadata.seriesDate,
|
||||
seriesTime: seriesMetadata.seriesTime,
|
||||
modality: seriesMetadata.modality,
|
||||
seriesInstanceUid: seriesMetadata.seriesInstanceUid,
|
||||
numImages: numImages,
|
||||
};
|
||||
|
||||
metadata.instance = instanceMetadata;
|
||||
|
||||
metadata.patient = {
|
||||
name: studyMetadata.patientName,
|
||||
id: studyMetadata.patientId,
|
||||
birthDate: studyMetadata.patientBirthDate,
|
||||
sex: studyMetadata.patientSex,
|
||||
age: studyMetadata.patientAge,
|
||||
};
|
||||
|
||||
// If there is sufficient information, populate
|
||||
// the imagePlane object for easier use in the Viewer
|
||||
metadata.imagePlane = this.getImagePlane(instanceMetadata);
|
||||
|
||||
// Add the metadata to the imageId lookup object
|
||||
this.metadataLookup.set(imageId, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the metadata for the given imageId
|
||||
* @param {String} imageId The Cornerstone ImageId
|
||||
* @returns image metadata
|
||||
*/
|
||||
getMetadata(imageId) {
|
||||
return this.metadataLookup.get(imageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a set of metadata to the Cornerstone metadata provider given a specific
|
||||
* imageId, type, and dataset
|
||||
*
|
||||
* @param imageId
|
||||
* @param type (e.g. series, instance, tagDisplay)
|
||||
* @param data
|
||||
*/
|
||||
addSpecificMetadata(imageId, type, data) {
|
||||
const metadata = {};
|
||||
metadata[type] = data;
|
||||
|
||||
const oldMetadata = this.metadataLookup.get(imageId);
|
||||
this.metadataLookup.set(imageId, Object.assign(oldMetadata, metadata));
|
||||
}
|
||||
|
||||
getFromImage(image, type, tag, attrName, defaultValue) {
|
||||
let value;
|
||||
|
||||
if (image.data) {
|
||||
value = this.getFromDataSet(image.data, type, tag);
|
||||
dicomJSONDataset = dicomData.dict;
|
||||
} else {
|
||||
value = image.instance[attrName];
|
||||
dicomJSONDataset = dicomJSONDatasetOrP10ArrayBuffer;
|
||||
}
|
||||
|
||||
return value === null ? defaultValue : value;
|
||||
}
|
||||
// Check if dataset is already naturalized.
|
||||
|
||||
getFromDataSet(dataSet, type, tag) {
|
||||
if (!dataSet) {
|
||||
return;
|
||||
}
|
||||
let naturalizedDataset;
|
||||
|
||||
const fn = dataSet[type];
|
||||
if (!fn) {
|
||||
return;
|
||||
}
|
||||
|
||||
return fn.call(dataSet, tag);
|
||||
}
|
||||
|
||||
getFrameIncrementPointer(image) {
|
||||
const dataSet = image.data;
|
||||
let frameInstancePointer = '';
|
||||
|
||||
if (parsingUtils.isValidDataSet(dataSet)) {
|
||||
const frameInstancePointerNames = {
|
||||
x00181063: 'frameTime',
|
||||
x00181065: 'frameTimeVector',
|
||||
};
|
||||
|
||||
// (0028,0009) = Frame Increment Pointer
|
||||
const frameInstancePointerTag = parsingUtils.attributeTag(
|
||||
dataSet,
|
||||
'x00280009'
|
||||
if (dicomJSONDataset['SeriesInstanceUID'] === undefined) {
|
||||
naturalizedDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomJSONDataset
|
||||
);
|
||||
frameInstancePointer = frameInstancePointerNames[frameInstancePointerTag];
|
||||
} else {
|
||||
frameInstancePointer = image.instance.frameIncrementPointer;
|
||||
naturalizedDataset = dicomJSONDataset;
|
||||
}
|
||||
|
||||
return frameInstancePointer || '';
|
||||
}
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
} = naturalizedDataset;
|
||||
|
||||
getFrameTimeVector(image) {
|
||||
const dataSet = image.data;
|
||||
const study = this._getAndCacheStudy(StudyInstanceUID);
|
||||
const series = this._getAndCacheSeriesFromStudy(study, SeriesInstanceUID);
|
||||
const instance = this._getAndCacheInstanceFromStudy(series, SOPInstanceUID);
|
||||
|
||||
if (parsingUtils.isValidDataSet(dataSet)) {
|
||||
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
|
||||
return parsingUtils.floatArray(dataSet, 'x00181065');
|
||||
Object.assign(instance, naturalizedDataset);
|
||||
|
||||
if (options.server) {
|
||||
await this._checkBulkDataAndInlineBinaries(instance, options.server);
|
||||
}
|
||||
|
||||
return image.instance.frameTimeVector;
|
||||
return instance;
|
||||
}
|
||||
|
||||
getFrameTime(image) {
|
||||
const dataSet = image.data;
|
||||
addImageIdToUIDs(imageId, uids) {
|
||||
// This method is a fallback for when you don't have WADO-URI or WADO-RS.
|
||||
// You can add instances fetched by any method by calling addInstance, and hook an imageId to point at it here.
|
||||
// An example would be dicom hosted at some random site.
|
||||
|
||||
if (parsingUtils.isValidDataSet(dataSet)) {
|
||||
// Frame Increment Pointer points to Frame Time (0018,1063) field or is not defined (for addtional flexibility).
|
||||
// Yet another value is possible for this field (5200,9230 for Multi-frame Functional Groups)
|
||||
// but that case is currently not supported.
|
||||
return dataSet.floatString('x00181063', -1);
|
||||
this.imageIdToUIDs.set(imageId, uids);
|
||||
}
|
||||
|
||||
_getAndCacheStudy(StudyInstanceUID) {
|
||||
const studies = this.studies;
|
||||
|
||||
let study = studies.get(StudyInstanceUID);
|
||||
|
||||
if (!study) {
|
||||
study = { series: new Map() };
|
||||
studies.set(StudyInstanceUID, study);
|
||||
}
|
||||
|
||||
return image.instance.frameTime;
|
||||
return study;
|
||||
}
|
||||
_getAndCacheSeriesFromStudy(study, SeriesInstanceUID) {
|
||||
let series = study.series.get(SeriesInstanceUID);
|
||||
|
||||
if (!series) {
|
||||
series = { instances: new Map() };
|
||||
study.series.set(SeriesInstanceUID, series);
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the related metadata for missing fields given a specified image
|
||||
*
|
||||
* @param image
|
||||
*/
|
||||
updateMetadata(image) {
|
||||
const imageMetadata = this.metadataLookup.get(image.imageId);
|
||||
if (!imageMetadata) {
|
||||
_getAndCacheInstanceFromStudy(series, SOPInstanceUID) {
|
||||
let instance = series.instances.get(SOPInstanceUID);
|
||||
|
||||
if (!instance) {
|
||||
instance = {};
|
||||
series.instances.set(SOPInstanceUID, instance);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
async _checkBulkDataAndInlineBinaries(instance, server) {
|
||||
await fetchOverlayData(instance, server);
|
||||
|
||||
if (instance.PhotometricInterpretation === 'PALETTE COLOR') {
|
||||
await fetchPaletteColorLookupTableData(instance, server);
|
||||
}
|
||||
}
|
||||
|
||||
_getInstance(imageId) {
|
||||
const uids = this._getUIDsFromImageID(imageId);
|
||||
|
||||
if (!uids) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageMetadata.patient.age =
|
||||
imageMetadata.patient.age ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00101010');
|
||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = uids;
|
||||
|
||||
imageMetadata.instance.rows = imageMetadata.instance.rows || image.rows;
|
||||
imageMetadata.instance.columns =
|
||||
imageMetadata.instance.columns || image.columns;
|
||||
|
||||
imageMetadata.instance.sopClassUid =
|
||||
imageMetadata.instance.sopClassUid ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00080016');
|
||||
imageMetadata.instance.sopInstanceUid =
|
||||
imageMetadata.instance.sopInstanceUid ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00080018');
|
||||
|
||||
imageMetadata.instance.pixelSpacing =
|
||||
imageMetadata.instance.pixelSpacing ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00280030');
|
||||
imageMetadata.instance.frameOfReferenceUID =
|
||||
imageMetadata.instance.frameOfReferenceUID ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00200052');
|
||||
imageMetadata.instance.imageOrientationPatient =
|
||||
imageMetadata.instance.imageOrientationPatient ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00200037');
|
||||
imageMetadata.instance.imagePositionPatient =
|
||||
imageMetadata.instance.imagePositionPatient ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00200032');
|
||||
|
||||
imageMetadata.instance.sliceThickness =
|
||||
imageMetadata.instance.sliceThickness ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00180050');
|
||||
imageMetadata.instance.sliceLocation =
|
||||
imageMetadata.instance.sliceLocation ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00201041');
|
||||
imageMetadata.instance.tablePosition =
|
||||
imageMetadata.instance.tablePosition ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00189327');
|
||||
imageMetadata.instance.spacingBetweenSlices =
|
||||
imageMetadata.instance.spacingBetweenSlices ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00180088');
|
||||
|
||||
imageMetadata.instance.lossyImageCompression =
|
||||
imageMetadata.instance.lossyImageCompression ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00282110');
|
||||
imageMetadata.instance.lossyImageCompressionRatio =
|
||||
imageMetadata.instance.lossyImageCompressionRatio ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00282112');
|
||||
|
||||
imageMetadata.instance.frameIncrementPointer =
|
||||
imageMetadata.instance.frameIncrementPointer ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00280009');
|
||||
imageMetadata.instance.frameTime =
|
||||
imageMetadata.instance.frameTime ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00181063');
|
||||
imageMetadata.instance.frameTimeVector =
|
||||
imageMetadata.instance.frameTimeVector ||
|
||||
this.getFromDataSet(image.data, 'string', 'x00181065');
|
||||
|
||||
if (
|
||||
(image.data || image.instance) &&
|
||||
!imageMetadata.instance.multiframeMetadata
|
||||
) {
|
||||
imageMetadata.instance.multiframeMetadata = this.getMultiframeModuleMetadata(
|
||||
image
|
||||
);
|
||||
}
|
||||
|
||||
imageMetadata.imagePlane =
|
||||
imageMetadata.imagePlane || this.getImagePlane(imageMetadata.instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs and returns the imagePlane given the metadata instance
|
||||
*
|
||||
* @param metadataInstance The metadata instance (InstanceMetadata class) containing information to construct imagePlane
|
||||
* @returns imagePlane The constructed imagePlane to be used in viewer easily
|
||||
*/
|
||||
getImagePlane(instance) {
|
||||
if (
|
||||
!instance.rows ||
|
||||
!instance.columns ||
|
||||
!instance.pixelSpacing ||
|
||||
!instance.frameOfReferenceUID ||
|
||||
!instance.imageOrientationPatient ||
|
||||
!instance.imagePositionPatient
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageOrientation = instance.imageOrientationPatient.split('\\');
|
||||
const imagePosition = instance.imagePositionPatient.split('\\');
|
||||
|
||||
let columnPixelSpacing = 1.0;
|
||||
let rowPixelSpacing = 1.0;
|
||||
if (instance.pixelSpacing) {
|
||||
const split = instance.pixelSpacing.split('\\');
|
||||
rowPixelSpacing = parseFloat(split[0]);
|
||||
columnPixelSpacing = parseFloat(split[1]);
|
||||
}
|
||||
|
||||
return {
|
||||
frameOfReferenceUID: instance.frameOfReferenceUID,
|
||||
rows: instance.rows,
|
||||
columns: instance.columns,
|
||||
rowCosines: [
|
||||
parseFloat(imageOrientation[0]),
|
||||
parseFloat(imageOrientation[1]),
|
||||
parseFloat(imageOrientation[2]),
|
||||
],
|
||||
columnCosines: [
|
||||
parseFloat(imageOrientation[3]),
|
||||
parseFloat(imageOrientation[4]),
|
||||
parseFloat(imageOrientation[5]),
|
||||
],
|
||||
imagePositionPatient: [
|
||||
parseFloat(imagePosition[0]),
|
||||
parseFloat(imagePosition[1]),
|
||||
parseFloat(imagePosition[2]),
|
||||
],
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This function extracts miltiframe information from a dicomParser.DataSet object.
|
||||
*
|
||||
* @param dataSet {Object} An instance of dicomParser.DataSet object where multiframe information can be found.
|
||||
* @return {Object} An object containing multiframe image metadata (frameIncrementPointer, frameTime, frameTimeVector, etc).
|
||||
*/
|
||||
getMultiframeModuleMetadata(image) {
|
||||
const imageInfo = {
|
||||
isMultiframeImage: false,
|
||||
frameIncrementPointer: null,
|
||||
numberOfFrames: 0,
|
||||
frameTime: 0,
|
||||
frameTimeVector: null,
|
||||
averageFrameRate: 0, // backwards compatibility only... it might be useless in the future
|
||||
};
|
||||
|
||||
let frameTime;
|
||||
|
||||
const numberOfFrames = this.getFromImage(
|
||||
image,
|
||||
'intString',
|
||||
'x00280008',
|
||||
'numberOfFrames',
|
||||
-1
|
||||
return this._getInstanceData(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
if (numberOfFrames > 0) {
|
||||
// set multi-frame image indicator
|
||||
imageInfo.isMultiframeImage = true;
|
||||
imageInfo.numberOfFrames = numberOfFrames;
|
||||
get(query, imageId, options = { fallback: false }) {
|
||||
const instance = this._getInstance(imageId);
|
||||
|
||||
// (0028,0009) = Frame Increment Pointer
|
||||
const frameIncrementPointer = this.getFrameIncrementPointer(image);
|
||||
if (query === INSTANCE) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (frameIncrementPointer === 'frameTimeVector') {
|
||||
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
|
||||
const frameTimeVector = this.getFrameTimeVector(image);
|
||||
return this.getTagFromInstance(query, instance, options);
|
||||
}
|
||||
|
||||
if (frameTimeVector instanceof Array && frameTimeVector.length > 0) {
|
||||
imageInfo.frameIncrementPointer = frameIncrementPointer;
|
||||
imageInfo.frameTimeVector = frameTimeVector;
|
||||
frameTime =
|
||||
frameTimeVector.reduce((a, b) => a + b) / frameTimeVector.length;
|
||||
imageInfo.averageFrameRate = 1000 / frameTime;
|
||||
}
|
||||
} else if (
|
||||
frameIncrementPointer === 'frameTime' ||
|
||||
frameIncrementPointer === ''
|
||||
) {
|
||||
frameTime = this.getFrameTime(image);
|
||||
getTag(query, imageId, options) {
|
||||
return this.get(query, imageId, options);
|
||||
}
|
||||
|
||||
if (frameTime > 0) {
|
||||
imageInfo.frameIncrementPointer = frameIncrementPointer;
|
||||
imageInfo.frameTime = frameTime;
|
||||
imageInfo.averageFrameRate = 1000 / frameTime;
|
||||
}
|
||||
getInstance(imageId) {
|
||||
return this.get(INSTANCE, imageId);
|
||||
}
|
||||
|
||||
getTagFromInstance(
|
||||
naturalizedTagOrWADOImageLoaderTag,
|
||||
instance,
|
||||
options = { fallback: false }
|
||||
) {
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If its a naturalized dcmjs tag present on the instance, return.
|
||||
if (instance[naturalizedTagOrWADOImageLoaderTag]) {
|
||||
return instance[naturalizedTagOrWADOImageLoaderTag];
|
||||
}
|
||||
|
||||
if (options.fallback) {
|
||||
// Perhaps the tag has fallbacks?
|
||||
const fallbackTag = getFallbackTagFromInstance(
|
||||
naturalizedTagOrWADOImageLoaderTag,
|
||||
instance
|
||||
);
|
||||
|
||||
if (fallbackTag) {
|
||||
return fallbackTag;
|
||||
}
|
||||
}
|
||||
|
||||
return imageInfo;
|
||||
// Maybe its a legacy CornerstoneWADOImageLoader tag then:
|
||||
return this._getCornerstoneWADOImageLoaderTag(
|
||||
naturalizedTagOrWADOImageLoaderTag,
|
||||
instance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bound reference to the provider function.
|
||||
*/
|
||||
getProvider() {
|
||||
let provider = this._provider;
|
||||
if (typeof this._provider !== FUNCTION) {
|
||||
provider = this.provider.bind(this);
|
||||
this._provider = provider;
|
||||
_getCornerstoneWADOImageLoaderTag(wadoImageLoaderTag, instance) {
|
||||
let metadata;
|
||||
|
||||
switch (wadoImageLoaderTag) {
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_SERIES_MODULE:
|
||||
const { SeriesDate, SeriesTime } = instance;
|
||||
|
||||
let seriesDate;
|
||||
let seriesTime;
|
||||
|
||||
if (SeriesDate) {
|
||||
seriesDate = dicomParser.parseDA(SeriesDate);
|
||||
}
|
||||
|
||||
if (SeriesTime) {
|
||||
seriesTime = dicomParser.parseTM(SeriesTime);
|
||||
}
|
||||
|
||||
metadata = {
|
||||
modality: instance.Modality,
|
||||
seriesInstanceUID: instance.SeriesInstanceUID,
|
||||
seriesNumber: instance.SeriesNumber,
|
||||
studyInstanceUID: instance.StudyInstanceUID,
|
||||
seriesDate,
|
||||
seriesTime,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
|
||||
metadata = {
|
||||
patientAge: instance.PatientAge,
|
||||
patientSize: instance.PatientSize,
|
||||
patientWeight: instance.PatientWeight,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE:
|
||||
const { ImageOrientationPatient } = instance;
|
||||
|
||||
// Fallback for DX images.
|
||||
const PixelSpacing = getFallbackTagFromInstance(
|
||||
'PixelSpacing',
|
||||
instance
|
||||
);
|
||||
|
||||
let rowPixelSpacing;
|
||||
let columnPixelSpacing;
|
||||
|
||||
let rowCosines;
|
||||
let columnCosines;
|
||||
|
||||
if (PixelSpacing) {
|
||||
rowPixelSpacing = PixelSpacing[0];
|
||||
columnPixelSpacing = PixelSpacing[1];
|
||||
}
|
||||
|
||||
if (ImageOrientationPatient) {
|
||||
rowCosines = ImageOrientationPatient.slice(0, 3);
|
||||
columnCosines = ImageOrientationPatient.slice(3, 6);
|
||||
}
|
||||
|
||||
metadata = {
|
||||
frameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
imageOrientationPatient: ImageOrientationPatient,
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
imagePositionPatient: instance.ImagePositionPatient,
|
||||
sliceThickness: instance.SliceThickness,
|
||||
sliceLocation: instance.SliceLocation,
|
||||
pixelSpacing: PixelSpacing,
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
|
||||
metadata = {
|
||||
samplesPerPixel: instance.SamplesPerPixel,
|
||||
photometricInterpretation: instance.PhotometricInterpretation,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
bitsAllocated: instance.BitsAllocated,
|
||||
bitsStored: instance.BitsStored,
|
||||
highBit: instance.HighBit,
|
||||
pixelRepresentation: instance.PixelRepresentation,
|
||||
planarConfiguration: instance.PlanarConfiguration,
|
||||
pixelAspectRatio: instance.PixelAspectRatio,
|
||||
smallestPixelValue: instance.SmallestPixelValue,
|
||||
largestPixelValue: instance.LargestPixelValue,
|
||||
redPaletteColorLookupTableDescriptor:
|
||||
instance.RedPaletteColorLookupTableDescriptor,
|
||||
greenPaletteColorLookupTableDescriptor:
|
||||
instance.GreenPaletteColorLookupTableDescriptor,
|
||||
bluePaletteColorLookupTableDescriptor:
|
||||
instance.BluePaletteColorLookupTableDescriptor,
|
||||
redPaletteColorLookupTableData:
|
||||
instance.RedPaletteColorLookupTableData,
|
||||
greenPaletteColorLookupTableData:
|
||||
instance.GreenPaletteColorLookupTableData,
|
||||
bluePaletteColorLookupTableData:
|
||||
instance.BluePaletteColorLookupTableData,
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
|
||||
const { WindowCenter, WindowWidth } = instance;
|
||||
|
||||
const windowCenter = Array.isArray(WindowCenter)
|
||||
? WindowCenter
|
||||
: [WindowCenter];
|
||||
const windowWidth = Array.isArray(WindowWidth)
|
||||
? WindowWidth
|
||||
: [WindowWidth];
|
||||
|
||||
metadata = {
|
||||
windowCenter,
|
||||
windowWidth,
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
|
||||
metadata = {
|
||||
rescaleIntercept: instance.RescaleIntercept,
|
||||
rescaleSlope: instance.RescaleSlope,
|
||||
rescaleType: instance.RescaleType,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.SOP_COMMON_MODULE:
|
||||
metadata = {
|
||||
sopClassUID: instance.SOPClassUID,
|
||||
sopInstanceUID: instance.SOPInstanceUID,
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PET_ISOTOPE_MODULE:
|
||||
const { RadiopharmaceuticalInformationSequence } = instance;
|
||||
|
||||
if (RadiopharmaceuticalInformationSequence) {
|
||||
const RadiopharmaceuticalInformation = Array.isArray(
|
||||
RadiopharmaceuticalInformationSequence
|
||||
)
|
||||
? RadiopharmaceuticalInformationSequence[0]
|
||||
: RadiopharmaceuticalInformationSequence;
|
||||
|
||||
const {
|
||||
RadiopharmaceuticalStartTime,
|
||||
RadionuclideTotalDose,
|
||||
RadionuclideHalfLife,
|
||||
} = RadiopharmaceuticalInformation;
|
||||
|
||||
const radiopharmaceuticalInfo = {
|
||||
radiopharmaceuticalStartTime: dicomParser.parseTM(
|
||||
RadiopharmaceuticalStartTime
|
||||
),
|
||||
radionuclideTotalDose: RadionuclideTotalDose,
|
||||
radionuclideHalfLife: RadionuclideHalfLife,
|
||||
};
|
||||
metadata = {
|
||||
radiopharmaceuticalInfo,
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.OVERLAY_PLANE_MODULE:
|
||||
metadata = [];
|
||||
|
||||
for (
|
||||
let overlayGroup = 0x00;
|
||||
overlayGroup <= 0x1e;
|
||||
overlayGroup += 0x02
|
||||
) {
|
||||
let groupStr = `60${overlayGroup.toString(16)}`;
|
||||
|
||||
if (groupStr.length === 3) {
|
||||
groupStr = `600${overlayGroup.toString(16)}`;
|
||||
}
|
||||
|
||||
const OverlayDataTag = `${groupStr}3000`;
|
||||
const OverlayData = instance[OverlayDataTag];
|
||||
|
||||
if (!OverlayData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const OverlayRowsTag = `${groupStr}0010`;
|
||||
const OverlayColumnsTag = `${groupStr}0011`;
|
||||
const OverlayType = `${groupStr}0040`;
|
||||
const OverlayOriginTag = `${groupStr}0050`;
|
||||
const OverlayDescriptionTag = `${groupStr}0022`;
|
||||
const OverlayLabelTag = `${groupStr}1500`;
|
||||
const ROIAreaTag = `${groupStr}1301`;
|
||||
const ROIMeanTag = `${groupStr}1302`;
|
||||
const ROIStandardDeviationTag = `${groupStr}1303`;
|
||||
const OverlayOrigin = instance[OverlayOriginTag];
|
||||
|
||||
const overlay = {
|
||||
rows: instance[OverlayRowsTag],
|
||||
columns: instance[OverlayColumnsTag],
|
||||
type: instance[OverlayType],
|
||||
x: OverlayOrigin[0],
|
||||
y: OverlayOrigin[1],
|
||||
pixelData: OverlayData,
|
||||
description: instance[OverlayDescriptionTag],
|
||||
label: instance[OverlayLabelTag],
|
||||
roiArea: instance[ROIAreaTag],
|
||||
roiMean: instance[ROIMeanTag],
|
||||
roiStandardDeviation: instance[ROIStandardDeviationTag],
|
||||
};
|
||||
|
||||
metadata.push(overlay);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_MODULE:
|
||||
const { PatientName } = instance;
|
||||
|
||||
let patientName;
|
||||
if (PatientName) {
|
||||
patientName = PatientName.Alphabetic;
|
||||
}
|
||||
|
||||
metadata = {
|
||||
patientName,
|
||||
patientId: instance.PatientID,
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
||||
metadata = {
|
||||
instanceNumber: instance.InstanceNumber,
|
||||
lossyImageCompression: instance.LossyImageCompression,
|
||||
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
||||
lossyImageCompressionMethod: instance.LossyImageCompressionMethod,
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_STUDY_MODULE:
|
||||
metadata = {
|
||||
studyDescription: instance.StudyDescription,
|
||||
studyDate: instance.StudyDate,
|
||||
studyTime: instance.StudyTime,
|
||||
accessionNumber: instance.AccessionNumber,
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.CINE_MODULE:
|
||||
metadata = {
|
||||
frameTime: instance.FrameTime,
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return provider;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up metadata for Cornerstone Tools given a specified type and imageId
|
||||
* A type may be, e.g. 'study', or 'patient', or 'imagePlaneModule'. These types
|
||||
* are keys in the stored metadata objects.
|
||||
*
|
||||
* @param type
|
||||
* @param imageId
|
||||
* @returns {Object} Relevant metadata of the specified type
|
||||
*/
|
||||
provider(type, imageId) {
|
||||
const imageMetadata = this.metadataLookup.get(imageId);
|
||||
if (!imageMetadata) {
|
||||
_getInstanceData(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
||||
const study = this.studies.get(StudyInstanceUID);
|
||||
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageMetadata.hasOwnProperty(type)) {
|
||||
return imageMetadata[type];
|
||||
const series = study.series.get(SeriesInstanceUID);
|
||||
|
||||
if (!series) {
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = series.instances.get(SOPInstanceUID);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
_getUIDsFromImageID(imageId) {
|
||||
if (imageId.includes('wadors:')) {
|
||||
const strippedImageId = imageId.split('studies/')[1];
|
||||
const splitImageId = strippedImageId.split('/');
|
||||
|
||||
return {
|
||||
StudyInstanceUID: splitImageId[0], // Note: splitImageId[1] === 'series'
|
||||
SeriesInstanceUID: splitImageId[2], // Note: splitImageId[3] === 'instances'
|
||||
SOPInstanceUID: splitImageId[4],
|
||||
};
|
||||
}
|
||||
if (imageId.includes('wado?requestType=WADO')) {
|
||||
const qs = queryString.parse(imageId);
|
||||
|
||||
return {
|
||||
StudyInstanceUID: qs.studyUID,
|
||||
SeriesInstanceUID: qs.seriesUID,
|
||||
SOPInstanceUID: qs.objectUID,
|
||||
};
|
||||
} else {
|
||||
// Maybe its a non-standard imageId
|
||||
return this.imageIdToUIDs.get(imageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MetadataProvider;
|
||||
const metadataProvider = new MetadataProvider();
|
||||
|
||||
export default metadataProvider;
|
||||
|
||||
const WADO_IMAGE_LOADER_TAGS = {
|
||||
// CornerstoneWADOImageLoader specific
|
||||
GENERAL_SERIES_MODULE: 'generalSeriesModule',
|
||||
PATIENT_STUDY_MODULE: 'patientStudyModule',
|
||||
IMAGE_PLANE_MODULE: 'imagePlaneModule',
|
||||
IMAGE_PIXEL_MODULE: 'imagePixelModule',
|
||||
VOI_LUT_MODULE: 'voiLutModule',
|
||||
MODALITY_LUT_MODULE: 'modalityLutModule',
|
||||
SOP_COMMON_MODULE: 'sopCommonModule',
|
||||
PET_ISOTOPE_MODULE: 'petIsotopeModule',
|
||||
OVERLAY_PLANE_MODULE: 'overlayPlaneModule',
|
||||
|
||||
// react-cornerstone-viewport specifc
|
||||
PATIENT_MODULE: 'patientModule',
|
||||
GENERAL_IMAGE_MODULE: 'generalImageModule',
|
||||
GENERAL_STUDY_MODULE: 'generalStudyModule',
|
||||
CINE_MODULE: 'cineModule',
|
||||
};
|
||||
|
||||
const INSTANCE = 'instance';
|
||||
@@ -1,9 +1,8 @@
|
||||
import { studyMetadataManager, updateMetaDataManager } from '../utils';
|
||||
import { studyMetadataManager } from '../utils';
|
||||
|
||||
import OHIFError from './OHIFError';
|
||||
import { StudyMetadata } from './metadata/StudyMetadata';
|
||||
import { StudyMetadataSource } from './StudyMetadataSource.js';
|
||||
import { StudySummary } from './metadata/StudySummary';
|
||||
import { retrieveStudyMetadata } from '../studies/retrieveStudyMetadata.js';
|
||||
|
||||
export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
@@ -19,12 +18,12 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
|
||||
/**
|
||||
* Load study info (OHIF.viewer.Studies) and study metadata (OHIF.viewer.StudyMetadataList) for a given study.
|
||||
* @param {StudySummary|StudyMetadata} study of StudySummary or StudyMetadata object.
|
||||
* @param {StudyMetadata} study StudyMetadata object.
|
||||
*/
|
||||
loadStudy(study) {
|
||||
if (!(study instanceof StudyMetadata) && !(study instanceof StudySummary)) {
|
||||
if (!(study instanceof StudyMetadata)) {
|
||||
throw new OHIFError(
|
||||
'OHIFStudyMetadataSource::loadStudy study is not an instance of StudySummary or StudyMetadata'
|
||||
'OHIFStudyMetadataSource::loadStudy study is not an instance of StudyMetadata'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +32,7 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
|
||||
if (study instanceof StudyMetadata) {
|
||||
const alreadyLoaded = OHIF.viewer.Studies.findBy({
|
||||
studyInstanceUid: studyInstanceUID,
|
||||
StudyInstanceUID: studyInstanceUID,
|
||||
});
|
||||
|
||||
if (!alreadyLoaded) {
|
||||
@@ -49,7 +48,7 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
// Create study metadata object
|
||||
const studyMetadata = new StudyMetadata(
|
||||
studyInfo,
|
||||
studyInfo.studyInstanceUid
|
||||
studyInfo.StudyInstanceUID
|
||||
);
|
||||
|
||||
// Get Study display sets
|
||||
@@ -72,10 +71,6 @@ export class OHIFStudyMetadataSource extends StudyMetadataSource {
|
||||
// Set some studyInfo properties
|
||||
studyInfo.selected = true;
|
||||
studyInfo.displaySets = studyMetadata.getDisplaySets();
|
||||
|
||||
// Updates WADO-RS metaDataManager
|
||||
updateMetaDataManager(studyInfo);
|
||||
|
||||
studyMetadataManager.add(studyMetadata);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import StudySummary from './StudySummary';
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const STRING = 'string';
|
||||
const propertyReplacementMap = {
|
||||
modalities: 'ModalitiesInStudy',
|
||||
patientBirthdate: 'PatientBirthDate',
|
||||
};
|
||||
|
||||
/**
|
||||
* OHIF Viewers specialized version of StudySummary class
|
||||
*/
|
||||
export class OHIFStudySummary extends StudySummary {
|
||||
// @Override
|
||||
addTags(tagMap) {
|
||||
const _hasOwn = Object.prototype.hasOwnProperty;
|
||||
const _tagMap = Object.create(null);
|
||||
for (let property in tagMap) {
|
||||
if (_hasOwn.call(tagMap, property)) {
|
||||
let standardProperty = OHIFStudySummary.getStandardPropertyName(
|
||||
property
|
||||
);
|
||||
if (standardProperty) {
|
||||
_tagMap[standardProperty] = tagMap[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
super.addTags(_tagMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a non-standard, OHIF specific, DICOM property name into a standard one.
|
||||
* @param {string} property A string representing a non-conforming keyword.
|
||||
* @returns {string|undefined} Returns a standard-conforming property name.
|
||||
*/
|
||||
static getStandardPropertyName(property) {
|
||||
let standardProperty;
|
||||
if (typeof property === STRING && property.charAt(0) !== '_') {
|
||||
if (property in propertyReplacementMap) {
|
||||
standardProperty = propertyReplacementMap[propertyReplacementMap];
|
||||
} else {
|
||||
standardProperty = property
|
||||
.replace(/^sop/, 'SOP')
|
||||
.replace(/Uid$/, 'UID')
|
||||
.replace(/Id$/, 'ID');
|
||||
standardProperty =
|
||||
standardProperty.charAt(0).toUpperCase() + standardProperty.substr(1);
|
||||
}
|
||||
}
|
||||
return standardProperty;
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,8 @@ class BaseLoadingListener {
|
||||
}
|
||||
|
||||
_getProgressId() {
|
||||
const displaySetInstanceUid = this.stack.displaySetInstanceUid;
|
||||
return 'StackProgress:' + displaySetInstanceUid;
|
||||
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
||||
return 'StackProgress:' + displaySetInstanceUID;
|
||||
}
|
||||
|
||||
_clearProgress() {
|
||||
@@ -362,7 +362,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
|
||||
_logProgress() {
|
||||
const totalFramesCount = this.stack.imageIds.length;
|
||||
const displaySetInstanceUid = this.stack.displaySetInstanceUid;
|
||||
const displaySetInstanceUID = this.stack.displaySetInstanceUID;
|
||||
let progressBar = '[';
|
||||
|
||||
for (let i = 0; i < totalFramesCount; i++) {
|
||||
@@ -371,7 +371,7 @@ class StackLoadingListener extends BaseLoadingListener {
|
||||
}
|
||||
|
||||
progressBar += ']';
|
||||
log.info(`${displaySetInstanceUid}: ${progressBar}`);
|
||||
log.info(`${displaySetInstanceUID}: ${progressBar}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,12 +388,12 @@ class StudyLoadingListener {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySetInstanceUid = stack.displaySetInstanceUid;
|
||||
const displaySetInstanceUID = stack.displaySetInstanceUID;
|
||||
|
||||
if (!this.listeners[displaySetInstanceUid]) {
|
||||
if (!this.listeners[displaySetInstanceUID]) {
|
||||
const listener = this._createListener(stack, stackMetaData);
|
||||
if (listener) {
|
||||
this.listeners[displaySetInstanceUid] = listener;
|
||||
this.listeners[displaySetInstanceUID] = listener;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,12 +424,12 @@ class StudyLoadingListener {
|
||||
}
|
||||
|
||||
clear() {
|
||||
const displaySetInstanceUids = Object.keys(this.listeners);
|
||||
const length = displaySetInstanceUids.length;
|
||||
const displaySetInstanceUIDs = Object.keys(this.listeners);
|
||||
const length = displaySetInstanceUIDs.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const displaySetInstanceUid = displaySetInstanceUids[i];
|
||||
const displaySet = this.listeners[displaySetInstanceUid];
|
||||
const displaySetInstanceUID = displaySetInstanceUIDs[i];
|
||||
const displaySet = this.listeners[displaySetInstanceUID];
|
||||
|
||||
displaySet.destroy();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class StudyMetadataSource {
|
||||
|
||||
/**
|
||||
* Load study info and study metadata for a given study into the viewer.
|
||||
* @param {StudySummary|StudyMetadata} study of StudySummary or StudyMetadata object.
|
||||
* @param {StudyMetadata} study StudyMetadata object.
|
||||
*/
|
||||
loadStudy(study) {
|
||||
/**
|
||||
|
||||
@@ -94,17 +94,23 @@ export class StudyPrefetcher {
|
||||
}
|
||||
|
||||
getStudy(image) {
|
||||
const studyMetadata = cornerstone.metaData.get('study', image.imageId);
|
||||
const StudyInstanceUID = cornerstone.metaData.get(
|
||||
'StudyInstanceUID',
|
||||
image.imageId
|
||||
);
|
||||
return OHIF.viewer.Studies.find(
|
||||
study => study.studyInstanceUid === studyMetadata.studyInstanceUid
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
getSeries(study, image) {
|
||||
const seriesMetadata = cornerstone.metaData.get('series', image.imageId);
|
||||
const SeriesInstanceUID = cornerstone.metaData.get(
|
||||
'SeriesInstanceUID',
|
||||
image.imageId
|
||||
);
|
||||
const studyMetadata = OHIF.viewerbase.getStudyMetadata(study);
|
||||
|
||||
return studyMetadata.getSeriesByUID(seriesMetadata.seriesInstanceUid);
|
||||
return studyMetadata.getSeriesByUID(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
getInstance(series, image) {
|
||||
@@ -112,13 +118,13 @@ export class StudyPrefetcher {
|
||||
'instance',
|
||||
image.imageId
|
||||
);
|
||||
return series.getInstanceByUID(instanceMetadata.sopInstanceUid);
|
||||
return series.getInstanceByUID(instanceMetadata.SOPInstanceUID);
|
||||
}
|
||||
|
||||
getActiveDisplaySet(displaySets, instance) {
|
||||
return displaySets.find(displaySet => {
|
||||
return displaySet.images.some(displaySetImage => {
|
||||
return displaySetImage.sopInstanceUid === instance.sopInstanceUid;
|
||||
return displaySetImage.SOPInstanceUID === instance.SOPInstanceUID;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Only imported in environment w/ `window`
|
||||
// So we need to mock these for tests
|
||||
import Mousetrap from 'mousetrap';
|
||||
import pausePlugin from 'mousetrap/plugins/pause/mousetrap-pause.js';
|
||||
import recordPlugin from 'mousetrap/plugins/record/mousetrap-record.js';
|
||||
|
||||
// import pausePlugin from './pausePlugin.js';
|
||||
// import recordPlugin from './recordPlugin.js';
|
||||
|
||||
// // // TODO: May need to bind these so Mousetrap = this in plugins;
|
||||
// pausePlugin(Mousetrap);
|
||||
// recordPlugin(Mousetrap);
|
||||
|
||||
// console.log(Mousetrap);
|
||||
// console.log(Object.keys(Mousetrap));
|
||||
|
||||
export default Mousetrap;
|
||||
@@ -1,15 +1,10 @@
|
||||
import {
|
||||
InstanceMetadata,
|
||||
SeriesMetadata,
|
||||
StudyMetadata,
|
||||
StudySummary,
|
||||
} from './metadata';
|
||||
import { InstanceMetadata, SeriesMetadata, StudyMetadata } from './metadata';
|
||||
|
||||
import CommandsManager from './CommandsManager.js';
|
||||
import { DICOMFileLoadingListener } from './StudyLoadingListener';
|
||||
import HotkeysManager from './HotkeysManager.js';
|
||||
import ImageSet from './ImageSet';
|
||||
import MetadataProvider from './MetadataProvider.js';
|
||||
import MetadataProvider from './MetadataProvider';
|
||||
import OHIFError from './OHIFError.js';
|
||||
import { OHIFStudyMetadataSource } from './OHIFStudyMetadataSource';
|
||||
import { StackLoadingListener } from './StudyLoadingListener';
|
||||
@@ -31,7 +26,6 @@ export {
|
||||
StudyMetadata,
|
||||
SeriesMetadata,
|
||||
InstanceMetadata,
|
||||
StudySummary,
|
||||
TypeSafeCollection,
|
||||
OHIFError,
|
||||
StudyMetadataSource,
|
||||
@@ -50,7 +44,6 @@ const classes = {
|
||||
StudyMetadata,
|
||||
SeriesMetadata,
|
||||
InstanceMetadata,
|
||||
StudySummary,
|
||||
TypeSafeCollection,
|
||||
OHIFError,
|
||||
StudyMetadataSource,
|
||||
|
||||
@@ -8,20 +8,12 @@ import OHIFError from '../OHIFError.js';
|
||||
|
||||
const UNDEFINED = 'undefined';
|
||||
const STRING = 'string';
|
||||
const STUDY_INSTANCE_UID = 'x0020000d';
|
||||
const SERIES_INSTANCE_UID = 'x0020000e';
|
||||
|
||||
export class InstanceMetadata extends Metadata {
|
||||
constructor(data, uid) {
|
||||
super(data, uid);
|
||||
// Initialize Private Properties
|
||||
Object.defineProperties(this, {
|
||||
_sopInstanceUID: {
|
||||
configurable: true, // configurable so that it can be redefined in sub-classes...
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: null,
|
||||
},
|
||||
_imageId: {
|
||||
configurable: true, // configurable so that it can be redefined in sub-classes...
|
||||
enumerable: false,
|
||||
@@ -43,14 +35,14 @@ export class InstanceMetadata extends Metadata {
|
||||
*/
|
||||
_definePublicProperties() {
|
||||
/**
|
||||
* Property: this.sopInstanceUID
|
||||
* Property: this.SOPInstanceUID
|
||||
* Same as this.getSOPInstanceUID()
|
||||
* It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
|
||||
* sopInstanceCollection.findBy({
|
||||
* sopInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
|
||||
* SOPInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
|
||||
* });
|
||||
*/
|
||||
Object.defineProperty(this, 'sopInstanceUID', {
|
||||
Object.defineProperty(this, 'SOPInstanceUID', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
get: function() {
|
||||
@@ -67,21 +59,21 @@ export class InstanceMetadata extends Metadata {
|
||||
* Returns the StudyInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
|
||||
*/
|
||||
getStudyInstanceUID() {
|
||||
return this.getTagValue(STUDY_INSTANCE_UID, null);
|
||||
return this.getTagValue('StudyInstanceUID', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SeriesInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
|
||||
*/
|
||||
getSeriesInstanceUID() {
|
||||
return this.getTagValue(SERIES_INSTANCE_UID, null);
|
||||
return this.getTagValue('SeriesInstanceUID', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SOPInstanceUID of the current instance.
|
||||
*/
|
||||
getSOPInstanceUID() {
|
||||
return this._sopInstanceUID;
|
||||
return this.getTagValue('SOPInstanceUID', null);
|
||||
}
|
||||
|
||||
// @TODO: Improve this... (E.g.: blob data)
|
||||
@@ -127,13 +119,6 @@ export class InstanceMetadata extends Metadata {
|
||||
return typeof value === STRING ? parseInt(value) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use getTagValue instead.
|
||||
*/
|
||||
getRawValue(tagOrProperty, defaultValue) {
|
||||
return this.getTagValue(tagOrProperty, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function should be overriden by specialized classes in order to allow client libraries or viewers to take advantage of the Study Metadata API.
|
||||
*/
|
||||
|
||||
Loaded 100 of 370 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user