Compare commits
100
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
719bfd02eb | ||
|
|
994818faeb | ||
|
|
f06ef53e83 | ||
|
|
c7008621e4 | ||
|
|
55a580659e | ||
|
|
fd7620ced9 | ||
|
|
cd1cbf0f85 | ||
|
|
08055398de | ||
|
|
f6db7fade6 | ||
|
|
58d2bf1d12 | ||
|
|
87f5eca659 | ||
|
|
922ba8cc42 | ||
|
|
777ff88f3d | ||
|
|
68858db4b6 | ||
|
|
ab106afae3 | ||
|
|
ac69b0a656 | ||
|
|
8edba24eec | ||
|
|
aae232fc3e | ||
|
|
8fad8ceb80 | ||
|
|
f00f48da2b | ||
|
|
076b89ef83 | ||
|
|
4920b527d1 | ||
|
|
482041f053 | ||
|
|
674edf85f0 | ||
|
|
33a1a465ea | ||
|
|
6e04de30b6 | ||
|
|
19117167ac | ||
|
|
22e3c5d953 | ||
|
|
9a34877be4 | ||
|
|
b1b66429b6 | ||
|
|
78e522fbf6 | ||
|
|
830cb82965 | ||
|
|
9a71edda6a | ||
|
|
7a8ae23839 | ||
|
|
6873c4f065 | ||
|
|
6ea16eccd9 | ||
|
|
ff381c6521 | ||
|
|
26cd695b8b | ||
|
|
a4f1b2195c | ||
|
|
c8df17b032 |
No files matched your search
+60
-27
@@ -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:
|
||||
@@ -341,6 +334,8 @@ workflows:
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/videos
|
||||
requires:
|
||||
- UNIT_TESTS
|
||||
# E2E: script-tag
|
||||
@@ -364,9 +359,39 @@ workflows:
|
||||
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 +444,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
|
||||
File renamed without changes.
+1
-1
@@ -9,7 +9,7 @@ daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- 'Story :raised_hands:'
|
||||
- 'Bug: Verified :bug:'
|
||||
- 'Task: CI/Tooling 🤖'
|
||||
- 'Task: CI/Tooling :robot:'
|
||||
- 'Task: Docs 📖'
|
||||
- 'Task: Docs :book:'
|
||||
- 'Task: Refactor 🛠'
|
||||
|
||||
@@ -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)
|
||||
@@ -84,7 +84,7 @@ We offer support through
|
||||
|
||||
For commercial support, academic collaberations, and answers to common
|
||||
questions; please read our
|
||||
[documented FAQ](https://docs.ohif.org/frequently-asked-questions.html#does-ohif-offer-commercial-support).
|
||||
[documented FAQ](https://docs.ohif.org/faq/index.html#does-ohif-offer-commercial-support).
|
||||
|
||||
## Quick Start Deployment
|
||||
|
||||
@@ -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
|
||||
@@ -296,14 +296,14 @@ MIT © [OHIF](https://github.com/OHIF)
|
||||
[monorepo]: https://en.wikipedia.org/wiki/Monorepo
|
||||
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo
|
||||
[how-to-clone]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork
|
||||
[ohif-architecture]: https://docs.ohif.org/advanced/architecture.html
|
||||
[ohif-extensions]: https://docs.ohif.org/advanced/architecture.html
|
||||
[ohif-architecture]: https://docs.ohif.org/architecture/index.html
|
||||
[ohif-extensions]: https://docs.ohif.org/architecture/index.html
|
||||
[deployment-docs]: https://docs.ohif.org/deployment/
|
||||
[react-url]: https://reactjs.org/
|
||||
[pwa-url]: https://developers.google.com/web/progressive-web-apps/
|
||||
[ohif-viewer-url]: https://www.npmjs.com/package/@ohif/viewer
|
||||
[configuration-url]: https://docs.ohif.org/essentials/configuration.html
|
||||
[extensions-url]: https://docs.ohif.org/advanced/extensions.html
|
||||
[configuration-url]: https://docs.ohif.org/configuring/
|
||||
[extensions-url]: https://docs.ohif.org/extensions/
|
||||
<!-- Platform -->
|
||||
[platform-core]: platform/core/README.md
|
||||
[core-npm]: https://www.npmjs.com/package/@ohif/core
|
||||
@@ -326,5 +326,4 @@ MIT © [OHIF](https://github.com/OHIF)
|
||||
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
|
||||
@@ -38,9 +38,9 @@ our integration and extension points.
|
||||
Check out these helpful links:
|
||||
|
||||
- Ready to dive into some code? Check out our
|
||||
[Getting Started Guide](./essentials/getting-started.md).
|
||||
[Getting Started Guide](./development/getting-started.md).
|
||||
- We're an active, vibrant community.
|
||||
[Learn how you can be more involved.](./contributing/index.md)
|
||||
[Learn how you can be more involved.](./development/contributing.md)
|
||||
- Feeling lost? Read our [help page](./help.md).
|
||||
|
||||
<!--
|
||||
|
||||
+18
-24
@@ -1,26 +1,20 @@
|
||||
# OHIF Viewers
|
||||
|
||||
- [Our Process](our-process.md)
|
||||
- Essentials
|
||||
- [Getting Started](essentials/getting-started.md)
|
||||
- [Installation](essentials/installation.md)
|
||||
- [Data Source](essentials/data-source.md)
|
||||
- [Configuration](essentials/configuration.md)
|
||||
- [Themeing](essentials/themeing.md)
|
||||
- [Translating](essentials/translating.md)
|
||||
- [Troubleshooting](essentials/troubleshooting.md)
|
||||
- [Scope of Project](essentials/scope-of-project.md)
|
||||
- Development
|
||||
- [Getting Started](development/getting-started.md)
|
||||
- [Contributing](development/contributing.md)
|
||||
- [Continuous Integration](development/continous-integration.md)
|
||||
- [Configuring](configuring/index.md)
|
||||
- [Data Source](configuring/data-source.md)
|
||||
|
||||
---
|
||||
|
||||
- [Advanced](advanced/index.md)
|
||||
- [Architecture](advanced/architecture.md)
|
||||
- [Overview](advanced/architecture.md#overview)
|
||||
- [Business Logic](advanced/architecture.md#business-logic)
|
||||
- [Component Library](advanced/architecture.md#react-component-library)
|
||||
- [Extensions](advanced/architecture.md#misc-extensions)
|
||||
- [Diagram](advanced/architecture.md#diagram)
|
||||
- [Common Questions](advanced/architecture.md#common-questions)
|
||||
- [Architecture](architecture/index.md)
|
||||
- [Viewer](viewer/index.md)
|
||||
- [Configuration](viewer/configuration.md)
|
||||
- [Themeing](viewer/themeing.md)
|
||||
- [Internationalization](viewer/internationalization.md)
|
||||
- [Extensions](extensions/index.md)
|
||||
- [Registering](extensions/index.md#registering-an-extension)
|
||||
- [Lifecycle Hooks](extensions/index.md#lifecycle-hooks)
|
||||
@@ -31,15 +25,15 @@
|
||||
- [SOP Class Handler](extensions/modules/sop-class-handler.md)
|
||||
- [Toolbar](extensions/modules/toolbar.md)
|
||||
- [Viewport](extensions/modules/viewport.md)
|
||||
- [Contexts](extension/index.md#contexts)
|
||||
- [OHIF Maintained](extension/index.md#maintained-extensions)
|
||||
- [Contexts](extensions/index.md#contexts)
|
||||
- [ExtensionManager](extensions/index.md#extensionmanager)
|
||||
- [OHIF Maintained](extensions/index.md#maintained-extensions)
|
||||
- [Services](services/index.md)
|
||||
- [Default](services/default/index.md)
|
||||
- [UI](services/ui/index.md)
|
||||
- [Dialog Service](services/ui/ui-dialog-service.md)
|
||||
- [Modal Service](services/ui/ui-modal-service.md)
|
||||
- [Notification Service](services/ui/ui-notification-service.md)
|
||||
- [Custom Tools](advanced/custom-tools.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -59,8 +53,8 @@
|
||||
|
||||
---
|
||||
|
||||
- [FAQ](frequently-asked-questions.md)
|
||||
- [Contributing](contributing/index.md)
|
||||
- [Continuous Integration](continous-integration.md)
|
||||
- [Browser Support](browser-support.md)
|
||||
- [FAQ](faq/index.md)
|
||||
- [Scope of Project](faq/scope-of-project.md)
|
||||
- [Browser Support](faq/browser-support.md)
|
||||
- [PWA vs Packaged](faq/pwa-vs-packaged.md)
|
||||
- [Help](help.md)
|
||||
@@ -1,62 +0,0 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Extension</th>
|
||||
<th>Description</th>
|
||||
<th>Modules</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- CORNERSTONE.js -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-cornerstone">
|
||||
Cornerstone
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
A viewport powered by <code>cornerstone.js</code>. Adds support for 2D DICOM rendering and manipulation, as well as support for the tools features in <a href="https://tools.cornerstonejs.org/examples/"><code>cornerstone-tools</code></a>. Also adds "CINE Dialog" to the Toolbar.
|
||||
</td>
|
||||
<td>Viewport, Toolbar</td>
|
||||
</tr>
|
||||
<!-- VTK.js -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-vtk">
|
||||
VTK.js
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
A viewport powered by <code>vtk.js</code>. Adds support for volume renderings and advanced features like MPR. Also adds "3D Rotate" to the Toolbar.
|
||||
</td>
|
||||
<td>Viewport, Toolbar</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-html">DICOM HTML</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders text and HTML content for <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12">specific SopClassUIDs</a>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-pdf">DICOM PDF</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders PDFs for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6">specific SopClassUID</a>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-microscopy">DICOM Microscopy</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders Microscopy images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7">specific SopClassUID</a>.
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1,9 +0,0 @@
|
||||
# Tool Management
|
||||
|
||||
This is not yet exposed in an easy/convenient way. Most tools are currently
|
||||
added by creating new Viewport, Toolbar, and SOPInstanceHandler extension
|
||||
modules. You can read more about that approach in
|
||||
[extensions](../extensions/index.md).
|
||||
|
||||
In the near future, we intend to improve the extensibility of tools for existing
|
||||
Viewports (like our Cornerstone.js and VTK.js viewports).
|
||||
@@ -1,3 +0,0 @@
|
||||
# Advanced
|
||||
|
||||
Advanced topics go beyond basic configuration and deployment. Their goal is to provide insight into this project's architecture and guidance on leveraging extensions.
|
||||
@@ -1,23 +0,0 @@
|
||||
# MonoRepos: A Crash Course
|
||||
|
||||
- [Lerna][lerna]
|
||||
|
||||
Solutions:
|
||||
|
||||
## Semantic-Release
|
||||
|
||||
- [Semantic-Release](https://github.com/semantic-release/semantic-release/issues/193#issuecomment-462063871)
|
||||
- [Multi-semantic-release](https://github.com/dhoulb/multi-semantic-release)
|
||||
- [semantic-release-monorepo](https://github.com/Updater/semantic-release-monorepo)
|
||||
|
||||
# Netlify
|
||||
|
||||
- https://community.netlify.com/t/best-practices-for-deploying-sites-from-monorepos/818
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[lerna]: https://github.com/lerna/lerna
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -10,6 +10,13 @@ allows our code to be used in more ways, it also increases complexity. Below, we
|
||||
aim to demistify that complexity by providing insight into how our Viewer is
|
||||
architected, and the role each of it's dependent libraries plays.
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Business Logic](#business-logic)
|
||||
- [Component Library](#react-component-library)
|
||||
- [Internationalization](#internationalization-i18n)
|
||||
- [Extensions & Configuration](#extensions--configuration)
|
||||
- [Common Questions](#common-questions)
|
||||
|
||||
## Overview
|
||||
|
||||
The [OHIF Medical Image Viewing Platform][viewers-project] is maintained as a
|
||||
@@ -39,27 +46,47 @@ you'll see the following:
|
||||
└── README.md
|
||||
```
|
||||
|
||||
Continue reading to see how these libraries work together to create the OHIF
|
||||
Viewer.
|
||||
The `platform` directory contains the business logic library, component library,
|
||||
and the application library that combines them to create a powerful medical
|
||||
imaging viewer.
|
||||
|
||||
### Business Logic
|
||||
The `extensions` directory contains many packages that can be registered with
|
||||
`@ohif/core`'s `ExtensionManager` to expand an application's supported features
|
||||
and functionality.
|
||||
|
||||

|
||||
|
||||
<center><i>architecture diagram</i></center>
|
||||
|
||||
This diagram is a conceptual illustration of how the Viewer is architected.
|
||||
|
||||
1. (optional) `extensions` can be registered with `@ohif/core`'s
|
||||
`ExtensionManager`
|
||||
2. `@ohif/core` provides bussiness logic and a way for `@ohif/viewer` to access
|
||||
registered extensions
|
||||
3. The `@ohif/viewer` composes and provides data to components from our
|
||||
component library (`@ohif/ui`)
|
||||
4. The `@ohif/viewer` can be built and served as a stand-alone PWA, or as an
|
||||
embeddable package ([`@ohif/viewer`][viewer-npm])
|
||||
|
||||
## Business Logic
|
||||
|
||||
The [`@ohif/core`][core-github] project offers pre-packaged solutions for
|
||||
features common to Web-based medical imaging viewers. For example:
|
||||
|
||||
- Hotkeys
|
||||
- [Hotkeys](#)
|
||||
- DICOM Web requests
|
||||
- Hanging Protocols
|
||||
- Managing a study's measurements
|
||||
- [Managing a study's measurements](../services/default/measurements.md)
|
||||
- Managing a study's DICOM metadata
|
||||
- A flexible pattern for extensions
|
||||
- [A flexible pattern for extensions](../extensions/index.md)
|
||||
- And many others
|
||||
|
||||
It does this while remaining decoupled from any particular view library or
|
||||
rendering logic. While we use it to power our React Viewer, it can be used with
|
||||
Vue, React, Vanilla JS, or any number of other frameworks.
|
||||
|
||||
### React Component Library
|
||||
## React Component Library
|
||||
|
||||
[`@ohif/ui`][ui-github] is a React Component library that contains the reusable
|
||||
components that power the OHIF Viewer. It allows us to build, compose, and test
|
||||
@@ -72,15 +99,11 @@ application.
|
||||
|
||||
[Check out our component library!](https://react.ohif.org/)
|
||||
|
||||
### Internationalization (i18n)
|
||||
## Internationalization (i18n)
|
||||
|
||||
...
|
||||
|
||||
### The Viewer
|
||||
|
||||
...
|
||||
|
||||
### Extensions & Configuration
|
||||
## Extensions & Configuration
|
||||
|
||||
While OHIF maintains several high value and commonly requested features in its
|
||||
own extensions, there are many instances where one may wish to further extend
|
||||
@@ -105,23 +128,6 @@ looking for ways to improve our extensibility ^\_^
|
||||
|
||||
[Click here to read more about extensions!](../extensions/index.md)
|
||||
|
||||
### Diagram
|
||||
|
||||
This diagram is a conceptual illustration of how the Viewer is architected.
|
||||
|
||||
1. (optional) `extensions` can be registered with `@ohif/core`'s extension
|
||||
manager
|
||||
2. `@ohif/core` provides bussiness logic and a way for `@ohif/viewer` to access
|
||||
registered extensions
|
||||
3. The `@ohif/viewer` composes and provides data to components from our
|
||||
component library (`@ohif/ui`)
|
||||
4. The `@ohif/viewer` can be built and served as a stand-alone PWA, or as an
|
||||
embeddable package ([`@ohif/viewer`][viewer-npm])
|
||||
|
||||

|
||||
|
||||
<center><i>architecture diagram</i></center>
|
||||
|
||||
## Common Questions
|
||||
|
||||
> When should I use the packaged source `@ohif/viewer` versus building a PWA
|
||||
@@ -141,10 +147,11 @@ you would need to recreate.
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[monorepo]: #
|
||||
[viewers-project]: https://github.com/OHIF/Viewers
|
||||
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
|
||||
[pwa]: https://developers.google.com/web/progressive-web-apps/
|
||||
[configuration]: ../essentials/configuration.md
|
||||
[configuration]: ../configuring/index.md
|
||||
[extensions]: ../extensions/index.md
|
||||
[core-github]: https://github.com/OHIF/viewers/platform/core
|
||||
[ui-github]: https://github.com/OHIF/Viewers/platform/ui
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
@@ -1,8 +1,9 @@
|
||||
# Data Source
|
||||
|
||||
After following the steps outlined in [Getting Started](./getting-started.md),
|
||||
you'll notice that the OHIF Viewer has data for several studies and their
|
||||
images. You didn't add this data, so where is it coming from?
|
||||
After following the steps outlined in
|
||||
[Getting Started](./../development/getting-started.md), you'll notice that the
|
||||
OHIF Viewer has data for several studies and their images. You didn't add this
|
||||
data, so where is it coming from?
|
||||
|
||||
By default, the viewer is configured to connect to a remote server hosted by the
|
||||
nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started,
|
||||
@@ -84,7 +85,7 @@ yarn run dev:orthanc
|
||||
#### Configuration: Learn More
|
||||
|
||||
> For more configuration fun, check out the
|
||||
> [Essentials Configuration](./configuration.md) guide.
|
||||
> [Essentials Configuration](./index.md) guide.
|
||||
|
||||
Let's take a look at what's going on under the hood here. `yarn run dev:orthanc`
|
||||
is running the `dev:orthanc` script in our project's `package.json`. That script
|
||||
@@ -130,7 +131,7 @@ window.config = {
|
||||
```
|
||||
|
||||
To learn more about how you can configure the OHIF Viewer, check out our
|
||||
[Configuration Guide](./configuration.md).
|
||||
[Configuration Guide](./index.md).
|
||||
|
||||
## Open Source DICOM Image Archives
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
> This step assumes you have an imaging archive. If you need assistance setting
|
||||
> one up, check out the [`Data Source` Guide](./data-source.md) or a deployment
|
||||
> recipe that contains an open source Image Archive
|
||||
> recipe that contains an open Image Archive
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Configuration Files](#configuration-files)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [How do I configure my project?](#how-do-i-configure-my-project)
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -16,8 +21,8 @@ and registered extension's features, are configured using this file.
|
||||
|
||||
**Embedded Use Note:**
|
||||
|
||||
Alternatively, when using the `commonjs` bundle for embedded use cases, these
|
||||
same values are what you'll pass to `installViewer` method:
|
||||
Alternatively, when using the `umd` bundle for embedded use cases, these same
|
||||
values are what you'll pass to `installViewer` method:
|
||||
|
||||
`OHIFStandaloneViewer.installViewer(window.config)`
|
||||
|
||||
@@ -36,10 +41,6 @@ alternative data source (or even specify different default hotkeys).
|
||||
| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` |
|
||||
| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` |
|
||||
|
||||
### Registering Extensions
|
||||
|
||||
> :warn: Instructions coming soon
|
||||
|
||||
## How do I configure my project?
|
||||
|
||||
The simplest way is to update the existing default config:
|
||||
@@ -65,6 +66,39 @@ window.config = {
|
||||
};
|
||||
```
|
||||
|
||||
The configuration can also be written as a JS Function in case you need to inject dependencies like external services:
|
||||
|
||||
```js
|
||||
window.config = ({ servicesManager } = {}) => {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
return {
|
||||
cornerstoneExtensionConfig: {
|
||||
tools: {
|
||||
ArrowAnnotate: {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) => UIDialogService.create({...
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
routerBasename: '/',
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
You can also create a new config file and specify its path relative to the build
|
||||
output's root by setting the `APP_CONFIG` environment variable. You can set the
|
||||
value of this environment variable a few different ways:
|
||||
@@ -10,9 +10,9 @@ your Viewer will display).
|
||||
## Overview
|
||||
|
||||
Our goal is to make deployment as simple and painless as possible; however,
|
||||
there is an inherent amount of complexity in customizing, optimizing, and
|
||||
deploying web applications. If you find yourself a little lost, please don't
|
||||
hesitate to [reach out for help](/help.md)
|
||||
there is an inherent amount of complexity in configuring and deploying web
|
||||
applications. If you find yourself a little lost, please don't hesitate to
|
||||
[reach out for help](/help.md)
|
||||
|
||||
## Deployment Scenarios
|
||||
|
||||
@@ -96,12 +96,12 @@ support it yet, but it is gaining wider adoption.
|
||||
If you have an existing archive and intend to host the OHIF Viewer at the same
|
||||
domain name as your archive, then connecting the two is as simple as following
|
||||
the steps layed out in our
|
||||
[Configuration Essentials Guide](./../essentials/configuration.md).
|
||||
[Configuration Essentials Guide](./../configuring/index.md).
|
||||
|
||||
#### What if I don't have an imaging archive?
|
||||
|
||||
We provide some guidance on configuring a local image archive in our
|
||||
[Data Source Essentials](./../essentials/data-source.md) guide. Hosting an
|
||||
[Data Source Essentials](./../configuring/data-source.md) guide. Hosting an
|
||||
archive remotely is a little trickier. You can check out some of our
|
||||
[advanced recipes](#recipes) for modeled setups that may work for you.
|
||||
|
||||
@@ -142,7 +142,7 @@ The OHIF Viewer can be configured to work with authorization servers that
|
||||
support one or more of the OpenID-Connect authorization flows. The Viewer finds
|
||||
it's OpenID-Connect settings on the `oidc` configuration key. You can set these
|
||||
values following the instructions laid out in the
|
||||
[Configuration Essentials Guide](./../essentials/configuration.md).
|
||||
[Configuration Essentials Guide](./../configuring/index.md).
|
||||
|
||||
_Example OpenID-Connect Settings:_
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Build for Production
|
||||
|
||||
> If you've already followed the
|
||||
> ["Getting Started" Guide](/essentials/getting-started.md), you can skip ahead
|
||||
> ["Getting Started" Guide](/development/getting-started.md), you can skip ahead
|
||||
> to [Configuration](#configuration)
|
||||
|
||||
## Overview
|
||||
@@ -73,7 +73,7 @@ and registered extension's features, are configured using this file.
|
||||
|
||||
The easiest way to apply your own configuration is to modify the `default.js`
|
||||
file. For more advanced cofiguration options, check out our
|
||||
[configuration essentials guide](/essentials/configuration.md).
|
||||
[configuration essentials guide](/configuring/index.md).
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@ include tags. Here's how it works:
|
||||
</ul>
|
||||
|
||||
<ol start="2">
|
||||
<li>Create a JS Object to hold the OHIF Viewer's configuration. Here are some
|
||||
<li>Create a JS Object or Function to hold the OHIF Viewer's configuration. Here are some
|
||||
example values that would allow the viewer to hit our public PACS:</li>
|
||||
</ol>
|
||||
|
||||
```js
|
||||
// Set before importing `ohif-viewer`
|
||||
// Set before importing `ohif-viewer` (JS Object)
|
||||
window.config = {
|
||||
// default: '/'
|
||||
routerBasename: '/',
|
||||
@@ -49,6 +49,9 @@ window.config = {
|
||||
};
|
||||
```
|
||||
|
||||
To learn more about how you can configure the OHIF Viewer, check out our
|
||||
[Configuration Guide](./index.md).
|
||||
|
||||
<ol start="3"><li>
|
||||
Render the viewer in the web page's target <code>div</code>
|
||||
</li></ol>
|
||||
|
||||
File renamed without changes.
@@ -7,8 +7,8 @@ like to discuss the changes you intend to make to clarify where or how they
|
||||
should be implemented, please don't hesitate to create a new issue. At a
|
||||
minimum, you may want to read the following documentation:
|
||||
|
||||
- [Essentials: Getting Started](/essentials/getting-started.md)
|
||||
- [Advanced: Architecture](/advanced/architecture.md)
|
||||
- [Getting Started](/development/getting-started.md)
|
||||
- [Architecture](/architecture/index.md)
|
||||
|
||||
Pull requests that are:
|
||||
|
||||
@@ -85,20 +85,12 @@ yarn run build
|
||||
yarn run build:package
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Take a look at the sidebar. Depending on how you intend to use The OHIF Viewer,
|
||||
different topics may be more pertinent than others. At a minimum, reading "The
|
||||
Essentials" and the deployment recipes should be a good starting point.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If you receive a _"No Studies Found"_ message and do not see your studies, try
|
||||
changing the Study Date filters to a wider range.
|
||||
- If you see a 'Loading' message which never resolves, check your browser
|
||||
JavaScript console inside the Developer Tools to identify any errors.
|
||||
- If you see any errors in your server console, check the
|
||||
[Troubleshooting](./troubleshooting.md) page for more in depth advice.
|
||||
|
||||
<!--
|
||||
Links
|
||||
File renamed without changes.
@@ -1,20 +0,0 @@
|
||||
# Troubleshooting
|
||||
|
||||
Common GitHub issues that are not easily remedied with cleaner code or
|
||||
documentation will be recorded here. Please feel free to make PRs to update this
|
||||
page.
|
||||
|
||||
## Common Problems
|
||||
|
||||
| Problem | Most Common Reasons |
|
||||
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ** Can't retrieve Study List over DICOMWeb** | 1. QIDO root URL is incorrect<br> 2. DICOM Web is not enabled on PACS |
|
||||
| ** Can't retrieve images** | 1. WADO Root URL is incorrect<br> 2. DICOM Web is not enabled on PACS<br> 3. HTTP Basic Authentication username and password are incorrect or not provided. |
|
||||
|
||||
## Debugging Steps
|
||||
|
||||
### Can't retrieve Study List over DICOMWeb
|
||||
|
||||
1. Check that you can query your PACS using an alternative DICOM Web client
|
||||
(e.g. cURL, or a Web Browser). If you cannot, then your PACS is configured
|
||||
incorrectly. Refer to the documentation of the image archive.
|
||||
+110
-24
@@ -8,12 +8,15 @@
|
||||
- [Modules](#modules)
|
||||
- [Contexts](#contexts)
|
||||
- [Consuming Extensions](#consuming-extensions)
|
||||
- [Extension Manager](#extensionmanager)
|
||||
- [Maintained Extensions](#maintained-extensions)
|
||||
|
||||
## Overview
|
||||
|
||||
We use extensions to help us isolate and package groups of related features.
|
||||
Extensions provide functionality, ui components, and new behaviors.
|
||||
Extensions provide functionality, ui components, and new behaviors. Ideally,
|
||||
they're built in a way that allows them to extend entirely different
|
||||
implementations of the `@ohif/viewer` project.
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/extensions-diagram.png">
|
||||
@@ -46,9 +49,8 @@ Practical examples of extensions include:
|
||||
|
||||
### Extension Skeleton
|
||||
|
||||
An extension is a plain JavaScript object has an `id` property, and one or more
|
||||
"getModuleFunctions" and/or lifecycle hooks. You can read more about
|
||||
[lifecycle hooks](#lifecycle-hooks) and [modules](#modules) further down.
|
||||
An extension is a plain JavaScript object that has an `id` property, and one or
|
||||
more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks).
|
||||
|
||||
```js
|
||||
// prettier-ignore
|
||||
@@ -71,11 +73,19 @@ export default {
|
||||
|
||||
### Registering an Extension
|
||||
|
||||
There are two different ways to register and configure extensions. You can
|
||||
leverage one or both strategies. Which one(s) you choose depend on your
|
||||
There are two different ways to register and configure extensions: At
|
||||
[runtime](#runtime-extensions) and at [build time](#bundled-extensions).
|
||||
|
||||
You can leverage one or both strategies. Which one(s) you choose depend on your
|
||||
application's requirements. Each [module](#modules) defined by the extension
|
||||
becomes available to the core application via the `ExtensionManager`.
|
||||
|
||||
#### Registering at Runtime
|
||||
|
||||
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
|
||||
that file includes an `Extensions` key that supports an array of extensions to
|
||||
register.
|
||||
|
||||
```js
|
||||
// prettier-ignore
|
||||
const config = {
|
||||
@@ -89,20 +99,13 @@ const config = {
|
||||
}
|
||||
```
|
||||
|
||||
#### Runtime Extensions
|
||||
|
||||
The `@ohif/viewer` uses a [configuration file](#) at startup. The schema for
|
||||
that file includes an `Extensions` key that supports an array of extensions to
|
||||
register.
|
||||
|
||||
#### Bundled Extensions
|
||||
#### Registering at Build Time
|
||||
|
||||
The `@ohif/viewer` works best when built as a "Progressive Web Application"
|
||||
(PWA). If you know the extensions your application will need, you can specify
|
||||
them at "build time" to leverage some advantaged afforded to us by modern
|
||||
tooling:
|
||||
them at "build time" to leverage advantages afforded to us by modern tooling:
|
||||
|
||||
- Code Splitting
|
||||
- Code Splitting (dynamic imports)
|
||||
- Tree Shaking
|
||||
- Dependency deduplication
|
||||
|
||||
@@ -114,23 +117,106 @@ You can update the list of bundled extensions by:
|
||||
|
||||
### Lifecycle Hooks
|
||||
|
||||
...
|
||||
Currently, there is only a single lifecycle hook for extensions:
|
||||
[`preRegistration`](./lifecycle/pre-registration.md)
|
||||
|
||||
If an extension defines the [`preRegistration`](./lifecycle/pre-registration.md)
|
||||
lifecycle hook, it is called before any modules are registered in the
|
||||
`ExtensionManager`. It's most commonly used to wire up extensions to
|
||||
[services](./../services/index.md) and [commands](./modules/commands.md), and to
|
||||
bootstrap 3rd party libraries.
|
||||
|
||||
### Modules
|
||||
|
||||
There are a few different module types. Each module type allows us to extend the
|
||||
viewer in a different way, and provides a consistent API for us to do so. You
|
||||
can find a full list of the different types of modules
|
||||
[`in ohif-core`][module-types]. Information on each type of module, it's API,
|
||||
and how we determine when/where it should be used is included below.
|
||||
Modules are the meat of extensions. They provide "definitions", components, and
|
||||
filtering/mapping logic that are then made available by various managers and
|
||||
services.
|
||||
|
||||
Each module type has a special purpose, and is consumed by our viewer
|
||||
differently.
|
||||
|
||||
| Type | Description | Examples |
|
||||
| ------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| [Commands](./modules/commands.md) | Adds named commands, scoped to a context, to the CommandsManager | `setToolActive()`, `nextSeries()` |
|
||||
| [Panel](./modules/panel.md) | Adds left or right hand side panels | `<ThumbnailList />`, `<MeasurementsTable />` |
|
||||
| [SOPClassHandler](./modules/sop-class-handler.md) | Determines how retrieved study data is split into "DisplaySets" | `getDisplaySetFromSeries()` |
|
||||
| [Toolbar](./modules/toolbar.md) | Adds buttons or custom components to the toolbar | Toolbar button, nested buttons, custom |
|
||||
| [Viewport](./modules/viewport.md) | Adds a component responsible for rendering a "DisplaySet" | `<CornerstoneViewport />`, `<DicomPdfViewport />` |
|
||||
|
||||
<figure style="text-align: center; font-style: italic;">Tbl. Module types with abridged descriptions and examples. Each module links to a dedicated documentation page.</figure>
|
||||
|
||||
### Contexts
|
||||
|
||||
...
|
||||
The `@ohif/viewer` tracks "active contexts" that extensions can use to scope
|
||||
their functionality. Some example contexts being:
|
||||
|
||||
- Route: `ROUTE:VIEWER`, `ROUTE:STUDY_LIST`
|
||||
- Active Viewport: `ACTIVE_VIEWPORT:CORNERSTONE`, `ACTIVE_VIEWPORT:VTK`
|
||||
|
||||
An extension module can use these to say "Only show this Toolbar Button if the
|
||||
active viewport is a Cornerstone viewport." This helps us use the appropriate UI
|
||||
and behaviors depending on the current contexts.
|
||||
|
||||
For example, if we have hotkey that "rotates the active viewport", each Viewport
|
||||
module that supports this behavior can add a command with the same name, scoped
|
||||
to the appropriate context. When the `command` is fired, the "active contexts"
|
||||
are used to determine the appropriate implementation of the rotate behavior.
|
||||
|
||||
## Consuming Extensions
|
||||
|
||||
...
|
||||
We consume extensions, via the `ExtensionManager`, in our `@ohif/viewer`
|
||||
project.
|
||||
|
||||
```js
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
});
|
||||
|
||||
// prettier-ignore
|
||||
extensionManager.registerExtensions([ /** **/ ]);
|
||||
```
|
||||
|
||||
The `@ohif/viewer` project handles data fetching, basic routing, wires up UI
|
||||
services, and is the home to the more bespoke application logic that doesn't
|
||||
make as much sense to make reusable.
|
||||
|
||||
Long-term, replacing the `@ohif/viewer` application and consuming extensions
|
||||
(and the `ExtensionManager`) in your own project is the ideal path for
|
||||
applications requiring a high degree of customization that can't be achieved
|
||||
with current theming, configuration, extension, and services support.
|
||||
|
||||
If you're not sure how to achieve your goals with the extensibility available
|
||||
today, create a GitHub issue!
|
||||
|
||||
### `ExtensionManager`
|
||||
|
||||
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` along with the application's
|
||||
configuration through the appConfig key (optional).
|
||||
|
||||
```js
|
||||
const commandsManager = new CommandsManager();
|
||||
const servicesManager = new ServicesManager();
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
appConfig
|
||||
});
|
||||
```
|
||||
|
||||
The `ExtensionManager` only has a few public members:
|
||||
|
||||
- `registerExtension` - Registers a single extension
|
||||
- `registerExtensions` - Registers an array of extensions
|
||||
- `modules` - An object containing registered extensions by `MODULE_TYPE`
|
||||
|
||||
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.
|
||||
|
||||
## Maintained Extensions
|
||||
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
# Lifecylce Hook: preRegistration
|
||||
|
||||
If an extension defines the `preRegistration` lifecycle hook, it is called
|
||||
before any modules are registered to the `ExtensionManager`.
|
||||
before any modules are registered in the `ExtensionManager`. This hook can be
|
||||
used to:
|
||||
|
||||
- initialize 3rd party libraries
|
||||
- register event listeners
|
||||
- add or call services
|
||||
- add or call commands
|
||||
|
||||
The `preRegistration` hook receives an object containing the
|
||||
`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any
|
||||
`configuration` that was provided with the extension at time of registration.
|
||||
|
||||
_Example `preRegistration` hook implementation_
|
||||
|
||||
```js
|
||||
export default {
|
||||
id: 'MyExampleExtension',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {object} params.configuration
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
* @returns void
|
||||
*/
|
||||
preRegistration({ servicesManager, commandsManager, configuration }) {
|
||||
console.log('Wiring up important stuff.');
|
||||
|
||||
|
||||
@@ -1,34 +1,158 @@
|
||||
# Module: Commands
|
||||
|
||||
The Commands Module allows us to register one or more commands scoped to
|
||||
specific contexts. Commands can be run by [hotkeys][#], [toolbar buttons][#],
|
||||
and any registered custom react component (like a [viewport][#] or [panel][#]).
|
||||
- [Overview](#overview)
|
||||
- [Command Definitions](#command-definitions)
|
||||
- [Commands Manager](#commands-manager)
|
||||
- [Instantiating](#instatiating)
|
||||
- [Public API](#public-api)
|
||||
- [Contexts](#contexts)
|
||||
|
||||
## Overview
|
||||
|
||||
An extension can register a Commands Module by defining a `getCommandsModule`
|
||||
method. The Commands Module allows us to register one or more commands scoped to
|
||||
specific [contexts](./../index.md#contexts). Commands have several unique
|
||||
characteristics that make them tremendously powerful:
|
||||
|
||||
- Multiple implementations for the same command can be defined
|
||||
- Only the correct command's implementation will be run, dependent on the
|
||||
application's "context"
|
||||
- Commands can be called from extensions, modules, and the consuming application
|
||||
|
||||
Here is a simple example commands module:
|
||||
|
||||
```js
|
||||
{
|
||||
getCommandsModule() {
|
||||
return {
|
||||
actions: {
|
||||
speak: ({ viewports, words }) => {
|
||||
console.log(viewports, words);
|
||||
},
|
||||
},
|
||||
definitions: {
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: 90 }
|
||||
},
|
||||
rotateViewportCCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
options: { rotation: -90 },
|
||||
context: 'ACTIVE_VIEWER::CORNERSTONE'
|
||||
},
|
||||
},
|
||||
defaultContext: 'VIEWER'
|
||||
}
|
||||
}
|
||||
export default {
|
||||
id: 'example-commands-module',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getCommandsModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
definitions: {
|
||||
sayHello: {
|
||||
commandFn: ({ words }) => {
|
||||
console.log(words);
|
||||
},
|
||||
options: { words: 'Hello!' },
|
||||
},
|
||||
},
|
||||
defaultContext: 'VIEWER',
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Each definition returned by the Commands Module is registered to the
|
||||
`ExtensionManager`'s `CommandsManager`.
|
||||
|
||||
## Command Definitions
|
||||
|
||||
The command definition consists of a named command (`myCommandName` below) and a
|
||||
`commandFn`. The command name is used to call the command, and the `commandFn`
|
||||
is the "command" that is actioned.
|
||||
|
||||
```js
|
||||
myCommandName: {
|
||||
commandFn: ({ viewports, other, options }) => { },
|
||||
storeContexts: ['viewports'],
|
||||
options: { words: 'Just kidding! Goodbye!' },
|
||||
context: 'ACTIVE_VIEWPORT::CORNERSTONE',
|
||||
}
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. |
|
||||
| `storeContexts` | string[] | (optional) Expected state objects to be passed in as props. Located using `getAppState` fn defined at `CommandsManager`'s instatiation. |
|
||||
| `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` |
|
||||
| `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. |
|
||||
|
||||
## Command Behavior
|
||||
|
||||
**I have many similar commands. How can I share their `commandFn` and make it
|
||||
reusable?**
|
||||
|
||||
This is where `storeContexts` and `options` come in. We use these in our
|
||||
`setToolActive` command. `storeContexts` helps us identify our `activeViewport`,
|
||||
and `options` allow us to pass in the name of a tool we would like to set as
|
||||
active.
|
||||
|
||||
**If there are multiple valid commands for the application's active contexts**
|
||||
|
||||
- What happens: all commands are run
|
||||
- When to use: A `clearData` command that cleans up state for multiple
|
||||
extensions
|
||||
|
||||
**If no commands are valid for the application's active contexts**
|
||||
|
||||
- What happens: a warning is printed to the console
|
||||
- When to use: a `hotkey` (like "invert") that doesn't make sense for the
|
||||
current viewport (PDF or HTML)
|
||||
|
||||
## `CommandsManager`
|
||||
|
||||
The `CommandsManager` is a class defined in the `@ohif/core` project. A single
|
||||
instance of it should be defined in the consuming application, and it should be
|
||||
used when constructing the `ExtensionManager`.
|
||||
|
||||
### Instantiating
|
||||
|
||||
When we instantiate the `CommandsManager`, we need to pass it two methods:
|
||||
|
||||
- `getAppState` - Should return the application's state when called
|
||||
- `getActiveContexts` - Should return the application's active contexts when
|
||||
called
|
||||
|
||||
These methods are used internally to help determine which commands are currently
|
||||
valid, and how to provide them with any state they may need at the time they are
|
||||
called.
|
||||
|
||||
```js
|
||||
const commandsManager = new CommandsManager({
|
||||
getAppState,
|
||||
getActiveContexts,
|
||||
});
|
||||
```
|
||||
|
||||
### Public API
|
||||
|
||||
If you would like to run a command in the consuming app or an extension, you can
|
||||
use one of the following methods:
|
||||
|
||||
```js
|
||||
// Returns all commands for a given context
|
||||
commandsManager.getContext('string');
|
||||
|
||||
// Attempts to run a command
|
||||
commandsManager.runCommand('speak', { command: 'hello' });
|
||||
|
||||
// Run command, but override the active contexts
|
||||
commandsManager.runCommand('speak', { command: 'hello' }, ['VIEWER']);
|
||||
```
|
||||
|
||||
The `ExtensionManager` handles registering commands and creating contexts, so
|
||||
most consumer's won't need these methods. If you find yourself using these, ask
|
||||
yourself "why can't I register these commands via an extension?"
|
||||
|
||||
```js
|
||||
// Used by the `ExtensionManager` to register new commands
|
||||
commandsManager.registerCommand('context', 'name', commandDefinition);
|
||||
|
||||
// Creates a new context; clears the context if it already exists
|
||||
commandsManager.createContext('string');
|
||||
```
|
||||
|
||||
### Contexts
|
||||
|
||||
It is up to the consuming application to define what contexts are possible, and
|
||||
which ones are currently active. As extensions depend heavily on these, we will
|
||||
likely publish guidance around creating contexts, and ways to override extension
|
||||
defined contexts in the near future. If you would like to discuss potential
|
||||
changes to how contexts work, please don't hesistate to createa new GitHub
|
||||
issue.
|
||||
|
||||
[Some additional information on Contexts can be found here.](./../index.md#contexts)
|
||||
@@ -1,3 +1,57 @@
|
||||
# Module: Panel
|
||||
|
||||
...
|
||||
An extension can register a Panel Module by defining a `getPanelModule` method.
|
||||
The panel module provides the ability to define `menuOptions` and `components`
|
||||
that can be used by the consuming application. `components` are React Components
|
||||
that can be displayed in the consuming application's "Panel" Component.
|
||||
|
||||

|
||||
|
||||
<center><i>A panel extension example</i></center>
|
||||
|
||||
The `menuOptions`'s `target` key points to a registered `components`'s `id`. A
|
||||
`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can
|
||||
optional provide it's own `context` value.
|
||||
|
||||
The `getPanelModule` receives an object containing the `ExtensionManager`'s
|
||||
associated `ServicesManager` and `CommandsManager`.
|
||||
|
||||
```js
|
||||
import MyComponent from './MyComponent.js';
|
||||
|
||||
export default {
|
||||
id: 'example-panel-module',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getPanelModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
// A suggested icon
|
||||
// Available icons determined by consuming app
|
||||
icon: 'list',
|
||||
// A suggested label
|
||||
label: 'Magic',
|
||||
// 'right' or 'left'
|
||||
from: 'right',
|
||||
// The target component to toggle open/close
|
||||
target: 'target-component-id',
|
||||
// Overrides `defaultContext`, if specified
|
||||
context: ['ACTIVE_VIEWPORT:MAGIC'],
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'target-component-id',
|
||||
component: MyComponent,
|
||||
},
|
||||
],
|
||||
defaultContext: ['ROUTE:VIEWER'],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
@@ -1,3 +1,90 @@
|
||||
# Module: SOP Class Handler
|
||||
|
||||
...
|
||||
An extension can register a SOP Class Handler Module by defining a
|
||||
`getSopClassHandlerModule` method. The SOP Class Handler is a bit different from
|
||||
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
|
||||
`getDisplaySetFromSeries` method should be applied to.
|
||||
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
|
||||
display set
|
||||
|
||||
A `displaySet` has the following shape:
|
||||
|
||||
```js
|
||||
return {
|
||||
plugin: 'html',
|
||||
modality: 'SR',
|
||||
displaySetInstanceUid: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
```
|
||||
|
||||
Where the `plugin` key is used to influence the default `ViewportComponent` for
|
||||
rendering the `displaySet`. Additional properties are passed to the
|
||||
`ViewportComponent` and used by the default `StudyBrowser` to render
|
||||
"thumbnails" for each `displaySet`
|
||||
|
||||
## Example SOP Class Handler Module
|
||||
|
||||
```js
|
||||
const SOP_CLASS_UIDS = {
|
||||
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',
|
||||
};
|
||||
|
||||
export default {
|
||||
id: 'example-sop-class-handler-module',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
sopClassUids: Object.values(SOP_CLASS_UIDS),
|
||||
|
||||
/**
|
||||
* @param {object} series -
|
||||
* @param {object} study -
|
||||
* @param {object} dicomWebClient -
|
||||
* @param {object} authorizationHeaders -
|
||||
*/
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: 0,
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
authorizationHeaders,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## `@ohif/viewer` usage
|
||||
|
||||
We use the `sopClassHandlerModule`s in three different places:
|
||||
|
||||
- `ViewerLocalFileData.js`
|
||||
- `ViewerRetrieveStudyData.js`
|
||||
- `StandaloneRouting.js`
|
||||
|
||||
Each time, it is used to map study and series data to `displaySets`. It does
|
||||
this by working alongside the `StudyMetadataManager` in `@ohif/core`. That
|
||||
manager has the method `createDisplaySets` that takes an array of
|
||||
`sopClassHandlerModules`.
|
||||
@@ -1,48 +1,130 @@
|
||||
# Module: Toolbar
|
||||
|
||||
An extension can register a Toolbar Module by providing a `getToolbarModule()`
|
||||
method that returns a React Component. The component does not receive any props.
|
||||
If you want to modify or react to state, you will need to connect to the redux
|
||||
store. The given toolbar must determine its set of elements and the context of
|
||||
them. The set of elements will be listed on toolbar `definitions`.
|
||||
An extension can register a Toolbar Module by defining a `getToolbarModule`
|
||||
method. This module is commonly used to define:
|
||||
|
||||

|
||||
- [Toolbar buttons](#button-definitions)
|
||||
- [Nested toolbar menus](#nested-toolbar-menus)
|
||||
- [Custom components](#custom-components)
|
||||
|
||||
<center><i>A toolbar extension example</i></center>
|
||||

|
||||
|
||||
Toolbar components are rendered in the `ToolbarRow` component.
|
||||
<center><i>Example toolbar button using the Dialog Service to show CINE controls.</i></center>
|
||||
|
||||
For a complete example implementation,
|
||||
[check out the OHIFCornerstoneViewport's Toolbar Module](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/toolbarModule.js).
|
||||
## Example Toolbar Module
|
||||
|
||||
## Toolbar Custom Component
|
||||
|
||||
Toolbar elements can define its own custom react component to be consumed when
|
||||
rendering it. So far, it accepts `Functional` and `Class` Components. For that,
|
||||
you just need to expose your `CustomToolbarComponent` as the value of key
|
||||
`CustomComponent`. In case the property `CustomComponent` is not present, a
|
||||
default toolbar component will be used to render it. See bellow
|
||||
The Toolbar Module should return an array of `definitions` and a
|
||||
`defaultContext`. There are currently a few different variations of definitions,
|
||||
each one is detailed further down.
|
||||
|
||||
```js
|
||||
definitions: [
|
||||
...
|
||||
export default {
|
||||
id: 'example-toolbar-module',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getToolbarModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
definitions: [
|
||||
/* Array of definitions */
|
||||
],
|
||||
defaultContext: ['ROUTE:VIEWER'],
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Button Definitions
|
||||
|
||||
The simplest definition has the following properties:
|
||||
|
||||
```js
|
||||
{
|
||||
id: 'StackScroll',
|
||||
label: 'Stack Scroll',
|
||||
icon: 'bars',
|
||||
type: 'setToolActive',
|
||||
commandName: 'setToolActive',
|
||||
commandOptions: { toolName: 'StackScroll' },
|
||||
},
|
||||
```
|
||||
|
||||
| property | description | values |
|
||||
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `id` | Unique string identifier for the definition | \* |
|
||||
| `label` | User/display friendly to show in UI | \* |
|
||||
| `icon` | A string name for an icon supported by the consuming application. | \* |
|
||||
| `type` | Used to determine the button's component and behavior | `"setToolActive"`, `"command"` |
|
||||
| `commandName` | (optional) The command to run when the button is used. | Any command registed by a `CommandModule` |
|
||||
| `commandOptions` | (optional) Options to pass the target `commandName` | \* |
|
||||
| `context` | (optional) Overrides module's `defaultContext` | Array of string context names |
|
||||
|
||||
Where a button with a `type` of `setToolActive` has an "active" styling applied
|
||||
when clicked; removing the active styling from all other buttons.
|
||||
|
||||
## Nested Toolbar Menus
|
||||
|
||||
You can indicate that buttons should be grouped and nested in a submenu by
|
||||
including `buttons` property in a definition:
|
||||
|
||||
```js
|
||||
{
|
||||
id: 'More',
|
||||
label: 'More',
|
||||
icon: 'ellipse-circle',
|
||||
buttons: [
|
||||
{
|
||||
id: 'Custom',
|
||||
label: 'Custom',
|
||||
icon: 'custom-icon',
|
||||
CustomComponent: CustomToolbarComponent,
|
||||
}
|
||||
...
|
||||
]
|
||||
id: 'cstInvert',
|
||||
label: 'Invert',
|
||||
icon: 'circle',
|
||||
type: 'command',
|
||||
commandName: 'invertViewport',
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||

|
||||
|
||||
<center><i>Example toolbar button demonstrating nested buttons.</i></center>
|
||||
|
||||
## Custom Components
|
||||
|
||||
The Toolbar Modules supports rendering custom components in place of the
|
||||
application's default. In place of the `type`, `commandName`, and
|
||||
`commandOptions` properties, we instead specify a `CustomComponent`.
|
||||
|
||||
```js
|
||||
{
|
||||
id: 'Custom',
|
||||
label: 'Custom',
|
||||
icon: 'custom-icon',
|
||||
CustomComponent: CustomToolbarComponent,
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`CustomComponent` components will receive the following props:
|
||||
The `CustomComponent` components will receive the following props:
|
||||
|
||||
- parentContext: parent context. (In most of the cases it will be a ToolbarRow
|
||||
instance)
|
||||
- toolbarClickCallback: callback method when clicking on toolbar
|
||||
- button: its own definition object
|
||||
- key: react key prop
|
||||
- activeButtons: list of active elements
|
||||
- isActive: if current
|
||||
```html
|
||||
<CustomComponent
|
||||
parentContext="{parentContext}"
|
||||
toolbarClickCallback="{_handleToolbarButtonClick.bind(this)}"
|
||||
button="{button}"
|
||||
key="{button.id}"
|
||||
activeButtons="{activeButtonsIds}"
|
||||
isActive="{isActive}"
|
||||
/>
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
| ---------------------- | -------- | ------------------------------- |
|
||||
| `activeButtons` | string[] | list of active buttons |
|
||||
| `button` | object | its own definition object |
|
||||
| `key` | string | React key prop |
|
||||
| `isActive` | boolean | If current button is active |
|
||||
| `parentContext` | ? | The parent component's context? |
|
||||
| `toolbarClickCallback` | func | Callback method for clicks |
|
||||
@@ -1,19 +1,39 @@
|
||||
# Module: Viewport
|
||||
|
||||
An extension can register a Viewport Module by providing a `getViewportModule()`
|
||||
method that returns a React Component. The React component will receive the
|
||||
following props:
|
||||
An extension can register a Viewport Module by defining a `getViewportModule`
|
||||
method that returns a React component. Currently, we use viewport components to
|
||||
add support for:
|
||||
|
||||
```js
|
||||
children: PropTypes.arrayOf(PropTypes.element)
|
||||
studies: PropTypes.object,
|
||||
displaySet: PropTypes.object,
|
||||
viewportData: PropTypes.object, // { studies, displaySet }
|
||||
viewportIndex: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
customProps: PropTypes.object
|
||||
- 2D Medical Image Viewing (cornerstone ext.)
|
||||
- Structured Reports as HTML (dicom html ext.)
|
||||
- Encapsulated PDFs as PDFs (dicom pdf ext.)
|
||||
- Whole Slide Microscopy Viewing (whole slide ext.)
|
||||
- etc.
|
||||
|
||||
The general pattern is, the [`sopClassHandlerModule`](#) helps us determine
|
||||
which Viewport Component a set of `sopClassUIDs` should default to. The Viewport
|
||||
Component receives props containing a display set it should know how to render.
|
||||
|
||||
## Viewport Component Props
|
||||
|
||||
Each `ViewportComponent` will receive the following props:
|
||||
|
||||
```html
|
||||
<viewportComponent
|
||||
viewportData="{viewportData}"
|
||||
viewportIndex="{viewportIndex}"
|
||||
children="{[children]}"
|
||||
/>
|
||||
```
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | --------------- | --------------------------------- |
|
||||
| `children` | React.element[] | |
|
||||
| `viewportData` | object | `viewportSpecificData` (probably) |
|
||||
| `viewportIndex` | number | |
|
||||
|
||||
### `@ohif/viewer`
|
||||
|
||||
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
|
||||
component is used depends on:
|
||||
|
||||
@@ -23,7 +43,4 @@ component is used depends on:
|
||||
|
||||

|
||||
|
||||
<center><i>An example of three Viewports</i></center>
|
||||
|
||||
For a complete example implementation,
|
||||
[check out the OHIFCornerstoneViewport](https://github.com/OHIF/Viewers/blob/master/extensions/cornerstone/src/OHIFCornerstoneViewport.js).
|
||||
<center><i>An example of three cornerstone Viewports</i></center>
|
||||
File renamed without changes.
@@ -19,10 +19,10 @@ Follow the steps outlined in the [Bug Report Template][bug-report-template].
|
||||
At the moment we are in the process of defining our roadmap and will do our best
|
||||
to communicate this to the community. If your requested feature is on the
|
||||
roadmap, then it will most likely be built at some point. If it is not, you are
|
||||
welcome to build it yourself and [contribute it](contributing/index.md). If you
|
||||
have resources and would like to fund the development of a feature, please
|
||||
[contact us](http://www.ohif.org) or work with community members that offer
|
||||
[consulting services][commercial-support].
|
||||
welcome to build it yourself and [contribute it](development/contributing.md).
|
||||
If you have resources and would like to fund the development of a feature,
|
||||
please [contact us](http://www.ohif.org) or work with community members that
|
||||
offer [consulting services][commercial-support].
|
||||
|
||||
### Who should I contact about Academic Collaborations?
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Installation
|
||||
# PWA vs Packaged
|
||||
|
||||
It's important to know that the OHIF Viewer project provides two different build
|
||||
processes:
|
||||
@@ -36,7 +36,7 @@ many data sources. The OHIF Viewer's scope **DOES** include configuration and
|
||||
support for services that are protected with OpenID-Connect.
|
||||
|
||||
In an effort to aide our users and contributors, we attempt to provide several
|
||||
[deployment and hosting recipes](./../deployment/index.md) as potential starting
|
||||
[deployment and hosting recipes](./deployment/index.md) as potential starting
|
||||
points. These are not meant to be rock solid, production ready, solutions; like
|
||||
most recipes, they should be augmented to best fit you and your organization's
|
||||
taste, preferences, etc.
|
||||
@@ -105,7 +105,7 @@ quality and test coverage must not changed by a significant margin. For some
|
||||
repositories, visual screenshot-based tests are also included, and video
|
||||
recordings of end-to-end tests are stored for later review.
|
||||
|
||||
[You can read more about our continous integration efforts here](/continous-integration.md)
|
||||
[You can read more about our continous integration efforts here](/development/continous-integration.md)
|
||||
|
||||
## Releases
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Quick Start
|
||||
|
||||
This page details how to get an instance of the OHIF Viewer up and running as
|
||||
fast as possible. It shows how to grab a pre-built version of the application,
|
||||
point it at your data source (PACS), and plop it on a web server.
|
||||
|
||||
## Options
|
||||
|
||||
### 1. Pre-built PWA
|
||||
|
||||
...
|
||||
|
||||
### 2. Script-Tag
|
||||
|
||||
...
|
||||
|
||||
### 3. Docker
|
||||
|
||||
...
|
||||
|
||||
## Security Concerns
|
||||
|
||||
- Secure your data
|
||||
|
||||
## Common Issues
|
||||
|
||||
- Missing server rewrite rules
|
||||
- CORS issues when requesting data from PACS
|
||||
@@ -1,6 +1,6 @@
|
||||
# Measurements Package (ohif-measurements)
|
||||
# Service: Measurements
|
||||
|
||||
## Package design
|
||||
...
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# 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'],
|
||||
},
|
||||
],
|
||||
// Config to pass to the bundled cornerstone extension
|
||||
cornerstoneExtensionConfig: {},
|
||||
};
|
||||
```
|
||||
|
||||
<!--
|
||||
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
|
||||
File renamed without changes.
@@ -0,0 +1,9 @@
|
||||
# Viewer
|
||||
|
||||
The OHIF Viewing Platform strives to be highly configurable and extensible. This
|
||||
makes it easier for our community members to keep their "secret sauce" private,
|
||||
and incentivises contributions back to the platform. The `@ohif/viewer` project
|
||||
of the platform is the lynchpin that combines everything to create our
|
||||
application.
|
||||
|
||||
- When configuration and themeing aren't enough
|
||||
@@ -1,4 +1,4 @@
|
||||
# Translating
|
||||
# Viewer: Internationalization
|
||||
|
||||
OHIF supports internationalization using [i18next](https://www.i18next.com/)
|
||||
through the npm package [@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n),
|
||||
@@ -15,7 +15,7 @@ where is the main instance of i18n containing several languages and tools.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
### Installing
|
||||
## Installing
|
||||
|
||||
```bash
|
||||
yarn add @ohif/i18n
|
||||
@@ -25,7 +25,7 @@ yarn add @ohif/i18n
|
||||
npm install --save @ohif/i18n
|
||||
```
|
||||
|
||||
### How it works
|
||||
## How it works
|
||||
|
||||
After installing `@ohif/i18n` npm package, the translation function
|
||||
[t](https://www.i18next.com/overview/api#t) can be used [with](#with-react) or
|
||||
@@ -57,13 +57,13 @@ If the translation.json file contains a key that matches the HTML content e.g.
|
||||
|
||||
---
|
||||
|
||||
#### With React
|
||||
### With React
|
||||
|
||||
This section will introduce you to [react-i18next](https://react.i18next.com/)
|
||||
basics and show how to implement the [t](https://www.i18next.com/overview/api#t)
|
||||
function easily.
|
||||
|
||||
##### Using HOCs
|
||||
#### Using HOCs
|
||||
|
||||
In most cases we used
|
||||
[High Order Components](https://react.i18next.com/latest/withtranslation-hoc) to
|
||||
@@ -86,13 +86,13 @@ export default withTranslation('MyNameSpace')(MyComponent);
|
||||
> [I18nextProvider](#using-outside-of-ohif-viewer) section, `withTranslation`
|
||||
> HOC doesnt works without a I18nextProvider
|
||||
|
||||
##### Using Hooks
|
||||
#### Using Hooks
|
||||
|
||||
Also, it's possible to get the `t` tool using
|
||||
[React Hooks](https://react.i18next.com/latest/usetranslation-hook), but it
|
||||
requires at least React > 16.8 😉
|
||||
|
||||
#### Using outside of OHIF viewer
|
||||
### Using outside of OHIF viewer
|
||||
|
||||
OHIF Viewer already sets a main
|
||||
[I18nextProvider](https://react.i18next.com/latest/i18nextprovider) connected to
|
||||
@@ -119,7 +119,7 @@ usage.
|
||||
|
||||
---
|
||||
|
||||
#### Without React
|
||||
### Without React
|
||||
|
||||
When needed, you can also use available translations _without React_.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Themeing
|
||||
# Viewer: Themeing
|
||||
|
||||
Themeing is currently accomplished with color variables that are defined within
|
||||
the [`:root`](https://css-tricks.com/almanac/selectors/r/root/) selector
|
||||
@@ -12,8 +12,10 @@ export default {
|
||||
*/
|
||||
|
||||
preRegistration({
|
||||
servicesManager,
|
||||
configuration: extensionConfiguration,
|
||||
servicesManager = {},
|
||||
commandsManager = {},
|
||||
appConfig = {},
|
||||
configuration = {},
|
||||
}) {},
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,58 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.2...@ohif/extension-cornerstone@2.0.0) (2019-12-09)
|
||||
|
||||
|
||||
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.1...@ohif/extension-cornerstone@1.7.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
@@ -67,6 +67,24 @@ Our Viewport wraps [cornerstonejs/react-cornerstone-viewport][react-viewport]
|
||||
and is connected the redux store. This module is the most prone to change as we
|
||||
hammer out our Viewport interface.
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
Tools can be configured through extension configuration using the tools key:
|
||||
|
||||
```js
|
||||
...
|
||||
cornerstoneExtensionConfig: {
|
||||
tools: {
|
||||
ArrowAnnotate: {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) => callback(prompt('Enter your custom annotation')),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
### Repositories
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "1.7.2",
|
||||
"version": "2.1.1",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -34,7 +34,7 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dcmjs": "^0.8.2",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"prop-types": "^15.6.2",
|
||||
@@ -47,6 +47,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"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"
|
||||
|
||||
@@ -248,10 +248,13 @@ class OHIFCornerstoneViewport extends Component {
|
||||
// TODO: Does it make more sense to use Context?
|
||||
if (this.props.children && this.props.children.length) {
|
||||
childrenWithProps = this.props.children.map((child, index) => {
|
||||
return React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index,
|
||||
});
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -148,18 +148,118 @@ const commandsModule = ({ servicesManager }) => {
|
||||
showDownloadViewportModal: ({ title, viewports }) => {
|
||||
const activeViewportIndex = viewports.activeViewportIndex;
|
||||
const { UIModalService } = servicesManager.services;
|
||||
UIModalService.show({
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
title,
|
||||
contentProps: {
|
||||
activeViewportIndex,
|
||||
onClose: UIModalService.hide,
|
||||
},
|
||||
if (UIModalService) {
|
||||
UIModalService.show({
|
||||
content: CornerstoneViewportDownloadForm,
|
||||
title,
|
||||
contentProps: {
|
||||
activeViewportIndex,
|
||||
onClose: UIModalService.hide,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
updateTableWithNewMeasurementData({
|
||||
toolType,
|
||||
measurementNumber,
|
||||
location,
|
||||
description,
|
||||
}) {
|
||||
// Update all measurements by measurement number
|
||||
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
|
||||
const measurements = measurementApi.tools[toolType].filter(
|
||||
m => m.measurementNumber === measurementNumber
|
||||
);
|
||||
|
||||
measurements.forEach(measurement => {
|
||||
measurement.location = location;
|
||||
measurement.description = description;
|
||||
|
||||
measurementApi.updateMeasurement(measurement.toolType, measurement);
|
||||
});
|
||||
|
||||
measurementApi.syncMeasurementsAndToolData();
|
||||
|
||||
// Update images in all active viewports
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
},
|
||||
getNearbyToolData({ element, canvasCoordinates, availableToolTypes }) {
|
||||
const nearbyTool = {};
|
||||
let pointNearTool = false;
|
||||
|
||||
availableToolTypes.forEach(toolType => {
|
||||
const elementToolData = cornerstoneTools.getToolState(
|
||||
element,
|
||||
toolType
|
||||
);
|
||||
|
||||
if (!elementToolData) {
|
||||
return;
|
||||
}
|
||||
|
||||
elementToolData.data.forEach((toolData, index) => {
|
||||
let elementToolInstance = cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
toolType
|
||||
);
|
||||
|
||||
if (!elementToolInstance) {
|
||||
elementToolInstance = cornerstoneTools.getToolForElement(
|
||||
element,
|
||||
`${toolType}Tool`
|
||||
);
|
||||
}
|
||||
|
||||
if (!elementToolInstance) {
|
||||
console.warn('Tool not found.');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
elementToolInstance.pointNearTool(
|
||||
element,
|
||||
toolData,
|
||||
canvasCoordinates
|
||||
)
|
||||
) {
|
||||
pointNearTool = true;
|
||||
nearbyTool.tool = toolData;
|
||||
nearbyTool.index = index;
|
||||
nearbyTool.toolType = toolType;
|
||||
}
|
||||
});
|
||||
|
||||
if (pointNearTool) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return pointNearTool ? nearbyTool : undefined;
|
||||
},
|
||||
removeToolState: ({ element, toolType, tool }) => {
|
||||
cornerstoneTools.removeToolState(element, toolType, tool);
|
||||
cornerstone.updateImage(element);
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
getNearbyToolData: {
|
||||
commandFn: actions.getNearbyToolData,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
removeToolState: {
|
||||
commandFn: actions.removeToolState,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
updateTableWithNewMeasurementData: {
|
||||
commandFn: actions.updateTableWithNewMeasurementData,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
showDownloadViewportModal: {
|
||||
commandFn: actions.showDownloadViewportModal,
|
||||
storeContexts: ['viewports'],
|
||||
|
||||
@@ -1,9 +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 merge from 'lodash.merge';
|
||||
import queryString from 'query-string';
|
||||
import { SimpleDialog } from '@ohif/ui';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
|
||||
function fallbackMetaDataProvider(type, imageId) {
|
||||
if (!imageId.includes('wado?requestType=WADO')) {
|
||||
@@ -26,30 +27,33 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} configuration
|
||||
* @param {Object} servicesManager
|
||||
* @param {Object} configuration
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
let dialogId = UIDialogService.create({
|
||||
content: SimpleDialog.InputDialog,
|
||||
defaultPosition: {
|
||||
x: (event && event.currentPoints.canvas.x) || 0,
|
||||
y: (event && event.currentPoints.canvas.y) || 0,
|
||||
},
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
label: 'New label',
|
||||
measurementData: data ? { description: data.text } : {},
|
||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||
onSubmit: value => {
|
||||
callback(value);
|
||||
UIDialogService.dismiss({ id: dialogId });
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
|
||||
if (UIDialogService) {
|
||||
let dialogId = UIDialogService.create({
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
content: SimpleDialog.InputDialog,
|
||||
useLastPosition: false,
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
label: 'New label',
|
||||
measurementData: data ? { description: data.text } : {},
|
||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||
onSubmit: value => {
|
||||
callback(value);
|
||||
UIDialogService.dismiss({ id: dialogId });
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
@@ -73,60 +77,61 @@ export default function init({ servicesManager, configuration = {} }) {
|
||||
initCornerstoneTools(defaultCsToolsConfig);
|
||||
|
||||
// ~~ Toooools 🙌
|
||||
const {
|
||||
PanTool,
|
||||
ZoomTool,
|
||||
WwwcTool,
|
||||
MagnifyTool,
|
||||
StackScrollTool,
|
||||
StackScrollMouseWheelTool,
|
||||
// Touch
|
||||
PanMultiTouchTool,
|
||||
ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
FreehandRoiTool,
|
||||
EllipticalRoiTool,
|
||||
DragProbeTool,
|
||||
RectangleRoiTool,
|
||||
// Segmentation
|
||||
BrushTool,
|
||||
} = csTools;
|
||||
const tools = [
|
||||
PanTool,
|
||||
ZoomTool,
|
||||
WwwcTool,
|
||||
MagnifyTool,
|
||||
StackScrollTool,
|
||||
StackScrollMouseWheelTool,
|
||||
csTools.PanTool,
|
||||
csTools.ZoomTool,
|
||||
csTools.WwwcTool,
|
||||
csTools.WwwcRegionTool,
|
||||
csTools.MagnifyTool,
|
||||
csTools.StackScrollTool,
|
||||
csTools.StackScrollMouseWheelTool,
|
||||
// Touch
|
||||
PanMultiTouchTool,
|
||||
ZoomTouchPinchTool,
|
||||
csTools.PanMultiTouchTool,
|
||||
csTools.ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
FreehandRoiTool,
|
||||
EllipticalRoiTool,
|
||||
DragProbeTool,
|
||||
RectangleRoiTool,
|
||||
csTools.ArrowAnnotateTool,
|
||||
csTools.EraserTool,
|
||||
csTools.BidirectionalTool,
|
||||
csTools.LengthTool,
|
||||
csTools.AngleTool,
|
||||
csTools.FreehandRoiTool,
|
||||
csTools.EllipticalRoiTool,
|
||||
csTools.DragProbeTool,
|
||||
csTools.RectangleRoiTool,
|
||||
// Segmentation
|
||||
BrushTool,
|
||||
csTools.BrushTool,
|
||||
];
|
||||
|
||||
tools.forEach(tool => csTools.addTool(tool));
|
||||
|
||||
csTools.addTool(csTools.ArrowAnnotateTool, {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) =>
|
||||
callInputDialog(null, eventDetails, callback),
|
||||
changeTextCallback: (data, eventDetails, callback) =>
|
||||
callInputDialog(data, eventDetails, callback),
|
||||
/* Add extension tools configuration here. */
|
||||
const internalToolsConfig = {
|
||||
ArrowAnnotate: {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) =>
|
||||
callInputDialog(null, eventDetails, callback),
|
||||
changeTextCallback: (data, eventDetails, callback) =>
|
||||
callInputDialog(data, eventDetails, callback),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/* 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, externalToolProps);
|
||||
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 });
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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.0",
|
||||
"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.8.2",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.1",
|
||||
"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,16 +16,88 @@ 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) {
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,36 @@ 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -3,6 +3,119 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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.0.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.6...@ohif/extension-vtk@1.0.0) (2019-12-09)
|
||||
|
||||
|
||||
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.54.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.5...@ohif/extension-vtk@0.54.6) (2019-12-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.54.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.4...@ohif/extension-vtk@0.54.5) (2019-12-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.54.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.3...@ohif/extension-vtk@0.54.4) (2019-12-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.54.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.2...@ohif/extension-vtk@0.54.3) (2019-12-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.54.3",
|
||||
"version": "1.1.6",
|
||||
"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.8.2",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
@@ -52,11 +52,10 @@
|
||||
"react-vtkjs-viewport": "^0.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^1.13.3",
|
||||
"@ohif/ui": "^0.65.1",
|
||||
"@ohif/core": "^2.2.1",
|
||||
"@ohif/ui": "^1.1.4",
|
||||
"cornerstone-tools": "^4.8.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",
|
||||
|
||||
@@ -358,10 +358,13 @@ class OHIFVTKViewport extends Component {
|
||||
// TODO: Does it make more sense to use Context?
|
||||
if (this.props.children && this.props.children.length) {
|
||||
childrenWithProps = this.props.children.map((child, index) => {
|
||||
return React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index,
|
||||
});
|
||||
return (
|
||||
child &&
|
||||
React.cloneElement(child, {
|
||||
viewportIndex: this.props.viewportIndex,
|
||||
key: index,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,74 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.0.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.3...@ohif/core@2.0.0) (2019-12-09)
|
||||
|
||||
|
||||
* feat!: Ability to configure cornerstone tools via extension configuration (#1229) ([55a5806](https://github.com/OHIF/Viewers/commit/55a580659ecb74ca6433461d8f9a05c2a2b69533)), closes [#1229](https://github.com/OHIF/Viewers/issues/1229)
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* modifies the exposed react <App /> components props. The contract for providing configuration for the app has changed. Please reference updated documentation for guidance.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.13.3](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.2...@ohif/core@1.13.3) (2019-12-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "1.13.3",
|
||||
"version": "2.2.1",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -39,6 +39,7 @@
|
||||
"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,80 @@
|
||||
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 were saved with success',
|
||||
};
|
||||
} 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,95 @@
|
||||
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,
|
||||
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,24 @@
|
||||
/**
|
||||
* 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._sopInstanceUID === 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._instance.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;
|
||||
@@ -59,7 +59,7 @@ export class CommandsManager {
|
||||
*
|
||||
* @method
|
||||
* @param {string} contextName - Namespace for commands
|
||||
* @returs {Object} - the matched context
|
||||
* @returns {Object} - the matched context
|
||||
*/
|
||||
getContext(contextName) {
|
||||
const context = this.contexts[contextName];
|
||||
|
||||
@@ -2,13 +2,14 @@ import MODULE_TYPES from './MODULE_TYPES.js';
|
||||
import log from './../log.js';
|
||||
|
||||
export default class ExtensionManager {
|
||||
constructor({ commandsManager, servicesManager }) {
|
||||
constructor({ commandsManager, servicesManager, appConfig = {} }) {
|
||||
this.modules = {};
|
||||
this.registeredExtensionIds = [];
|
||||
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
||||
//
|
||||
this._commandsManager = commandsManager;
|
||||
this._servicesManager = servicesManager;
|
||||
this._appConfig = appConfig;
|
||||
|
||||
this.moduleTypeNames.forEach(moduleType => {
|
||||
this.modules[moduleType] = [];
|
||||
@@ -26,7 +27,7 @@ export default class ExtensionManager {
|
||||
const hasConfiguration = Array.isArray(extension);
|
||||
|
||||
if (hasConfiguration) {
|
||||
const [ohifExtension, configuration] = extensions;
|
||||
const [ohifExtension, configuration] = extension;
|
||||
this.registerExtension(ohifExtension, configuration);
|
||||
} else {
|
||||
this.registerExtension(extension);
|
||||
@@ -70,6 +71,7 @@ export default class ExtensionManager {
|
||||
extension.preRegistration({
|
||||
servicesManager: this._servicesManager,
|
||||
commandsManager: this._commandsManager,
|
||||
appConfig: this._appConfig,
|
||||
configuration,
|
||||
});
|
||||
}
|
||||
@@ -79,7 +81,8 @@ export default class ExtensionManager {
|
||||
const extensionModule = this._getExtensionModule(
|
||||
moduleType,
|
||||
extension,
|
||||
extensionId
|
||||
extensionId,
|
||||
configuration
|
||||
);
|
||||
|
||||
if (extensionModule) {
|
||||
@@ -102,7 +105,7 @@ export default class ExtensionManager {
|
||||
* @param {Object} extension
|
||||
* @param {string} extensionId - Used for logging warnings
|
||||
*/
|
||||
_getExtensionModule(moduleType, extension, extensionId) {
|
||||
_getExtensionModule(moduleType, extension, extensionId, configuration) {
|
||||
const getModuleFnName = 'get' + _capitalizeFirstCharacter(moduleType);
|
||||
const getModuleFn = extension[getModuleFnName];
|
||||
|
||||
@@ -114,6 +117,8 @@ export default class ExtensionManager {
|
||||
const extensionModule = getModuleFn({
|
||||
servicesManager: this._servicesManager,
|
||||
commandsManager: this._commandsManager,
|
||||
appConfig: this._appConfig,
|
||||
configuration,
|
||||
});
|
||||
|
||||
if (!extensionModule) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import log from './../log.js';
|
||||
jest.mock('./../log.js');
|
||||
|
||||
describe('ExtensionManager.js', () => {
|
||||
let extensionManager, commandsManager;
|
||||
let extensionManager, commandsManager, servicesManager, appConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
commandsManager = {
|
||||
@@ -14,7 +14,17 @@ describe('ExtensionManager.js', () => {
|
||||
getContext: jest.fn(),
|
||||
registerCommand: jest.fn(),
|
||||
};
|
||||
extensionManager = new ExtensionManager({ commandsManager });
|
||||
servicesManager = {
|
||||
registerService: jest.fn(),
|
||||
};
|
||||
appConfig = {
|
||||
testing: true,
|
||||
};
|
||||
extensionManager = new ExtensionManager({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
appConfig,
|
||||
});
|
||||
log.warn.mockClear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -37,6 +47,24 @@ describe('ExtensionManager.js', () => {
|
||||
// Assert
|
||||
expect(extensionManager.registerExtension.mock.calls.length).toBe(3);
|
||||
});
|
||||
|
||||
it('calls registerExtension() for each extension passing its configuration if tuple', () => {
|
||||
const fakeConfiguration = { testing: true };
|
||||
extensionManager.registerExtension = jest.fn();
|
||||
|
||||
// SUT
|
||||
const fakeExtensions = [
|
||||
{ one: '1' },
|
||||
[{ two: '2' }, fakeConfiguration],
|
||||
{ three: '3 ' },
|
||||
];
|
||||
extensionManager.registerExtensions(fakeExtensions);
|
||||
|
||||
// Assert
|
||||
expect(extensionManager.registerExtension.mock.calls[1][1]).toEqual(
|
||||
fakeConfiguration
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerExtension()', () => {
|
||||
@@ -49,19 +77,19 @@ describe('ExtensionManager.js', () => {
|
||||
expect(fakeExtension.preRegistration.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('calls preRegistration() passing configuration along with servicesManager and commandsManager instances for extension', () => {
|
||||
const configuration = { config: 'Some configuration' };
|
||||
extensionManager._servicesManager = { services: { TestService: {} } };
|
||||
it('calls preRegistration() passing dependencies and extension configuration to extension', () => {
|
||||
const extensionConfiguration = { config: 'Some configuration' };
|
||||
|
||||
// SUT
|
||||
const fakeExtension = { one: '1', preRegistration: jest.fn() };
|
||||
extensionManager.registerExtension(fakeExtension, configuration);
|
||||
const extension = { one: '1', preRegistration: jest.fn() };
|
||||
extensionManager.registerExtension(extension, extensionConfiguration);
|
||||
|
||||
// Assert
|
||||
expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({
|
||||
servicesManager: extensionManager._servicesManager,
|
||||
commandsManager: extensionManager._commandsManager,
|
||||
configuration,
|
||||
expect(extension.preRegistration.mock.calls[0][0]).toEqual({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
appConfig,
|
||||
configuration: extensionConfiguration,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,8 +163,8 @@ describe('ExtensionManager.js', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('successfully passes a servicesManager and commandsManager instances to each module', () => {
|
||||
extensionManager._servicesManager = { services: { TestService: {} } };
|
||||
it('successfully passes dependencies to each module along with extension configuration', () => {
|
||||
const extensionConfiguration = { testing: true };
|
||||
|
||||
const extension = {
|
||||
id: 'hello-world',
|
||||
@@ -147,11 +175,17 @@ describe('ExtensionManager.js', () => {
|
||||
getCommandsModule: jest.fn(),
|
||||
};
|
||||
|
||||
extensionManager.registerExtension(extension);
|
||||
extensionManager.registerExtension(extension, extensionConfiguration);
|
||||
|
||||
expect(extension.getViewportModule.mock.calls[0][0]).toEqual({
|
||||
servicesManager: extensionManager._servicesManager,
|
||||
commandsManager: extensionManager._commandsManager,
|
||||
Object.keys(extension).forEach(module => {
|
||||
if (typeof extension[module] === 'function') {
|
||||
expect(extension[module].mock.calls[0][0]).toEqual({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
appConfig,
|
||||
configuration: extensionConfiguration,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ServicesManager } from './services';
|
||||
import classes, { CommandsManager, HotkeysManager } from './classes/';
|
||||
|
||||
import DICOMWeb from './DICOMWeb';
|
||||
import DICOMSR from './DICOMSR';
|
||||
import cornerstone from './cornerstone.js';
|
||||
import hangingProtocols from './hanging-protocols';
|
||||
import header from './header.js';
|
||||
@@ -20,9 +21,9 @@ import user from './user.js';
|
||||
import utils from './utils/';
|
||||
|
||||
import {
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
} from './services';
|
||||
|
||||
const OHIF = {
|
||||
@@ -46,13 +47,14 @@ const OHIF = {
|
||||
object,
|
||||
log,
|
||||
DICOMWeb,
|
||||
DICOMSR,
|
||||
viewer: {},
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -76,12 +78,13 @@ export {
|
||||
object,
|
||||
log,
|
||||
DICOMWeb,
|
||||
DICOMSR,
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -10,9 +10,9 @@ describe('Top level exports', () => {
|
||||
'HotkeysManager',
|
||||
'ServicesManager',
|
||||
//
|
||||
'createUINotificationService',
|
||||
'createUIModalService',
|
||||
'createUIDialogService',
|
||||
'UINotificationService',
|
||||
'UIModalService',
|
||||
'UIDialogService',
|
||||
//
|
||||
'utils',
|
||||
'studies',
|
||||
@@ -28,6 +28,7 @@ describe('Top level exports', () => {
|
||||
'object',
|
||||
'log',
|
||||
'DICOMWeb',
|
||||
'DICOMSR',
|
||||
'OHIF', //
|
||||
'measurements',
|
||||
'hangingProtocols',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import log from '../../log';
|
||||
import getLabel from '../lib/getLabel';
|
||||
import getDescription from '../lib/getDescription';
|
||||
@@ -7,13 +8,32 @@ import guid from '../../utils/guid';
|
||||
import studyMetadataManager from '../../utils/studyMetadataManager';
|
||||
import { measurementApiDefaultConfig } from './../configuration.js';
|
||||
|
||||
|
||||
const configuration = {
|
||||
...measurementApiDefaultConfig,
|
||||
...measurementApiDefaultConfig
|
||||
};
|
||||
|
||||
export default class MeasurementApi {
|
||||
static Instance;
|
||||
|
||||
/**
|
||||
* Set configuration: It should merge default configuration with any new one
|
||||
*
|
||||
* @static
|
||||
* @param {Object} config
|
||||
* @param {Object} config.server
|
||||
* @param {string} config.server.type - The server type
|
||||
* @param {string} config.server.wadoRoot - The server wado URL root
|
||||
* @param {Array} config.measurementTools
|
||||
* @param {string} config.measurementTools[].id - The tool group id
|
||||
* @param {string} config.measurementTools[].name - The tool group name
|
||||
* @param {Array} config.measurementTools[].childTools - The child tool's configuration
|
||||
* @param {Object} config.dataExchange
|
||||
* @param {Function} config.dataExchange.store - Function that store measurement data
|
||||
* @param {Function} config.dataExchange.retrieve - Function that retrieves measurement data
|
||||
*
|
||||
* @memberof MeasurementApi
|
||||
*/
|
||||
static setConfiguration(config) {
|
||||
Object.assign(configuration, config);
|
||||
}
|
||||
@@ -203,13 +223,14 @@ export default class MeasurementApi {
|
||||
|
||||
retrieveMeasurements(patientId, timepointIds) {
|
||||
const retrievalFn = configuration.dataExchange.retrieve;
|
||||
const { server } = configuration;
|
||||
if (typeof retrievalFn !== 'function') {
|
||||
log.error('Measurement retrieval function has not been configured.');
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
retrievalFn(patientId, timepointIds).then(measurementData => {
|
||||
retrievalFn(server).then(measurementData => {
|
||||
if (measurementData) {
|
||||
log.info('Measurement data retrieval');
|
||||
log.info(measurementData);
|
||||
@@ -230,6 +251,10 @@ export default class MeasurementApi {
|
||||
// Synchronize the new tool data
|
||||
this.syncMeasurementsAndToolData();
|
||||
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
|
||||
// Let others know that the measurements are updated
|
||||
this.onMeasurementsUpdated();
|
||||
}, reject);
|
||||
@@ -237,6 +262,7 @@ export default class MeasurementApi {
|
||||
}
|
||||
|
||||
storeMeasurements(timepointId) {
|
||||
const { server } = configuration;
|
||||
const storeFn = configuration.dataExchange.store;
|
||||
if (typeof storeFn !== 'function') {
|
||||
log.error('Measurement store function has not been configured.');
|
||||
@@ -278,8 +304,9 @@ export default class MeasurementApi {
|
||||
};
|
||||
|
||||
log.info('Saving Measurements for timepoints:', timepoints);
|
||||
return storeFn(measurementData, filter).then(() => {
|
||||
return storeFn(measurementData, filter, server).then(result => {
|
||||
log.info('Measurement storage completed');
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ export default class ServicesManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new service.
|
||||
*
|
||||
* @param {Object} service
|
||||
* @param {Object} configuration
|
||||
*/
|
||||
registerService(service) {
|
||||
registerService(service, configuration = {}) {
|
||||
if (!service) {
|
||||
log.warn(
|
||||
'Attempting to register a null/undefined service. Exiting early.'
|
||||
@@ -18,32 +20,47 @@ export default class ServicesManager {
|
||||
return;
|
||||
}
|
||||
|
||||
let serviceName = service.name;
|
||||
|
||||
if (!serviceName) {
|
||||
if (!service.name) {
|
||||
log.warn(`Service name not set. Exiting early.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.registeredServiceNames.includes(serviceName)) {
|
||||
if (this.registeredServiceNames.includes(service.name)) {
|
||||
log.warn(
|
||||
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
|
||||
`Service name ${service.name} has already been registered. Exiting before duplicating services.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.services[service.name] = service;
|
||||
if (service.create) {
|
||||
this.services[service.name] = service.create({
|
||||
configuration,
|
||||
});
|
||||
} else {
|
||||
log.warn(`Service create factory function not defined. Exiting early.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Track service registration
|
||||
this.registeredServiceNames.push(serviceName);
|
||||
/* Track service registration */
|
||||
this.registeredServiceNames.push(service.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of services.
|
||||
* An array of services, or an array of arrays that contains service
|
||||
* configuration pairs.
|
||||
*
|
||||
* @param {Object[]} services - Array of services
|
||||
*/
|
||||
registerServices(services) {
|
||||
services.forEach(service => this.registerService(service));
|
||||
services.forEach(service => {
|
||||
const hasConfiguration = Array.isArray(service);
|
||||
|
||||
if (hasConfiguration) {
|
||||
const [ohifService, configuration] = service;
|
||||
this.registerService(ohifService, configuration);
|
||||
} else {
|
||||
this.registerService(service);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,18 +16,32 @@ describe('ServicesManager.js', () => {
|
||||
it('calls registerService() for each service', () => {
|
||||
servicesManager.registerService = jest.fn();
|
||||
|
||||
const fakeServices = [
|
||||
{ name: 'UINotificationTestService', hide: jest.fn() },
|
||||
{ name: 'UIModalTestService', hide: jest.fn() },
|
||||
];
|
||||
|
||||
servicesManager.registerServices(fakeServices);
|
||||
servicesManager.registerServices([
|
||||
{ name: 'UINotificationTestService', create: jest.fn() },
|
||||
{ name: 'UIModalTestService', create: jest.fn() },
|
||||
]);
|
||||
|
||||
expect(servicesManager.registerService.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('calls registerService() for each service passing its configuration if tuple', () => {
|
||||
servicesManager.registerService = jest.fn();
|
||||
const fakeConfiguration = { testing: true };
|
||||
|
||||
servicesManager.registerServices([
|
||||
{ name: 'UINotificationTestService', create: jest.fn() },
|
||||
[{ name: 'UIModalTestService', create: jest.fn() }, fakeConfiguration],
|
||||
]);
|
||||
|
||||
expect(servicesManager.registerService.mock.calls[1][1]).toEqual(
|
||||
fakeConfiguration
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerService()', () => {
|
||||
const fakeService = { name: 'UINotificationService', create: jest.fn() };
|
||||
|
||||
it('logs a warning if the service is null or undefined', () => {
|
||||
const undefinedService = undefined;
|
||||
const nullService = null;
|
||||
@@ -39,8 +53,8 @@ describe('ServicesManager.js', () => {
|
||||
});
|
||||
|
||||
it('logs a warning if the service does not have a name', () => {
|
||||
const serviceWithEmptyName = { name: '', hide: jest.fn() };
|
||||
const serviceWithoutName = { hide: jest.fn() };
|
||||
const serviceWithEmptyName = { name: '', create: jest.fn() };
|
||||
const serviceWithoutName = { create: jest.fn() };
|
||||
|
||||
servicesManager.registerService(serviceWithEmptyName);
|
||||
servicesManager.registerService(serviceWithoutName);
|
||||
@@ -48,23 +62,37 @@ describe('ServicesManager.js', () => {
|
||||
expect(log.warn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('tracks which services have been registered', () => {
|
||||
const service = {
|
||||
name: 'UINotificationService',
|
||||
};
|
||||
it('logs a warning if the service does not have a create factory function', () => {
|
||||
const serviceWithoutCreate = { name: 'UINotificationService' };
|
||||
|
||||
servicesManager.registerService(service);
|
||||
|
||||
expect(servicesManager.registeredServiceNames).toContain(service.name);
|
||||
});
|
||||
|
||||
it('logs a warning if the service has an name that has already been registered', () => {
|
||||
const service = { name: 'UINotificationService' };
|
||||
|
||||
servicesManager.registerService(service);
|
||||
servicesManager.registerService(service);
|
||||
servicesManager.registerService(serviceWithoutCreate);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('tracks which services have been registered', () => {
|
||||
servicesManager.registerService(fakeService);
|
||||
|
||||
expect(servicesManager.registeredServiceNames).toContain(
|
||||
fakeService.name
|
||||
);
|
||||
});
|
||||
|
||||
it('logs a warning if the service has an name that has already been registered', () => {
|
||||
servicesManager.registerService(fakeService);
|
||||
servicesManager.registerService(fakeService);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('pass dependencies and configuration to service create factory function', () => {
|
||||
const configuration = { config: 'Some configuration' };
|
||||
|
||||
servicesManager.registerService(fakeService, configuration);
|
||||
|
||||
expect(fakeService.create.mock.calls[0][0]).toEqual({
|
||||
configuration,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,59 +17,60 @@
|
||||
* @property {Object} contentProps The dialog content props.
|
||||
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
|
||||
* @property {boolean} [showOverlay=false] Controls dialog overlay.
|
||||
* @property {boolean} [centralize=false] Center the dialog on the screen.
|
||||
* @property {boolean} [preservePosition=true] Use last position instead of default.
|
||||
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
||||
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
|
||||
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||
* @property {Function} onStop Called when dragging stops.
|
||||
* @property {Function} onDrag Called while dragging.
|
||||
*/
|
||||
|
||||
const uiDialogServicePublicAPI = {
|
||||
name: 'UIDialogService',
|
||||
dismiss,
|
||||
dismissAll,
|
||||
create,
|
||||
const name = 'UIDialogService';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
dismiss: _dismiss,
|
||||
dismissAll: _dismissAll,
|
||||
create: _create,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const uiDialogServiceImplementation = {
|
||||
const serviceImplementation = {
|
||||
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
|
||||
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
|
||||
_create: () => console.warn('create() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUIDialogService() {
|
||||
return uiDialogServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a new UI dialog;
|
||||
*
|
||||
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, isDraggable, showOverlay, defaultPosition, position }
|
||||
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, centralize, isDraggable, showOverlay, preservePosition, defaultPosition }
|
||||
*/
|
||||
function create({
|
||||
function _create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
centralize = false,
|
||||
preservePosition = true,
|
||||
isDraggable = true,
|
||||
showOverlay = false,
|
||||
defaultPosition,
|
||||
position,
|
||||
}) {
|
||||
return uiDialogServiceImplementation._create({
|
||||
return serviceImplementation._create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
centralize,
|
||||
preservePosition,
|
||||
isDraggable,
|
||||
showOverlay,
|
||||
defaultPosition,
|
||||
position,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,8 +79,8 @@ function create({
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function dismissAll() {
|
||||
return uiDialogServiceImplementation._dismissAll();
|
||||
function _dismissAll() {
|
||||
return serviceImplementation._dismissAll();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,8 +88,8 @@ function dismissAll() {
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function dismiss({ id }) {
|
||||
return uiDialogServiceImplementation._dismiss({ id });
|
||||
function _dismiss({ id }) {
|
||||
return serviceImplementation._dismiss({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,14 +107,19 @@ function setServiceImplementation({
|
||||
create: createImplementation,
|
||||
}) {
|
||||
if (dismissImplementation) {
|
||||
uiDialogServiceImplementation._dismiss = dismissImplementation;
|
||||
serviceImplementation._dismiss = dismissImplementation;
|
||||
}
|
||||
if (dismissAllImplementation) {
|
||||
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
|
||||
serviceImplementation._dismissAll = dismissAllImplementation;
|
||||
}
|
||||
if (createImplementation) {
|
||||
uiDialogServiceImplementation._create = createImplementation;
|
||||
serviceImplementation._create = createImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default createUIDialogService;
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
@@ -11,28 +11,26 @@
|
||||
* @property {string} [customClassName=null] The custom class to style the modal.
|
||||
*/
|
||||
|
||||
const uiModalServicePublicAPI = {
|
||||
name: 'UIModalService',
|
||||
hide,
|
||||
show,
|
||||
const name = 'UIModalService';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
hide: _hide,
|
||||
show: _show,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const uiModalServiceImplementation = {
|
||||
const serviceImplementation = {
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUIModalService() {
|
||||
return uiModalServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a new UI modal;
|
||||
*
|
||||
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
|
||||
*/
|
||||
function show({
|
||||
function _show({
|
||||
content = null,
|
||||
contentProps = null,
|
||||
shouldCloseOnEsc = false,
|
||||
@@ -41,7 +39,7 @@ function show({
|
||||
title = null,
|
||||
customClassName = null,
|
||||
}) {
|
||||
return uiModalServiceImplementation._show({
|
||||
return serviceImplementation._show({
|
||||
content,
|
||||
contentProps,
|
||||
shouldCloseOnEsc,
|
||||
@@ -57,8 +55,8 @@ function show({
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function hide() {
|
||||
return uiModalServiceImplementation._hide();
|
||||
function _hide() {
|
||||
return serviceImplementation._hide();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,11 +72,16 @@ function setServiceImplementation({
|
||||
show: showImplementation,
|
||||
}) {
|
||||
if (hideImplementation) {
|
||||
uiModalServiceImplementation._hide = hideImplementation;
|
||||
serviceImplementation._hide = hideImplementation;
|
||||
}
|
||||
if (showImplementation) {
|
||||
uiModalServiceImplementation._show = showImplementation;
|
||||
serviceImplementation._show = showImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default createUIModalService;
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
@@ -10,22 +10,20 @@
|
||||
* @property {boolean} [autoClose=true]
|
||||
*/
|
||||
|
||||
const uiNotificationServicePublicAPI = {
|
||||
name: 'UINotificationService',
|
||||
hide,
|
||||
show,
|
||||
const name = 'UINotificationService';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
hide: _hide,
|
||||
show: _show,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const uiNotificationServiceImplementation = {
|
||||
const serviceImplementation = {
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUINotificationService() {
|
||||
return uiNotificationServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a new UI notification; returns the
|
||||
* ID of the created notification.
|
||||
@@ -33,7 +31,7 @@ function createUINotificationService() {
|
||||
* @param {Notification} notification { title, message, duration, position, type, autoClose}
|
||||
* @returns {number} id
|
||||
*/
|
||||
function show({
|
||||
function _show({
|
||||
title,
|
||||
message,
|
||||
duration = 5000,
|
||||
@@ -41,7 +39,7 @@ function show({
|
||||
type = 'info',
|
||||
autoClose = true,
|
||||
}) {
|
||||
return uiNotificationServiceImplementation._show({
|
||||
return serviceImplementation._show({
|
||||
title,
|
||||
message,
|
||||
duration,
|
||||
@@ -57,8 +55,8 @@ function show({
|
||||
* @param {number} id - id of the notification to hide/dismiss
|
||||
* @returns undefined
|
||||
*/
|
||||
function hide(id) {
|
||||
return uiNotificationServiceImplementation._hide({ id });
|
||||
function _hide(id) {
|
||||
return serviceImplementation._hide({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,11 +72,16 @@ function setServiceImplementation({
|
||||
show: showImplementation,
|
||||
}) {
|
||||
if (hideImplementation) {
|
||||
uiNotificationServiceImplementation._hide = hideImplementation;
|
||||
serviceImplementation._hide = hideImplementation;
|
||||
}
|
||||
if (showImplementation) {
|
||||
uiNotificationServiceImplementation._show = showImplementation;
|
||||
serviceImplementation._show = showImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default createUINotificationService;
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import ServicesManager from './ServicesManager.js';
|
||||
import createUINotificationService from './UINotificationService';
|
||||
import createUIModalService from './UIModalService';
|
||||
import createUIDialogService from './UIDialogService';
|
||||
import UINotificationService from './UINotificationService';
|
||||
import UIModalService from './UIModalService';
|
||||
import UIDialogService from './UIDialogService';
|
||||
|
||||
export {
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
ServicesManager,
|
||||
};
|
||||
@@ -3,6 +3,25 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.3...@ohif/i18n@0.52.4) (2019-12-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/i18n
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.3](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.2...@ohif/i18n@0.52.3) (2019-12-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* translations ([#1234](https://github.com/OHIF/Viewers/issues/1234)) ([30b9e44](https://github.com/OHIF/Viewers/commit/30b9e4422073557287ef26a80b38eeb3f3fcff4c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# @ohif/i18n
|
||||
|
||||
More information available at
|
||||
[OHIF Docs](https://docs.ohif.org/essentials/translating.html).
|
||||
[OHIF Docs](https://docs.ohif.org/viewer/internationalization.html).
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/i18n",
|
||||
"version": "0.52.2",
|
||||
"version": "0.52.4",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -29,19 +29,20 @@ function addLocales(newLocales) {
|
||||
customDebug(resourceBundle, 'info');
|
||||
}
|
||||
|
||||
// Note: Developers can add the API key to use the in-context editor.
|
||||
// DO NOT commit the API key
|
||||
const config = (window.config && window.config.i18n) || {};
|
||||
|
||||
/*
|
||||
* Note: Developers can add the API key to use the
|
||||
* in-context editor using environment variables.
|
||||
* (DO NOT commit the API key)
|
||||
*/
|
||||
const locizeOptions = {
|
||||
projectId: config.LOCIZE_PROJECTID,
|
||||
apiKey: config.LOCIZE_API_KEY,
|
||||
projectId: process.env.LOCIZE_PROJECTID,
|
||||
apiKey: process.env.LOCIZE_API_KEY,
|
||||
referenceLng: 'en-US',
|
||||
fallbacklng: 'en-US',
|
||||
};
|
||||
|
||||
const envUseLocize = !!config.USE_LOCIZE;
|
||||
const envApiKeyAvailable = !!config.LOCIZE_API_KEY;
|
||||
const envUseLocize = !!process.env.USE_LOCIZE;
|
||||
const envApiKeyAvailable = !!process.env.LOCIZE_API_KEY;
|
||||
const DEFAULT_LANGUAGE = 'en-US';
|
||||
|
||||
function initI18n(
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"Clear": "Clear",
|
||||
"Coronal": "Coronal",
|
||||
"Crosshairs": "Crosshairs",
|
||||
"Download": "Download",
|
||||
"Ellipse": "Ellipse",
|
||||
"Elliptical": "Elliptical",
|
||||
"Flip H": "Flip H",
|
||||
|
||||
@@ -10,18 +10,19 @@
|
||||
"Clear": "Limpiar",
|
||||
"Coronal": "Coronal",
|
||||
"Crosshairs": "Punto de mira",
|
||||
"Download": "Descargar",
|
||||
"Ellipse": "Elipse",
|
||||
"Elliptical": "Elíptico",
|
||||
"Flip H": "Espejo H",
|
||||
"Flip V": "Espejo V",
|
||||
"Flip H": "Voltear H",
|
||||
"Flip V": "Voltear V",
|
||||
"Freehand": "Mano alzada",
|
||||
"Invert": "Invertir",
|
||||
"Invert": "Negativo",
|
||||
"Layout": "$t(Common:Layout)",
|
||||
"Length": "Longitud",
|
||||
"Levels": "Niveles",
|
||||
"Magnify": "Aumentar",
|
||||
"Levels": "W/L",
|
||||
"Magnify": "Lupa",
|
||||
"Manual": "Manual",
|
||||
"Measurements": "Mediciones",
|
||||
"Measurements": "Medidas",
|
||||
"More": "$t(Common:More)",
|
||||
"Next": "$t(Common:Next)",
|
||||
"Pan": "Mover",
|
||||
@@ -32,11 +33,11 @@
|
||||
"Rectangle": "Rectángulo",
|
||||
"Reset": "$t(Common:Reset)",
|
||||
"Reset to Defaults": "$t(Common:Reset) por defecto",
|
||||
"Rotate Right": "Rotar ->",
|
||||
"Rotate Right": "Girar ->",
|
||||
"Sagittal": "Sagital",
|
||||
"Save": "Guardar",
|
||||
"Stack Scroll": "Scroll en pila",
|
||||
"Stack Scroll": "Scroll",
|
||||
"Stop": "$t(Common:Stop)",
|
||||
"Themes": "Temas",
|
||||
"Zoom": "Zoom"
|
||||
"Zoom": "Ampliar"
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"Image": "Imagen",
|
||||
"Layout": "Disposición",
|
||||
"Layout": "Formato",
|
||||
"Measurements": "Medidas",
|
||||
"More": "Más",
|
||||
"Next": "Siguiente",
|
||||
"Play": "Play",
|
||||
"Previous": "Anterior",
|
||||
"Reset": "Resetear",
|
||||
"Reset": "Restaurar",
|
||||
"RowsPerPage": "filas por página",
|
||||
"Series": "Secuencia",
|
||||
"Show": "Mostrar",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"Clear": "Limpar",
|
||||
"Coronal": "Coronal",
|
||||
"Crosshairs": "Localizador",
|
||||
"Download": "Baixar",
|
||||
"Ellipse": "Elipse",
|
||||
"Elliptical": "Elíptico",
|
||||
"Flip H": "Inverter H",
|
||||
@@ -30,8 +31,8 @@
|
||||
"Probe": "Prova",
|
||||
"ROI Window": "Janela ROI",
|
||||
"Rectangle": "Retângulo",
|
||||
"Reset": "Resetar",
|
||||
"Reset to Defaults": "Restaurar para o Padrão",
|
||||
"Reset": "$t(Common:Reset)",
|
||||
"Reset to Defaults": "$t(Common:Reset) para o Padrão",
|
||||
"Rotate Right": "Girar à direita",
|
||||
"Sagittal": "Sagital",
|
||||
"Save": "Salvar",
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"Next": "Próximo",
|
||||
"Play": "Play",
|
||||
"Previous": "Anterior",
|
||||
"Reset": "Reset",
|
||||
"Reset": "Restaurar",
|
||||
"Stop": "Stop"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"Cancel": "Cancelar",
|
||||
"Reset to Defaults": "Restaurar Default",
|
||||
"Reset to Defaults": "$t(Common:Reset) para Padrão",
|
||||
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
|
||||
"Save": "Salvar",
|
||||
"SaveMessage": "Preferências salvas",
|
||||
|
||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user