Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70672ed663 | ||
|
|
858484f250 | ||
|
|
83f9478977 | ||
|
|
b5b851ac7c | ||
|
|
e5175bf3e6 | ||
|
|
8c8f69accb | ||
|
|
f9037f831d | ||
|
|
9e0bd52c6a | ||
|
|
f1b017dc08 | ||
|
|
81dcbe294d | ||
|
|
e09a3d2e93 |
No files matched your search
@@ -48,7 +48,7 @@ Check out these helpful links:
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[ohif-org]: https://www.ohif.org
|
||||
[ohif-org]: http://www.ohif.org
|
||||
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
||||
[gh-issues]: https://github.com/OHIF/Viewers/issues
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -5,6 +5,7 @@
|
||||
- [Getting Started](development/getting-started.md)
|
||||
- [Contributing](development/contributing.md)
|
||||
- [Continuous Integration](development/continous-integration.md)
|
||||
- [Testing](development/testing.md)
|
||||
- [Configuring](configuring/index.md)
|
||||
- [Data Source](configuring/data-source.md)
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ 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)
|
||||
|
||||
@@ -74,10 +73,10 @@ This diagram is a conceptual illustration of how the Viewer is architected.
|
||||
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](../services/default/measurements.md)
|
||||
- Managing a study's measurements
|
||||
- Managing a study's DICOM metadata
|
||||
- [A flexible pattern for extensions](../extensions/index.md)
|
||||
- And many others
|
||||
@@ -99,10 +98,6 @@ application.
|
||||
|
||||
[Check out our component library!](https://react.ohif.org/)
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
...
|
||||
|
||||
## Extensions & Configuration
|
||||
|
||||
While OHIF maintains several high value and commonly requested features in its
|
||||
@@ -120,7 +115,7 @@ the viewer. Some common use cases include:
|
||||
We expose common integration points via [extensions](../extensions/index.md) to
|
||||
make this possible. The viewer and many of our own extensions also offer
|
||||
[configuration][configuration]. For a list of extensions maintained by OHIF,
|
||||
[check out this helpful table](../extensions/index.mdl#maintained-extensions).
|
||||
[check out this helpful table](../extensions/index.md#maintained-extensions).
|
||||
|
||||
If you find yourself thinking "I wish the Viewer could do X", and you can't
|
||||
accomplish it with an extension today, create a GitHub issue! We're actively
|
||||
@@ -147,12 +142,12 @@ you would need to recreate.
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[monorepo]: #
|
||||
[monorepo]: https://github.com/OHIF/Viewers/issues/768
|
||||
[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]: ../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
|
||||
[ui-github]: https://github.com/OHIF/Viewers/tree/master/platform/ui
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -123,7 +123,7 @@ There are two important steps to making sure this setup works:
|
||||
Most image archives do not provide either of these features "out of the box".
|
||||
It's common to use IIS, Nginx, or Apache to route incoming requests and append
|
||||
appropriate headers. You can find an example of this setup in our
|
||||
[Nginx + Image Archive Deployment Recipe](deployment/recipes/nginx--image-archive.md).
|
||||
[Nginx + Image Archive Deployment Recipe](./recipes/nginx--image-archive.md).
|
||||
|
||||
#### What if my archive doesn't support DicomWeb?
|
||||
|
||||
@@ -166,7 +166,7 @@ window.config = {
|
||||
```
|
||||
|
||||
You can find an example of this setup in our
|
||||
[User Account Control Deployment Recipe](deployment/recipes/user-account-control.md).
|
||||
[User Account Control Deployment Recipe](./recipes/user-account-control.md).
|
||||
|
||||
#### Choosing a Flow for the Viewer
|
||||
|
||||
@@ -183,12 +183,12 @@ many possible configurations, so please don't feel limited to these setups.
|
||||
Please feel free to suggest or contribute your own recipes.
|
||||
|
||||
- Script Include
|
||||
- [Embedding the Viewer](deployment/recipes/embedded-viewer.md)
|
||||
- [Embedding the Viewer](./recipes/embedded-viewer.md)
|
||||
- Stand-Alone
|
||||
- [Build for Production](deployment/recipes/build-for-production.md)
|
||||
- [Static](deployment/recipes/static-assets.md)
|
||||
- [Nginx + Image Archive](deployment/recipes/nginx--image-archive.md)
|
||||
- [User Account Control](deployment/recipes/user-account-control.md)
|
||||
- [Build for Production](./recipes/build-for-production.md)
|
||||
- [Static](./recipes/static-assets.md)
|
||||
- [Nginx + Image Archive](./recipes/nginx--image-archive.md)
|
||||
- [User Account Control](./recipes/user-account-control.md)
|
||||
|
||||
<!--
|
||||
Links
|
||||
|
||||
@@ -50,7 +50,7 @@ window.config = {
|
||||
```
|
||||
|
||||
To learn more about how you can configure the OHIF Viewer, check out our
|
||||
[Configuration Guide](./index.md).
|
||||
[Configuration Guide](../../configuring/index.md).
|
||||
|
||||
<ol start="3"><li>
|
||||
Render the viewer in the web page's target <code>div</code>
|
||||
|
||||
@@ -49,9 +49,7 @@ We can solve this one of two ways:
|
||||
1. Have our Image Archive located at the same domain as our Web App
|
||||
2. Add appropriate `Access-Control-Allow-*` HTTP headers
|
||||
|
||||
This solution uses the first approach, but you can see an example of the second
|
||||
in the `docker-compose` bundled with this project for local development:
|
||||
[HERE](https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/Nginx-Orthanc/docker-compose.yml)
|
||||
**This solution uses the first approach.**
|
||||
|
||||
You can read more about CORS in this Medium article: [Understanding
|
||||
CORS][understanding-cors]
|
||||
@@ -121,7 +119,10 @@ likely want to update:
|
||||
|
||||
#### OHIF Viewer
|
||||
|
||||
The OHIF Viewer's configuration is imported from a static `.js` file. The configuration we use is set to a specific file when we build the viewer, and determined by the env variable: `APP_CONFIG`. You can see where we set its value in the `dockerfile` for this solution:
|
||||
The OHIF Viewer's configuration is imported from a static `.js` file. The
|
||||
configuration we use is set to a specific file when we build the viewer, and
|
||||
determined by the env variable: `APP_CONFIG`. You can see where we set its value
|
||||
in the `dockerfile` for this solution:
|
||||
|
||||
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
|
||||
|
||||
@@ -138,11 +139,11 @@ Viewer's configuration, you can run:
|
||||
|
||||
All other files are found in: `/docker/OpenResty-Orthanc/`
|
||||
|
||||
| Service | Configuration | Docs |
|
||||
| ----------------- | ---------------------------------------------- | ------------------------------------------- |
|
||||
| OHIF Viewer | [dockerfile][dockerfile] / [config.js][config] | You're reading them now! |
|
||||
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
|
||||
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
|
||||
| Service | Configuration | Docs |
|
||||
| ----------------- | --------------------------------- | ------------------------------------------- |
|
||||
| OHIF Viewer | [dockerfile][dockerfile] | You're reading them now! |
|
||||
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
|
||||
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
|
||||
|
||||
## Next Steps
|
||||
|
||||
@@ -218,10 +219,11 @@ following resources helpful:
|
||||
- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/)
|
||||
- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/)
|
||||
|
||||
For a different take on this setup, check out the repository one of our
|
||||
community members put together:
|
||||
For a different take on this setup, check out the repositories our community
|
||||
members put together:
|
||||
|
||||
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
|
||||
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
|
||||
|
||||
<!--
|
||||
Links
|
||||
@@ -234,8 +236,7 @@ community members put together:
|
||||
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
|
||||
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
|
||||
<!-- SRC -->
|
||||
[config]: #
|
||||
[dockerfile]: #
|
||||
[config-nginx]: #
|
||||
[config-orthanc]: #
|
||||
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
|
||||
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf
|
||||
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -122,7 +122,10 @@ likely want to update:
|
||||
|
||||
#### OHIF Viewer
|
||||
|
||||
The OHIF Viewer's configuration is imported from a static `.js` file. The configuration we use is set to a specific file when we build the viewer, and determined by the env variable: `APP_CONFIG`. You can see where we set its value in the `dockerfile` for this solution:
|
||||
The OHIF Viewer's configuration is imported from a static `.js` file. The
|
||||
configuration we use is set to a specific file when we build the viewer, and
|
||||
determined by the env variable: `APP_CONFIG`. You can see where we set its value
|
||||
in the `dockerfile` for this solution:
|
||||
|
||||
`ENV APP_CONFIG=config/docker_openresty-orthanc-keycloak.js`
|
||||
|
||||
@@ -146,8 +149,8 @@ All other files are found in: `/docker/OpenResty-Orthanc-Keycloak/`
|
||||
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
|
||||
| Keycloak | [`/ohif-keycloak-realm.json`][config-keycloak]\* | |
|
||||
|
||||
- \* These are the seed values for Keycloak. They can be manually updated at
|
||||
`http://127.0.0.1/auth/admin`
|
||||
\* These are the seed values for Keycloak. They can be manually updated at
|
||||
`http://127.0.0.1/auth/admin`
|
||||
|
||||
#### Keycloak Themeing
|
||||
|
||||
@@ -261,12 +264,12 @@ for OAuth:
|
||||
|
||||
- [Diagrams of OpenID Connect Flows](https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660)
|
||||
- [KeyCloak: OpenID Connect Flows](https://www.keycloak.org/docs/latest/securing_apps/index.html#authorization-code)
|
||||
- [Good description on SSO Protocols](https://www.keycloak.org/docs/2.5/server_admin/topics/sso-protocols/oidc.html)
|
||||
|
||||
For a different take on this setup, check out the repository one of our
|
||||
community members put together:
|
||||
For a different take on this setup, check out the repositories our community
|
||||
members put together:
|
||||
|
||||
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
|
||||
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
|
||||
|
||||
<!--
|
||||
Links
|
||||
@@ -278,8 +281,8 @@ community members put together:
|
||||
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
|
||||
<!-- SRC -->
|
||||
[config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/src/config.js
|
||||
[dockerfile]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/.recipes
|
||||
[config-nginx]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/.recipes
|
||||
[config-orthanc]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/.recipes
|
||||
[config-keycloak]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/.recipes
|
||||
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/dockerfile
|
||||
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/nginx.conf
|
||||
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/orthanc.json
|
||||
[config-keycloak]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/config/ohif-keycloak-realm.json
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -114,8 +114,8 @@ the tips above will help ensure your changes are reviewed.
|
||||
|
||||
## Testing contribution pull requests
|
||||
|
||||
OHIF uses [netlify](netlify.com) so that pull requests are autogenerated and
|
||||
available for testing.
|
||||
OHIF uses [netlify](https://www.netlify.com/) so that pull requests are
|
||||
autogenerated and available for testing.
|
||||
|
||||
For example, [this url][example-url] allows you to test [pull request 237, the
|
||||
request that created this FAQ entry,][pr-237] using data pulled from Amazon S3.
|
||||
|
||||
@@ -132,7 +132,7 @@ feature `X` or scenario `Y`? Open an issue and let's discuss.
|
||||
[flow-org]: https://flow.org/
|
||||
<!-- Talks -->
|
||||
[assert-js-talks]: https://www.youtube.com/playlist?list=PLZ66c9_z3umNSrKSb5cmpxdXZcIPNvKGw
|
||||
[kent-talk]: https://www.youtube.com/watch?list=PLV5CVI1eNcJgNqzNwcs4UKrlJdhfDjshf
|
||||
[kent-talk]: https://www.youtube.com/watch?v=Fha2bVoC8SE
|
||||
[gleb-talk]: https://www.youtube.com/watch?v=5FnalKRjpZk
|
||||
[kent-blog]: https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests
|
||||
<!-- Images -->
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
</td>
|
||||
<td>Viewport, Toolbar</td>
|
||||
</tr>
|
||||
<!-- dicom-html -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-html">DICOM HTML</a>
|
||||
@@ -40,6 +41,7 @@
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<!-- dicom-pdf -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-pdf">DICOM PDF</a>
|
||||
@@ -49,6 +51,7 @@
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<!-- dicom-microscopy -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-microscopy">DICOM Microscopy</a>
|
||||
@@ -58,5 +61,15 @@
|
||||
</td>
|
||||
<td>Viewport, SopClassHandler</td>
|
||||
</tr>
|
||||
<!-- dicom-segmentation -->
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.npmjs.com/package/@ohif/extension-dicom-segmentation">DICOM Segmentation</a>
|
||||
</td>
|
||||
<td>
|
||||
Renders segmentation images for a <a href="https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7">specific SopClassUID</a>.
|
||||
</td>
|
||||
<td>Panel, Toolbar</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -74,7 +74,8 @@ export default {
|
||||
### Registering an Extension
|
||||
|
||||
There are two different ways to register and configure extensions: At
|
||||
[runtime](#runtime-extensions) and at [build time](#bundled-extensions).
|
||||
[runtime](#registering-at-runtime) and at
|
||||
[build time](#registering-at-build-time).
|
||||
|
||||
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
|
||||
@@ -82,9 +83,9 @@ 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.
|
||||
The `@ohif/viewer` uses a [configuration file](../viewer/configuration.md) at
|
||||
startup. The schema for that file includes an `Extensions` key that supports an
|
||||
array of extensions to register.
|
||||
|
||||
```js
|
||||
// prettier-ignore
|
||||
@@ -193,8 +194,8 @@ today, create a GitHub issue!
|
||||
|
||||
The `ExtensionManager` is a class made available to us via the `@ohif/core`
|
||||
project (platform/core). Our application instantiates a single instance of it,
|
||||
and provides a `ServicesManager` and `CommandsManager` along with the application's
|
||||
configuration through the appConfig key (optional).
|
||||
and provides a `ServicesManager` and `CommandsManager` along with the
|
||||
application's configuration through the appConfig key (optional).
|
||||
|
||||
```js
|
||||
const commandsManager = new CommandsManager();
|
||||
@@ -202,7 +203,7 @@ const servicesManager = new ServicesManager();
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
appConfig
|
||||
appConfig,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -212,8 +213,9 @@ The `ExtensionManager` only has a few public members:
|
||||
- `registerExtensions` - Registers an array of extensions
|
||||
- `modules` - An object containing registered extensions by `MODULE_TYPE`
|
||||
|
||||
During registration, lifecycle hooks and modules have access to the extension's config,
|
||||
the application's config and `ExtensionManager`'s `ServicesManager` and `CommandsManager` instances.
|
||||
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.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 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:
|
||||
An extension can register a [SOP Class][sop-class-link] Handler Module by
|
||||
defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link]
|
||||
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.
|
||||
@@ -76,6 +76,13 @@ getSopClassHandlerModule({ servicesManager, commandsManager }) {
|
||||
};
|
||||
```
|
||||
|
||||
### More examples :
|
||||
|
||||
- [Dicom-HTML SOP][dicom-html-sop]
|
||||
- [Dicom-PDF SOP][dicom-pdf-sop]
|
||||
- [Dicom-Microscopy SOP][dicom-micro-sop]
|
||||
- [Dicom-Segmentation SOP][dicom-seg-sop]
|
||||
|
||||
## `@ohif/viewer` usage
|
||||
|
||||
We use the `sopClassHandlerModule`s in three different places:
|
||||
@@ -88,3 +95,11 @@ 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`.
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html
|
||||
[dicom-html-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12
|
||||
[dicom-pdf-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6
|
||||
[dicom-micro-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7
|
||||
[dicom-seg-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -75,7 +75,7 @@ items to our ["Active Development" GitHub Project Board][gh-board]. Items on
|
||||
this project board are either in active development by Core Team members, or
|
||||
queued up for development as in-progress items are completed.
|
||||
|
||||
> :pencil: Want to contribute but not sure where to start? Check out [Up for
|
||||
> 🖋 Want to contribute but not sure where to start? Check out [Up for
|
||||
> grabs][label-grabs] issues and our [Contributing
|
||||
> documentation][contributing-docs]
|
||||
|
||||
@@ -156,5 +156,5 @@ addressed and a Stable version is released.
|
||||
[ohif-prod]: https://viewer.ohif.org
|
||||
[gh-board]: https://github.com/OHIF/Viewers/projects/4
|
||||
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
|
||||
[contributing-docs]: https://docs.ohif.org/contributing/
|
||||
[contributing-docs]: ./development/contributing.md
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -62,7 +62,7 @@ able to fail or be removed. Related patterns that may reduce coupling include:
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[commands]: #/
|
||||
[commands]: ../extensions/modules/commands.md
|
||||
[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services
|
||||
[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js
|
||||
[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Example](#example)
|
||||
- [Tips & Tricks](#/)
|
||||
- [Maintained Services](#/)
|
||||
- [Tips & Tricks](#tips--tricks)
|
||||
- [Maintained Services](#maintained-services)
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ console.log(T('$t(Common:Play) my translated text'));
|
||||
|
||||
# Main Concepts While Translating
|
||||
|
||||
## - Namespaces
|
||||
## Namespaces
|
||||
|
||||
Namespaces are being used to organize translations in smaller portions, combined
|
||||
semantically or by use. Each `.json` file inside `@ohif/i18n` npm package
|
||||
@@ -157,7 +157,7 @@ NameSpace, like this following example getting data from `Common` NameSpace:
|
||||
$t(Common:Reset)
|
||||
```
|
||||
|
||||
## - Extending Languages in @ohif/i18n
|
||||
## Extending Languages in @ohif/i18n
|
||||
|
||||
Sometimes, even using the same language, some nouns or jargons can change
|
||||
according to the country, states or even from Hospital to Hospital.
|
||||
@@ -212,7 +212,7 @@ object like this:
|
||||
Please check the `index.js` files inside locales folder for an example of this
|
||||
exporting structure.
|
||||
|
||||
### - Extending languages dynamically
|
||||
### Extending languages dynamically
|
||||
|
||||
You have access to the i18next instance, so you can use the
|
||||
[addResourceBundle](https://www.i18next.com/how-to/add-or-load-translations#add-after-init)
|
||||
@@ -314,4 +314,4 @@ REACT_APP_I18N_DEBUG=true yarn run dev
|
||||
### Contributing with new languages
|
||||
|
||||
Contributions of any kind are welcome! Please check the
|
||||
[instructions](https://docs.ohif.org/contributing.html).
|
||||
[instructions](../development/contributing.md).
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.1...@ohif/extension-cornerstone@2.5.2) (2020-04-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.0...@ohif/extension-cornerstone@2.5.1) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.5.1",
|
||||
"version": "2.5.2",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -7,6 +7,9 @@ import { getEnabledElement } from './state';
|
||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||
const scroll = cornerstoneTools.import('util/scroll');
|
||||
|
||||
const { studyMetadataManager } = OHIF.utils;
|
||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||
|
||||
const commandsModule = ({ servicesManager }) => {
|
||||
const actions = {
|
||||
rotateViewport: ({ viewports, rotation }) => {
|
||||
@@ -255,9 +258,35 @@ const commandsModule = ({ servicesManager }) => {
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
jumpToImage: ({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
activeViewportIndex
|
||||
}) => {
|
||||
const study = studyMetadataManager.get(StudyInstanceUID);
|
||||
|
||||
const displaySet = study.findDisplaySet(ds => {
|
||||
return ds.images.find(i => i.getSOPInstanceUID() === SOPInstanceUID)
|
||||
});
|
||||
|
||||
displaySet.SOPInstanceUID = SOPInstanceUID;
|
||||
displaySet.frameIndex = frameIndex;
|
||||
|
||||
window.store.dispatch(setViewportSpecificData(activeViewportIndex, displaySet));
|
||||
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
jumpToImage: {
|
||||
commandFn: actions.jumpToImage,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
getNearbyToolData: {
|
||||
commandFn: actions.getNearbyToolData,
|
||||
storeContexts: [],
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
const webpack = require('webpack');
|
||||
const merge = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './..');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'OHIFExtDicomSeg',
|
||||
libraryTarget: 'umd',
|
||||
libraryExport: 'default',
|
||||
filename: pkg.main,
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.LimitChunkCountPlugin({
|
||||
maxChunks: 1,
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## 0.0.2 (2020-04-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Open Health Imaging Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1 @@
|
||||
# @ohif/extension-dicom-rt
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-rt",
|
||||
"version": "0.0.2",
|
||||
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||
"dev:dicom-html": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"dcmjs": "^0.8.3",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"react-select": "^3.0.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { MODULE_TYPES, utils, DICOMWeb } from '@ohif/core';
|
||||
import loadRTStruct from './loadRTStruct';
|
||||
|
||||
import id from './id';
|
||||
|
||||
// TODO: Should probably use dcmjs for this
|
||||
const SOP_CLASS_UIDS = {
|
||||
DICOM_RT_STRUCT: '1.2.840.10008.5.1.4.1.1.481.3',
|
||||
};
|
||||
|
||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
const OHIFDicomRTStructSopClassHandler = {
|
||||
id: 'OHIFDicomRTStructSopClassHandler',
|
||||
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUIDs,
|
||||
getDisplaySetFromSeries: function (
|
||||
series,
|
||||
study,
|
||||
dicomWebClient,
|
||||
authorizationHeaders
|
||||
) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
const metadata = instance.getData().metadata;
|
||||
const {
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesDescription,
|
||||
FrameOfReferenceUID,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
} = metadata;
|
||||
|
||||
// TODO -> GET REFERENCED FRAME OF REFERENCE SEQUENCE.
|
||||
|
||||
const rtStructDisplaySet = {
|
||||
Modality: 'RTSTRUCT',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
authorizationHeaders,
|
||||
metadata,
|
||||
isDerived: true,
|
||||
referencedDisplaySetUID: null, // Assigned when loaded.
|
||||
labelmapIndex: null, // Assigned when loaded.
|
||||
isLoaded: false,
|
||||
SeriesDate,
|
||||
SeriesTime,
|
||||
SeriesDescription,
|
||||
};
|
||||
|
||||
if (!metadata.ReferencedSeriesSequence) {
|
||||
const ReferencedFrameOfReferenceSequence =
|
||||
metadata.ReferencedFrameOfReferenceSequence;
|
||||
|
||||
if (ReferencedFrameOfReferenceSequence) {
|
||||
// TODO -> @dannyrb Do we augment metadata or add a (potentially large? fallback list in filterDerivedDisplaySets )
|
||||
metadata.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
ReferencedFrameOfReferenceSequence
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||
return loadRTStruct(
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies
|
||||
).catch(error => {
|
||||
rtStructDisplaySet.isLoaded = false;
|
||||
throw new Error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return rtStructDisplaySet;
|
||||
},
|
||||
};
|
||||
|
||||
function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||
ReferencedFrameOfReferenceSequence
|
||||
) {
|
||||
const ReferencedSeriesSequence = [];
|
||||
|
||||
_getSequenceAsArray(ReferencedFrameOfReferenceSequence).forEach(
|
||||
referencedFrameOfReference => {
|
||||
const { RTReferencedStudySequence } = referencedFrameOfReference;
|
||||
|
||||
_getSequenceAsArray(RTReferencedStudySequence).forEach(
|
||||
rtReferencedStudy => {
|
||||
const { RTReferencedSeriesSequence } = rtReferencedStudy;
|
||||
|
||||
_getSequenceAsArray(RTReferencedSeriesSequence).forEach(
|
||||
rtReferencedSeries => {
|
||||
const ReferencedInstanceSequence = [];
|
||||
const {
|
||||
ContourImageSequence,
|
||||
SeriesInstanceUID,
|
||||
} = rtReferencedSeries;
|
||||
|
||||
_getSequenceAsArray(ContourImageSequence).forEach(
|
||||
contourImage => {
|
||||
ReferencedInstanceSequence.push({
|
||||
ReferencedSOPInstanceUID:
|
||||
contourImage.ReferencedSOPInstanceUID,
|
||||
ReferencedSOPClassUID: contourImage.ReferencedSOPClassUID,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const referencedSeries = {
|
||||
SeriesInstanceUID,
|
||||
ReferencedInstanceSequence,
|
||||
};
|
||||
|
||||
ReferencedSeriesSequence.push(referencedSeries);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return ReferencedSeriesSequence;
|
||||
}
|
||||
|
||||
function _getSequenceAsArray(sequence) {
|
||||
return Array.isArray(sequence) ? sequence : [sequence];
|
||||
}
|
||||
|
||||
function _getReferencedFrameOfReferenceSequence(instance) {
|
||||
const referencedFrameOfReferenceSequenceRaw = instance['30060010'];
|
||||
|
||||
const referencedFrameOfReferenceSequence = [];
|
||||
|
||||
if (
|
||||
!referencedFrameOfReferenceSequenceRaw ||
|
||||
!referencedFrameOfReferenceSequenceRaw.Value
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
referencedFrameOfReferenceSequenceRaw.Value.forEach(
|
||||
referencedFrameOfReferenceRaw => {
|
||||
const frameOfReferenceUID = DICOMWeb.getString(
|
||||
referencedFrameOfReferenceRaw['00200052']
|
||||
);
|
||||
|
||||
const referencedFrameOfReference = { frameOfReferenceUID };
|
||||
const RTReferencedStudySequenceRaw =
|
||||
referencedFrameOfReferenceRaw['30060012'];
|
||||
|
||||
referencedFrameOfReferenceSequence.push(referencedFrameOfReference);
|
||||
|
||||
if (RTReferencedStudySequenceRaw && RTReferencedStudySequenceRaw.Value) {
|
||||
referencedFrameOfReference.rtReferencedStudySequence = [];
|
||||
|
||||
const { rtReferencedStudySequence } = referencedFrameOfReference;
|
||||
|
||||
RTReferencedStudySequenceRaw.Value.forEach(rtReferencedStudyRaw => {
|
||||
const referencedSopClassUID = DICOMWeb.getString(
|
||||
rtReferencedStudyRaw['00081150']
|
||||
);
|
||||
|
||||
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||
rtReferencedStudyRaw['00081155']
|
||||
);
|
||||
|
||||
const rtReferencedStudy = {
|
||||
referencedSopClassUID,
|
||||
referencedSOPInstanceUID,
|
||||
rtReferencedSeriesSequence: [],
|
||||
};
|
||||
|
||||
rtReferencedStudySequence.push(rtReferencedStudy);
|
||||
|
||||
const { rtReferencedSeriesSequence } = rtReferencedStudy;
|
||||
|
||||
const rtReferencedSeriesSequenceRaw =
|
||||
rtReferencedStudyRaw['30060014'];
|
||||
|
||||
rtReferencedSeriesSequenceRaw.Value.forEach(rtReferencedSeriesRaw => {
|
||||
const seriesInstanceUID = DICOMWeb.getString(
|
||||
rtReferencedSeriesRaw['0020000E']
|
||||
);
|
||||
|
||||
const rtReferencedSeries = {
|
||||
seriesInstanceUID,
|
||||
contourImageSequence: [],
|
||||
};
|
||||
|
||||
rtReferencedSeriesSequence.push(rtReferencedSeries);
|
||||
|
||||
const { contourImageSequence } = rtReferencedSeries;
|
||||
|
||||
const contourImageSequenceRaw = rtReferencedSeriesRaw['30060016'];
|
||||
|
||||
contourImageSequenceRaw.Value.forEach(contourImageRaw => {
|
||||
const referencedSOPClassUID = DICOMWeb.getString(
|
||||
contourImageRaw['00081150']
|
||||
);
|
||||
|
||||
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||
contourImageRaw['00081155']
|
||||
);
|
||||
|
||||
const referencedFrameNumber = DICOMWeb.getString(
|
||||
contourImageRaw['00081160']
|
||||
);
|
||||
const referencedSegmentNumber = DICOMWeb.getString(
|
||||
contourImageRaw['0062000B']
|
||||
);
|
||||
|
||||
const contourImage = {
|
||||
referencedSOPClassUID,
|
||||
referencedSOPInstanceUID,
|
||||
referencedFrameNumber,
|
||||
referencedSegmentNumber,
|
||||
};
|
||||
|
||||
contourImageSequence.push(contourImage);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return referencedFrameOfReferenceSequence;
|
||||
}
|
||||
|
||||
export default OHIFDicomRTStructSopClassHandler;
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
|
||||
const LoadingIndicator = ({ height = "200px", width = "200px", expand = false }) => {
|
||||
return (
|
||||
<div style={expand ? { height: "100%", width: "100%" } : {}}>
|
||||
<svg style={{ margin: 'auto', background: 'none', display: 'block', shapeRendering: 'auto' }} width={width} height={height} viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||
<g transform="rotate(0 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(30 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(60 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(90 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(120 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(150 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(180 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(210 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(240 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(270 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(300 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g><g transform="rotate(330 50 50)">
|
||||
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
|
||||
</rect>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingIndicator;
|
||||
@@ -0,0 +1,44 @@
|
||||
.dcmrt-panel-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-primary-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dcmrt-panel-section .header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--ui-gray-dark);
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.dcmrt-panel-section .header .eye-icon,
|
||||
.dcmrt-panel-section .header .angle-double-down,
|
||||
.dcmrt-panel-section .header .angle-double-up {
|
||||
cursor: pointer;
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.dcmrt-panel-section .header .eye-icon:hover,
|
||||
.dcmrt-panel-section .header .angle-double-down:hover,
|
||||
.dcmrt-panel-section .header .angle-double-up:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.dcmrt-panel-section .header .eye-icon.expanded {
|
||||
color: var(--default-color);
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.icons .angle-double-down, .icons .angle-double-up {
|
||||
margin-left: 15px;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@ohif/ui';
|
||||
|
||||
import './PanelSection.css';
|
||||
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
|
||||
|
||||
const PanelSection = ({
|
||||
title,
|
||||
children,
|
||||
visible = false,
|
||||
expanded = false,
|
||||
loading = false,
|
||||
hideVisibleButton = false,
|
||||
onVisibilityChange = () => { },
|
||||
onExpandChange = () => { }
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(expanded);
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(visible);
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dcmrt-panel-section"
|
||||
style={{
|
||||
marginBottom: isExpanded ? 0 : 2,
|
||||
height: isExpanded ? '100%' : 'unset'
|
||||
}}
|
||||
>
|
||||
<div className="header">
|
||||
<div>{title}</div>
|
||||
<div className="icons">
|
||||
{!hideVisibleButton && (
|
||||
<Icon
|
||||
className={`eye-icon ${isVisible && 'expanded'}`}
|
||||
name={isVisible ? "eye" : "eye-closed"}
|
||||
width="20px"
|
||||
height="20px"
|
||||
onClick={() => {
|
||||
const newVisibility = !isVisible;
|
||||
setIsVisible(newVisibility);
|
||||
onVisibilityChange(newVisibility);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Icon
|
||||
className={`angle-double-${isExpanded ? 'down' : 'up'} ${isExpanded && 'expanded'}`}
|
||||
name={`angle-double-${isExpanded ? 'down' : 'up'}`}
|
||||
width="20px"
|
||||
height="20px"
|
||||
onClick={() => {
|
||||
const newExpandValue = !isExpanded;
|
||||
setIsExpanded(newExpandValue);
|
||||
onExpandChange(newExpandValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{loading && isExpanded && <LoadingIndicator expand height="70px" width="70px" />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PanelSection.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||
visible: PropTypes.bool,
|
||||
expanded: PropTypes.bool,
|
||||
onVisibilityChange: PropTypes.func
|
||||
};
|
||||
|
||||
PanelSection.defaultProps = {
|
||||
visible: false,
|
||||
expanded: false,
|
||||
onVisibilityChange: () => { }
|
||||
};
|
||||
|
||||
export default PanelSection;
|
||||
@@ -0,0 +1,26 @@
|
||||
.dcmrt-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--primary-background-color);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header .cog-icon {
|
||||
align-self: flex-end;
|
||||
color: var(--default-color);
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dcmrt-panel .dcmrt-panel-header .cog-icon:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
|
||||
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import './RTPanel.css';
|
||||
import StructureSetItem from '../StructureSetItem/StructureSetItem';
|
||||
import RTPanelSettings from '../RTSettings/RTSettings';
|
||||
import PanelSection from '../PanelSection/PanelSection';
|
||||
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
|
||||
|
||||
const { studyMetadataManager } = utils;
|
||||
|
||||
const refreshViewport = () => {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* RTPanel component
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {Array} props.studies
|
||||
* @param {Array} props.viewports - viewportSpecificData
|
||||
* @param {number} props.activeIndex - activeViewportIndex
|
||||
* @param {number} props.isOpen - isOpen
|
||||
* @returns component
|
||||
*/
|
||||
const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }) => {
|
||||
const [selectedContour, setSelectedContour] = useState();
|
||||
const DEFAULT_SET_INDEX = 0;
|
||||
const DEFAULT_STATE = {
|
||||
referencedDisplaysets: [],
|
||||
sets: [],
|
||||
selectedSet: null,
|
||||
};
|
||||
|
||||
const [state, setState] = useState(DEFAULT_STATE);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const activeViewport = viewports[activeIndex];
|
||||
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||
*
|
||||
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
||||
* or other components that could rely on loaded rtstructs that
|
||||
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
||||
* before the structs are fully loaded, the panel can subscribe to this custom event
|
||||
* and update itself with the new structs.
|
||||
*
|
||||
* This limitation is due to the fact that the rtmodule is an object (which will be
|
||||
* updated after the structs are loaded) that React its not aware of its changes
|
||||
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||
*
|
||||
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
useEffect(() => {
|
||||
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateStructureSets = () => {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const StructureSets = module.state.StructureSets;
|
||||
|
||||
if (StructureSets && StructureSets.length) {
|
||||
const viewportSets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||
activeViewport.SeriesInstanceUID
|
||||
);
|
||||
|
||||
const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID);
|
||||
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||
Modality: 'RTSTRUCT',
|
||||
});
|
||||
|
||||
if (viewportSets.length) {
|
||||
const defaultSet = viewportSets[DEFAULT_SET_INDEX];
|
||||
setState({
|
||||
referencedDisplaysets,
|
||||
selectedSet: defaultSet,
|
||||
sets: viewportSets
|
||||
});
|
||||
} else {
|
||||
setState(DEFAULT_STATE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateStructureSets();
|
||||
}, [studies, viewports, activeIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowSettings(showSettings && !isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
const toContourItem = ({ ROINumber, ROIName, RTROIObservations, colorArray, visible }, loadedSet) => {
|
||||
let interpretedType = '';
|
||||
if (RTROIObservations && RTROIObservations.RTROIInterpretedType) {
|
||||
interpretedType = `(${RTROIObservations.RTROIInterpretedType})`;
|
||||
}
|
||||
|
||||
const isSameContour = selectedContour && selectedContour === ROINumber;
|
||||
return (
|
||||
<StructureSetItem
|
||||
key={ROINumber}
|
||||
selected={isSameContour}
|
||||
onClick={() => {
|
||||
setSelectedContour(isSameContour ? null : ROINumber);
|
||||
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
const element = enabledElements[activeIndex].element;
|
||||
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||
|
||||
if (!toolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = toolState.data[0].imageIds;
|
||||
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const imageId = module.getters.imageIdOfCenterFrameOfROIContour(
|
||||
state.selectedSet.SeriesInstanceUID,
|
||||
ROINumber,
|
||||
imageIds
|
||||
);
|
||||
|
||||
const frameIndex = imageIds.indexOf(imageId);
|
||||
const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId);
|
||||
const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId);
|
||||
|
||||
onContourItemClick({
|
||||
StudyInstanceUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex,
|
||||
activeViewportIndex: activeIndex
|
||||
});
|
||||
}}
|
||||
label={`${ROIName} ${interpretedType}`}
|
||||
index={ROINumber}
|
||||
color={colorArray}
|
||||
visible={visible}
|
||||
onVisibilityChange={() => {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
module.setters.toggleROIContour(state.selectedSet.SeriesInstanceUID, ROINumber);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const configurationChangeHandler = newConfiguration => {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
module.configuration.lineWidth = newConfiguration.lineWidth;
|
||||
module.configuration.opacity = newConfiguration.opacity;
|
||||
refreshViewport();
|
||||
};
|
||||
|
||||
if (showSettings) {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
return (
|
||||
<RTPanelSettings
|
||||
configuration={module.configuration}
|
||||
onBack={() => setShowSettings(false)}
|
||||
onChange={configurationChangeHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dcmrt-panel">
|
||||
<div className="dcmrt-panel-header">
|
||||
<h3>RT Structure Sets</h3>
|
||||
<Icon
|
||||
className="cog-icon"
|
||||
name="cog"
|
||||
width="25px"
|
||||
height="25px"
|
||||
onClick={() => setShowSettings(true)}
|
||||
/>
|
||||
</div>
|
||||
{!state.referencedDisplaysets.length && <LoadingIndicator expand height="70px" width="70px" />}
|
||||
{state.referencedDisplaysets.map(displaySet => {
|
||||
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
|
||||
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||
|
||||
const loadedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||
return (
|
||||
<PanelSection
|
||||
key={SeriesInstanceUID}
|
||||
title={metadata.StructureSetLabel}
|
||||
loading={!isLoaded || !loadedSet}
|
||||
visible={isLoaded && loadedSet.visible}
|
||||
hideVisibleButton={!isLoaded}
|
||||
expanded={isLoaded && loadedSet.SeriesInstanceUID === state.selectedSet.SeriesInstanceUID}
|
||||
onVisibilityChange={() => {
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
module.setters.toggleStructureSet(SeriesInstanceUID);
|
||||
}}
|
||||
onExpandChange={async () => {
|
||||
if (!isLoaded) {
|
||||
await displaySet.load(viewports[activeIndex], studies);
|
||||
const module = cornerstoneTools.getModule('rtstruct');
|
||||
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||
const selectedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||
setState(state => ({ ...state, selectedSet, sets }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ScrollableArea>
|
||||
<TableList headless>
|
||||
{isLoaded && loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))}
|
||||
</TableList>
|
||||
</ScrollableArea>
|
||||
</PanelSection>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RTPanel.propTypes = {
|
||||
viewports: PropTypes.shape({
|
||||
displaySetInstanceUID: PropTypes.string,
|
||||
frameRate: PropTypes.any,
|
||||
InstanceNumber: PropTypes.number,
|
||||
isMultiFrame: PropTypes.bool,
|
||||
isReconstructable: PropTypes.bool,
|
||||
Modality: PropTypes.string,
|
||||
plugin: PropTypes.string,
|
||||
SeriesDate: PropTypes.string,
|
||||
SeriesDescription: PropTypes.string,
|
||||
SeriesInstanceUID: PropTypes.string,
|
||||
SeriesNumber: PropTypes.any,
|
||||
SeriesTime: PropTypes.string,
|
||||
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
|
||||
StudyInstanceUID: PropTypes.string,
|
||||
}),
|
||||
activeIndex: PropTypes.number.isRequired,
|
||||
studies: PropTypes.array.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
RTPanel.defaultProps = {};
|
||||
|
||||
export default RTPanel;
|
||||
@@ -0,0 +1,88 @@
|
||||
.dcmrt-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dcmrt-settings .settings-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.dcmrt-settings .settings-title h3 {
|
||||
color: var(--text-primary-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.dcmrt-settings .settings-section .content {
|
||||
padding: 10px;
|
||||
background-color: var(--ui-gray-dark);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.dcmrt-settings .settings-section .header {
|
||||
border-bottom: 2px solid black;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--ui-gray-dark);
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.dcmrt-settings .settings-section p {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dcmrt-settings .range {
|
||||
display: flex;
|
||||
padding: 10px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dcmrt-settings .range:not(:last-of-type) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.dcmrt-settings .range label {
|
||||
margin: 0;
|
||||
display: unset;
|
||||
min-width: 40px;
|
||||
max-width: 40px;
|
||||
}
|
||||
|
||||
.dcmrt-settings .range input {
|
||||
width: 100%;
|
||||
padding: 0 20px 0 20px;
|
||||
}
|
||||
|
||||
.dcmrt-settings .return-button {
|
||||
color: #333;
|
||||
background-color: #fff;
|
||||
padding: 4px 12px;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.dcmrt-settings .return-button:hover,
|
||||
.dcmrt-settings .return-button:active,
|
||||
.dcmrt-settings .return-button.active {
|
||||
background-color: var(--active-color);
|
||||
border-color: var(--ui-border-color-active);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Range } from '@ohif/ui';
|
||||
|
||||
import './RTSettings.css';
|
||||
|
||||
const RTSettings = ({ configuration, onBack, onChange }) => {
|
||||
const toFloat = value => parseFloat(value / 100).toFixed(2);
|
||||
|
||||
const save = (field, value) => {
|
||||
onChange({ ...configuration, [field]: value });
|
||||
};
|
||||
|
||||
const SettingsSection = ({ title, children }) => {
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div className="header">{title}</div>
|
||||
<div className="content">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dcmrt-settings">
|
||||
<div className="settings-title">
|
||||
<h3>RT Structure Set Settings</h3>
|
||||
<button className="return-button" onClick={onBack}>
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<SettingsSection title="Segment Outline">
|
||||
<div className="range">
|
||||
<label htmlFor="range">Opacity</label>
|
||||
<Range
|
||||
showPercentage
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
value={configuration.opacity * 100}
|
||||
onChange={event => save('opacity', toFloat(event.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="range">
|
||||
<label htmlFor="range">Width</label>
|
||||
<Range
|
||||
showValue
|
||||
step={1}
|
||||
min={1}
|
||||
max={5}
|
||||
value={configuration.lineWidth}
|
||||
onChange={event => save('lineWidth', parseInt(event.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RTSettings.propTypes = {
|
||||
configuration: PropTypes.shape({
|
||||
lineWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
}).isRequired,
|
||||
onBack: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RTSettings;
|
||||
@@ -0,0 +1,85 @@
|
||||
.dcmrt-structure-set-item .item-color-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-color {
|
||||
margin-top: 8px;
|
||||
border-radius: 100%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-info {
|
||||
display: inline-block;
|
||||
margin-top: 9px;
|
||||
padding-left: 9px;
|
||||
width: 90px;
|
||||
border-left: 1px solid var(--text-secondary-color);
|
||||
color: var(--text-primary-color);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item.selected .item-actions {
|
||||
height: 35px;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-actions {
|
||||
margin-left: -1px;
|
||||
background-color: var(--ui-gray-darker);
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
visibility: hidden;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-actions .btnAction {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--default-color);
|
||||
cursor: pointer;
|
||||
line-height: 35px;
|
||||
height: 35px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-actions .btnAction:hover, .dcmrt-structure-set-item .item-actions .btnAction:active {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-actions .btnAction i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item.selected .item-actions {
|
||||
height: 35px;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-label {
|
||||
overflow: hidden;
|
||||
max-width: calc(100% - 15px); /* calc(100% - 50px); 20px = eye icon */
|
||||
text-overflow: ellipsis;
|
||||
overflow-wrap: normal;
|
||||
white-space: nowrap;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-label .eye-icon {
|
||||
cursor: pointer;
|
||||
color: var(--active-color);
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-label .eye-icon:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
|
||||
.dcmrt-structure-set-item .item-label .eye-icon.--visible {
|
||||
color: var(--default-color);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TableListItem, Icon } from '@ohif/ui';
|
||||
|
||||
import './StructureSetItem.css';
|
||||
|
||||
const ColoredCircle = ({ color }) => {
|
||||
return (
|
||||
<div
|
||||
className="item-color"
|
||||
style={{ backgroundColor: `rgba(${color.join(',')})` }}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
ColoredCircle.propTypes = {
|
||||
color: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
const StructureSetItem = ({
|
||||
index,
|
||||
label,
|
||||
onClick,
|
||||
itemClass,
|
||||
color,
|
||||
visible = true,
|
||||
onVisibilityChange,
|
||||
selected = false,
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(visible);
|
||||
return (
|
||||
<div className={`dcmrt-structure-set-item ${selected && 'selected'}`}>
|
||||
<TableListItem
|
||||
key={index}
|
||||
itemKey={index}
|
||||
itemIndex={index}
|
||||
itemClass={itemClass}
|
||||
itemMeta={<ColoredCircle color={color} />}
|
||||
itemMetaClass="item-color-section"
|
||||
onItemClick={onClick}
|
||||
>
|
||||
<div>
|
||||
<div className="item-label" style={{ marginBottom: 4 }}>
|
||||
{label}
|
||||
<Icon
|
||||
className={`eye-icon ${isVisible && '--visible'}`}
|
||||
name={isVisible ? 'eye' : 'eye-closed'}
|
||||
width="20px"
|
||||
height="20px"
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
const newVisibility = !isVisible;
|
||||
setIsVisible(newVisibility);
|
||||
onVisibilityChange(newVisibility);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{false && <div className="item-info">{'...'}</div>}
|
||||
{false && (
|
||||
<div className="item-actions">
|
||||
<button
|
||||
className="btnAction"
|
||||
onClick={() => console.log('Relabelling...')}
|
||||
>
|
||||
<span style={{ marginRight: '4px' }}>
|
||||
<Icon name="edit" width="14px" height="14px" />
|
||||
</span>
|
||||
Relabel
|
||||
</button>
|
||||
<button
|
||||
className="btnAction"
|
||||
onClick={() => console.log('Editing description...')}
|
||||
>
|
||||
<span style={{ marginRight: '4px' }}>
|
||||
<Icon name="edit" width="14px" height="14px" />
|
||||
</span>
|
||||
Description
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TableListItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
StructureSetItem.propTypes = {
|
||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
itemClass: PropTypes.string,
|
||||
color: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
StructureSetItem.defaultProps = {
|
||||
itemClass: '',
|
||||
onClick: () => { },
|
||||
};
|
||||
|
||||
export default StructureSetItem;
|
||||
@@ -0,0 +1,3 @@
|
||||
const id = 'rt';
|
||||
|
||||
export default id;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
|
||||
import init from './init.js';
|
||||
import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler';
|
||||
import id from './id.js';
|
||||
import RTPanel from './components/RTPanel/RTPanel';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id,
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getPanelModule({ commandsManager }) {
|
||||
const ExtendedRTPanel = props => {
|
||||
const contourItemClickHandler = contourData => {
|
||||
commandsManager.runCommand('jumpToImage', contourData);
|
||||
};
|
||||
|
||||
return (
|
||||
<RTPanel
|
||||
{...props}
|
||||
onContourItemClick={contourItemClickHandler}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'list',
|
||||
label: 'RTSTRUCT',
|
||||
target: 'rt-panel',
|
||||
isDisabled: studies => {
|
||||
if (!studies) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < studies.length; i++) {
|
||||
const study = studies[i];
|
||||
|
||||
if (study && study.series) {
|
||||
for (let j = 0; j < study.series.length; j++) {
|
||||
const series = study.series[j];
|
||||
console.log(studies);
|
||||
if (['RTSTRUCT', 'RTPLAN', 'RTDOSE'].includes(series.Modality)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'rt-panel',
|
||||
component: ExtendedRTPanel,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER'],
|
||||
};
|
||||
},
|
||||
getSopClassHandlerModule({ servicesManager }) {
|
||||
return sopClassHandlerModule;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import RTStructDisplayTool from './tools/RTStructDisplayTool';
|
||||
import rtStructModule from './tools/modules/rtStructModule';
|
||||
|
||||
import TOOL_NAMES from './utils/toolNames';
|
||||
|
||||
const defaultConfig = {
|
||||
TOOL_NAMES: {
|
||||
RT_STRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} configuration
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
const conifg = Object.assign({}, defaultConfig, configuration);
|
||||
|
||||
TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL = conifg.TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL;
|
||||
|
||||
cornerstoneTools.register('module', 'rtstruct', rtStructModule);
|
||||
cornerstoneTools.addTool(RTStructDisplayTool);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
|
||||
import TOOL_NAMES from './utils/toolNames';
|
||||
|
||||
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
|
||||
const globalImageIdSpecificToolStateManager =
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
const { DicomLoaderService } = OHIF.utils;
|
||||
|
||||
export default async function loadRTStruct(
|
||||
rtStructDisplaySet,
|
||||
referencedDisplaySet,
|
||||
studies
|
||||
) {
|
||||
const rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||
|
||||
// Set here is loading is asynchronous.
|
||||
// If this function throws its set back to false.
|
||||
rtStructDisplaySet.isLoaded = true;
|
||||
|
||||
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
||||
|
||||
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||
rtStructDisplaySet,
|
||||
studies
|
||||
);
|
||||
|
||||
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
|
||||
const rtStructDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||
dicomData.dict
|
||||
);
|
||||
|
||||
rtStructDataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
|
||||
dicomData.meta
|
||||
);
|
||||
|
||||
// global cornerstone tools state to attach measurements to.
|
||||
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||
|
||||
const {
|
||||
StructureSetROISequence,
|
||||
ROIContourSequence,
|
||||
RTROIObservationsSequence,
|
||||
StructureSetLabel,
|
||||
} = rtStructDataset;
|
||||
|
||||
// Define our structure set entry and add it to the rtstruct module state.
|
||||
const structureSet = {
|
||||
StructureSetLabel,
|
||||
SeriesInstanceUID: rtStructDataset.SeriesInstanceUID,
|
||||
ROIContours: [],
|
||||
referencedSeriesSequence:
|
||||
rtStructDisplaySet.metadata.ReferencedSeriesSequence,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
rtStructModule.setters.structureSet(structureSet);
|
||||
|
||||
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||
|
||||
for (let i = 0; i < ROIContourSequence.length; i++) {
|
||||
|
||||
const ROIContour = ROIContourSequence[i];
|
||||
const { ReferencedROINumber, ContourSequence } = ROIContour;
|
||||
|
||||
if (!ContourSequence) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_setROIContourMetadata(
|
||||
structureSet,
|
||||
StructureSetROISequence,
|
||||
RTROIObservationsSequence,
|
||||
ROIContour
|
||||
);
|
||||
|
||||
for (let c = 0; c < ContourSequence.length; c++) {
|
||||
const {
|
||||
ContourImageSequence,
|
||||
ContourData,
|
||||
NumberOfContourPoints,
|
||||
ContourGeometricType,
|
||||
} = ContourSequence[c];
|
||||
|
||||
if (ContourGeometricType !== 'CLOSED_PLANAR') {
|
||||
// TODO: Do we want to visualise types other than closed planar?
|
||||
// We could easily do open planar.
|
||||
continue;
|
||||
}
|
||||
|
||||
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
||||
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||
const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
|
||||
toolState,
|
||||
imageId,
|
||||
rtStructDisplayToolName
|
||||
);
|
||||
|
||||
const imagePlane = cornerstone.metaData.get('imagePlaneModule', imageId);
|
||||
const points = [];
|
||||
|
||||
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
|
||||
points.push({
|
||||
x: ContourData[p],
|
||||
y: ContourData[p + 1],
|
||||
z: ContourData[p + 2],
|
||||
});
|
||||
}
|
||||
|
||||
transformPointsToImagePlane(points, imagePlane);
|
||||
|
||||
const measurementData = {
|
||||
handles: {
|
||||
points,
|
||||
},
|
||||
structureSetSeriesInstanceUid: rtStructDataset.SeriesInstanceUID,
|
||||
ROINumber: ReferencedROINumber,
|
||||
};
|
||||
|
||||
imageIdSpecificToolData.push(measurementData);
|
||||
}
|
||||
}
|
||||
|
||||
_setToolEnabledIfNotEnabled(rtStructDisplayToolName);
|
||||
|
||||
/*
|
||||
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||
*
|
||||
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
||||
* or other components that could rely on loaded rtstructs that
|
||||
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
||||
* before the structs are fully loaded, the panel can subscribe to this custom event
|
||||
* and update itself with the new structs.
|
||||
*
|
||||
* This limitation is due to the fact that the rtmodule is an object (which will be
|
||||
* updated after the structs are loaded) that React its not aware of its changes
|
||||
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||
*
|
||||
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||
* without subscribing to external events.
|
||||
*/
|
||||
const event = new CustomEvent('extensiondicomrtrtloaded');
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
function _setROIContourMetadata(
|
||||
structureSet,
|
||||
StructureSetROISequence,
|
||||
RTROIObservationsSequence,
|
||||
ROIContour
|
||||
) {
|
||||
const StructureSetROI = StructureSetROISequence.find(
|
||||
structureSetROI =>
|
||||
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||
);
|
||||
|
||||
const ROIContourData = {
|
||||
ROINumber: StructureSetROI.ROINumber,
|
||||
ROIName: StructureSetROI.ROIName,
|
||||
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
|
||||
ROIDescription: StructureSetROI.ROIDescription,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
_setROIContourDataColor(ROIContour, ROIContourData);
|
||||
|
||||
if (RTROIObservationsSequence) {
|
||||
// If present, add additional RTROIObservations metadata.
|
||||
_setROIContourRTROIObservations(
|
||||
ROIContourData,
|
||||
RTROIObservationsSequence,
|
||||
ROIContour.ReferencedROINumber
|
||||
);
|
||||
}
|
||||
|
||||
structureSet.ROIContours.push(ROIContourData);
|
||||
}
|
||||
|
||||
function _setROIContourDataColor(ROIContour, ROIContourData) {
|
||||
let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;
|
||||
|
||||
if (!ROIDisplayColor && RecommendedDisplayCIELabValue) {
|
||||
// If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color.
|
||||
ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue);
|
||||
}
|
||||
|
||||
if (ROIDisplayColor) {
|
||||
ROIContourData.colorArray = [...ROIDisplayColor];
|
||||
} else {
|
||||
//Choose a color from the cornerstoneTools colorLUT
|
||||
// We sample from the default color LUT here (i.e. 0), as we have nothing else to go on.
|
||||
const { getters } = cornerstoneTools.getModule('segmentation');
|
||||
const color = getters.colorForSegmentIndexColorLUT(
|
||||
0,
|
||||
ROIContourData.ROINumber
|
||||
);
|
||||
|
||||
ROIContourData.colorArray = [...color];
|
||||
}
|
||||
}
|
||||
|
||||
function _setROIContourRTROIObservations(
|
||||
ROIContourData,
|
||||
RTROIObservationsSequence,
|
||||
ROINumber
|
||||
) {
|
||||
const RTROIObservations = RTROIObservationsSequence.find(
|
||||
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
|
||||
);
|
||||
|
||||
if (RTROIObservations) {
|
||||
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
|
||||
const {
|
||||
ObservationNumber,
|
||||
ROIObservationDescription,
|
||||
RTROIInterpretedType,
|
||||
ROIInterpreter,
|
||||
} = RTROIObservations;
|
||||
|
||||
ROIContourData.RTROIObservations = {
|
||||
ObservationNumber,
|
||||
ROIObservationDescription,
|
||||
RTROIInterpretedType,
|
||||
ROIInterpreter,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function _setToolEnabledIfNotEnabled(toolName) {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
const { element } = enabledElement;
|
||||
const tool = cornerstoneTools.getToolForElement(element, toolName);
|
||||
|
||||
if (tool.mode !== 'enabled') {
|
||||
// If not already active or passive, set passive so contours render.
|
||||
cornerstoneTools.setToolEnabled(toolName);
|
||||
}
|
||||
|
||||
cornerstone.updateImage(element);
|
||||
});
|
||||
}
|
||||
|
||||
function _getOrCreateImageIdSpecificToolData(toolState, imageId, toolName) {
|
||||
if (toolState.hasOwnProperty(imageId) === false) {
|
||||
toolState[imageId] = {};
|
||||
}
|
||||
|
||||
const imageIdToolState = toolState[imageId];
|
||||
|
||||
// If we don't have tool state for this type of tool, add an empty object
|
||||
if (imageIdToolState.hasOwnProperty(toolName) === false) {
|
||||
imageIdToolState[toolName] = {
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
return imageIdToolState[toolName].data;
|
||||
}
|
||||
|
||||
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
||||
imageIdSopInstanceUidPairsEntry =>
|
||||
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||
);
|
||||
|
||||
return imageIdSopInstanceUidPairsEntry.imageId;
|
||||
};
|
||||
|
||||
function _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
const study = studies.find(
|
||||
study => study.StudyInstanceUID === StudyInstanceUID
|
||||
);
|
||||
|
||||
const displaySets = study.displaySets.filter(set => {
|
||||
return set.SeriesInstanceUID === SeriesInstanceUID;
|
||||
});
|
||||
|
||||
if (displaySets.length > 1) {
|
||||
console.warn(
|
||||
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||
);
|
||||
// TODO -> We could make check the instance list and see if any match?
|
||||
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
|
||||
// ^ Will that even happen?
|
||||
}
|
||||
|
||||
const referencedDisplaySet = displaySets[0];
|
||||
|
||||
return referencedDisplaySet.images.map(image => {
|
||||
return {
|
||||
imageId: image.getImageId(),
|
||||
sopInstanceUID: image.getSOPInstanceUID(),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
|
||||
|
||||
import TOOL_NAMES from '../utils/toolNames';
|
||||
|
||||
// Cornerstone 3rd party dev kit imports
|
||||
const draw = importInternal('drawing/draw');
|
||||
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
|
||||
const getNewContext = importInternal('drawing/getNewContext');
|
||||
const BaseTool = importInternal('base/BaseTool');
|
||||
|
||||
/**
|
||||
* @class RTStructDisplayTool - Renders RTSTRUCT data in a read only manner (i.e. as an overlay).
|
||||
* @extends cornerstoneTools.BaseTool
|
||||
*/
|
||||
export default class RTStructDisplayTool extends BaseTool {
|
||||
constructor(props = {}) {
|
||||
const defaultProps = {
|
||||
mixins: ['enabledOrDisabledBinaryTool'],
|
||||
name: TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL,
|
||||
};
|
||||
|
||||
const initialProps = Object.assign(defaultProps, props);
|
||||
|
||||
super(initialProps);
|
||||
|
||||
this._rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||
}
|
||||
|
||||
renderToolData(evt) {
|
||||
const eventData = evt.detail;
|
||||
const rtstructModule = this._rtStructModule;
|
||||
|
||||
const toolState = getToolState(evt.currentTarget, this.name);
|
||||
|
||||
if (!toolState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { lineWidth, opacity } = rtstructModule.configuration;
|
||||
|
||||
// We have tool data for this element - iterate over each one and draw it
|
||||
const context = getNewContext(eventData.canvasContext.canvas);
|
||||
|
||||
for (let i = 0; i < toolState.data.length; i++) {
|
||||
const data = toolState.data[i];
|
||||
|
||||
const structureSet = rtstructModule.getters.structureSet(
|
||||
data.structureSetSeriesInstanceUid
|
||||
);
|
||||
|
||||
// Don't render if entire StructureSet is hidden.
|
||||
if (structureSet.visible === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ROIContourData = rtstructModule.getters.ROIContour(
|
||||
data.structureSetSeriesInstanceUid,
|
||||
data.ROINumber
|
||||
);
|
||||
|
||||
// Don't render if ROIContour is hidden.
|
||||
if (ROIContourData.visible === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const points = data.handles.points;
|
||||
|
||||
if (!points.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const colorArray = ROIContourData.colorArray;
|
||||
const color = `rgba(${colorArray[0]},${colorArray[1]},${
|
||||
colorArray[2]
|
||||
},${opacity})`;
|
||||
|
||||
lineWidth;
|
||||
|
||||
draw(context, context => {
|
||||
drawJoinedLines(
|
||||
context,
|
||||
eventData.element,
|
||||
points[points.length - 1],
|
||||
points,
|
||||
{
|
||||
color,
|
||||
lineWidth,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import TOOL_NAMES from '../../../utils/toolNames';
|
||||
|
||||
const globalImageIdSpecificToolStateManager =
|
||||
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||
|
||||
/**
|
||||
* getImageIdOfCenterFrameOfROIContour - Returns the imageId nearest to the center of the
|
||||
* volumes extent in the stack's Z direction which contains one of the ROIContour's Contours.
|
||||
* @param {string} structureSetSeriesInstanceUid The SeriesInstanceUID of the structure set.
|
||||
* @param {number} ROINumber The ROINumber of the region.
|
||||
* @param {string} imageIdsInStack The stack of imageIds.
|
||||
*
|
||||
* @returns The imageId
|
||||
*/
|
||||
export default function getImageIdOfCenterFrameOfROIContour(
|
||||
structureSetSeriesInstanceUid,
|
||||
ROINumber,
|
||||
imageIdsInStack
|
||||
) {
|
||||
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||
const imageIdIndicies = [];
|
||||
|
||||
for (let i = 0; i < imageIdsInStack.length; i++) {
|
||||
const imageId = imageIdsInStack[i];
|
||||
|
||||
const imageIdSpecificToolState = toolState[imageId];
|
||||
|
||||
if (
|
||||
!imageIdSpecificToolState ||
|
||||
!imageIdSpecificToolState[rtStructDisplayToolName] ||
|
||||
!imageIdSpecificToolState[rtStructDisplayToolName].data
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const toolData = imageIdSpecificToolState[rtStructDisplayToolName].data;
|
||||
|
||||
if (
|
||||
_toolDataContainsROIContour(
|
||||
toolData,
|
||||
structureSetSeriesInstanceUid,
|
||||
ROINumber
|
||||
)
|
||||
) {
|
||||
imageIdIndicies.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
const centerImageIdIndex =
|
||||
imageIdIndicies[Math.floor(imageIdIndicies.length / 2)];
|
||||
|
||||
return imageIdsInStack[centerImageIdIndex];
|
||||
}
|
||||
|
||||
function _toolDataContainsROIContour(
|
||||
toolData,
|
||||
structureSetSeriesInstanceUid,
|
||||
ROINumber
|
||||
) {
|
||||
return !!toolData.some(
|
||||
toolDataI =>
|
||||
toolDataI.structureSetSeriesInstanceUid ===
|
||||
structureSetSeriesInstanceUid && toolDataI.ROINumber === ROINumber
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* structureSetReferencesSeriesInstanceUid - Returns true if the structure set
|
||||
* references the given SeriesInstanceUID.
|
||||
* @param {*} StructureSet
|
||||
* @param {*} SeriesInstanceUID
|
||||
*/
|
||||
export default function structureSetReferencesSeriesInstanceUid(
|
||||
StructureSet,
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
const { referencedSeriesSequence } = StructureSet;
|
||||
return referencedSeriesSequence.some(
|
||||
referencedSeries =>
|
||||
referencedSeries.SeriesInstanceUID ===
|
||||
SeriesInstanceUID
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import getImageIdOfCenterFrameOfROIContour from './lib/getImageIdOfCenterFrameOfROIContour';
|
||||
import structureSetReferencesSeriesInstanceUid from './lib/structureSetReferencesSeriesInstanceUid';
|
||||
|
||||
// We should put this as a helper somewhere as we are using it in mutliple places.
|
||||
function refreshViewport() {
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
}
|
||||
|
||||
const configuration = {
|
||||
lineWidth: 3,
|
||||
opacity: 1.0,
|
||||
};
|
||||
|
||||
const state = {
|
||||
StructureSets: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a structure set to the module.
|
||||
* @param {Object} structureSetData The structure set data.
|
||||
*/
|
||||
function setStructureSet(structureSetData) {
|
||||
state.StructureSets.push(structureSetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the StructureSet with the given SeriesInstanceUID.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
*
|
||||
* @returns {Object} The StructureSet.
|
||||
*/
|
||||
function getStructureSet(SeriesInstanceUID) {
|
||||
return state.StructureSets.find(
|
||||
structureSet => structureSet.SeriesInstanceUID === SeriesInstanceUID
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ROI Contour with the given ROINumber on the StructureSet defined by the given
|
||||
* SeriesInstanceUID.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour to fetch.
|
||||
*/
|
||||
function getROIContour(SeriesInstanceUID, ROINumber) {
|
||||
const structureSet = getStructureSet(SeriesInstanceUID);
|
||||
|
||||
if (!structureSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
return structureSet.ROIContours.find(
|
||||
ROIContour => ROIContour.ROINumber === ROINumber
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the StructureSet.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
*/
|
||||
function setHideStructureSet(SeriesInstanceUID) {
|
||||
_setStructureSetVisible(SeriesInstanceUID, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the StructureSet.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
*/
|
||||
function setShowStructureSet(SeriesInstanceUID) {
|
||||
_setStructureSetVisible(SeriesInstanceUID, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the StructureSet.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||
*/
|
||||
function _setStructureSetVisible(SeriesInstanceUID, visible = true) {
|
||||
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||
|
||||
if (StructureSet) {
|
||||
StructureSet.visible = visible;
|
||||
|
||||
refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the StructureSet.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
*/
|
||||
function setToggleStructureSet(SeriesInstanceUID) {
|
||||
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||
|
||||
if (StructureSet) {
|
||||
StructureSet.visible = !StructureSet.visible;
|
||||
|
||||
refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the ROIContour.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||
*/
|
||||
function setHideROIContour(SeriesInstanceUID, ROINumber) {
|
||||
_setROIContourVisible(SeriesInstanceUID, ROINumber, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the ROIContour.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||
*/
|
||||
function setShowROIContour(SeriesInstanceUID, ROINumber) {
|
||||
_setROIContourVisible(SeriesInstanceUID, ROINumber, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the ROIContour.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||
*/
|
||||
function _setROIContourVisible(SeriesInstanceUID, ROINumber, visible = true) {
|
||||
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||
|
||||
if (ROIContour) {
|
||||
ROIContour.visible = visible;
|
||||
|
||||
refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the ROIContour.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||
*/
|
||||
function setToggleROIContour(SeriesInstanceUID, ROINumber) {
|
||||
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||
|
||||
if (ROIContour) {
|
||||
ROIContour.visible = !ROIContour.visible;
|
||||
|
||||
refreshViewport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of StructureSets which reference the given SeriesInstanceUID.
|
||||
* @param {string} SeriesInstanceUID The SeriesInstanceUID to check.
|
||||
*/
|
||||
function getStructuresSetsWhichReferenceSeriesInstanceUid(
|
||||
SeriesInstanceUID
|
||||
) {
|
||||
const { StructureSets } = state;
|
||||
return StructureSets.filter(StructureSet =>
|
||||
structureSetReferencesSeriesInstanceUid(
|
||||
StructureSet,
|
||||
SeriesInstanceUID
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
getters: {
|
||||
structureSet: getStructureSet,
|
||||
ROIContour: getROIContour,
|
||||
structuresSetsWhichReferenceSeriesInstanceUid: getStructuresSetsWhichReferenceSeriesInstanceUid,
|
||||
imageIdOfCenterFrameOfROIContour: getImageIdOfCenterFrameOfROIContour,
|
||||
},
|
||||
setters: {
|
||||
structureSet: setStructureSet,
|
||||
hideROIContour: setHideROIContour,
|
||||
showROIContour: setShowROIContour,
|
||||
toggleROIContour: setToggleROIContour,
|
||||
hideStructureSet: setHideStructureSet,
|
||||
showStructureSet: setShowStructureSet,
|
||||
toggleStructureSet: setToggleStructureSet,
|
||||
},
|
||||
state,
|
||||
configuration,
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const TOOL_NAMES = {
|
||||
RTSTRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
|
||||
};
|
||||
|
||||
export default TOOL_NAMES;
|
||||
@@ -0,0 +1,91 @@
|
||||
export default function transformPointsToImagePlane(points, imagePlane) {
|
||||
// See Equation C.7.6.2.1-1 of the DICOM standard
|
||||
|
||||
const {
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
rowPixelSpacing: deltaI,
|
||||
columnPixelSpacing: deltaJ,
|
||||
imagePositionPatient,
|
||||
} = imagePlane;
|
||||
|
||||
const X = [rowCosines[0], rowCosines[1], rowCosines[2]];
|
||||
const Y = [columnCosines[0], columnCosines[1], columnCosines[2]];
|
||||
const S = [
|
||||
imagePositionPatient[0],
|
||||
imagePositionPatient[1],
|
||||
imagePositionPatient[2],
|
||||
];
|
||||
|
||||
// 9 sets of simulataneous equations to choose from, choose which set to solve
|
||||
// Based on the largest component of each direction cosine.
|
||||
// This avoids NaNs or floating point errors caused by dividing by very small numbers and ensures a safe mapping
|
||||
// when mapping between planes that are close to orthogonal.
|
||||
|
||||
let ix = 0;
|
||||
let iy = 0;
|
||||
let largestDirectionCosineMagnitude = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
// Find the element with the largest magnitude in each direction cosine vector.
|
||||
for (let i = 0; i < X.length; i++) {
|
||||
if (Math.abs(X[i]) > largestDirectionCosineMagnitude.x) {
|
||||
ix = i;
|
||||
largestDirectionCosineMagnitude.x = Math.abs(X[i]);
|
||||
}
|
||||
if (Math.abs(Y[i]) > largestDirectionCosineMagnitude.y) {
|
||||
iy = i;
|
||||
largestDirectionCosineMagnitude.y = Math.abs(Y[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const ci = {
|
||||
// Index of max elements in X and Y
|
||||
ix,
|
||||
iy,
|
||||
};
|
||||
|
||||
// Sanity Check
|
||||
const directionCosineMagnitude = {
|
||||
x: Math.pow(X[0], 2) + Math.pow(X[1], 2) + Math.pow(X[2], 2),
|
||||
y: Math.pow(Y[0], 2) + Math.pow(Y[1], 2) + Math.pow(Y[2], 2),
|
||||
};
|
||||
|
||||
if (directionCosineMagnitude.x < 0.99 || directionCosineMagnitude.y < 0.99) {
|
||||
throw Error(
|
||||
`Direction cosines do not sum to 1 in quadrature. There is likely a mistake in the DICOM metadata.` +
|
||||
`directionCosineMagnitudes: ${directionCosineMagnitude.x}, ${directionCosineMagnitude.y}`
|
||||
);
|
||||
}
|
||||
|
||||
// Fill in elements that won't change between points
|
||||
const c = [undefined, Y[ci.ix], X[ci.ix], undefined, X[ci.iy], Y[ci.iy]];
|
||||
|
||||
for (let pointI = 0; pointI < points.length; pointI++) {
|
||||
// Subtract imagePositionPatient from the coordinate
|
||||
const r = [
|
||||
points[pointI].x - S[0],
|
||||
points[pointI].y - S[1],
|
||||
points[pointI].z - S[2],
|
||||
];
|
||||
|
||||
// Set the variable terms in c.
|
||||
c[0] = r[ci.ix];
|
||||
c[3] = r[ci.iy];
|
||||
|
||||
// General case: Solves the two choosen simulataneous equations to go from the patient coordinate system to the imagePlane.
|
||||
const i =
|
||||
(c[0] - (c[1] * c[3]) / c[5]) /
|
||||
(c[2] * deltaI * (1 - (c[1] * c[4]) / (c[2] * c[5])));
|
||||
const j = (c[3] - c[4] * i * deltaI) / (c[5] * deltaJ);
|
||||
|
||||
// NOTE: Add (0.5, 0.5) to the coordinate, as PCS reference frame is with respect to the centre of the first pixel.
|
||||
points[pointI].x = i + 0.5;
|
||||
points[pointI].y = j + 0.5;
|
||||
delete points[pointI].z;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.1...@ohif/extension-dicom-segmentation@0.1.2) (2020-04-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.0...@ohif/extension-dicom-segmentation@0.1.1) (2020-04-01)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-segmentation",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -371,6 +371,7 @@ SegmentationPanel.propTypes = {
|
||||
}),
|
||||
activeIndex: PropTypes.number.isRequired,
|
||||
studies: PropTypes.array.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
SegmentationPanel.defaultProps = {};
|
||||
|
||||
|
||||
@@ -3,6 +3,43 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.9...@ohif/extension-vtk@1.3.10) (2020-04-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.8...@ohif/extension-vtk@1.3.9) (2020-04-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.7...@ohif/extension-vtk@1.3.8) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.6...@ohif/extension-vtk@1.3.7) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.5...@ohif/extension-vtk@1.3.6) (2020-04-01)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "1.3.6",
|
||||
"version": "1.3.10",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -53,8 +53,8 @@
|
||||
"react-vtkjs-viewport": "^0.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^2.6.5",
|
||||
"@ohif/ui": "^1.4.1",
|
||||
"@ohif/core": "^2.6.9",
|
||||
"@ohif/ui": "^1.4.3",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { getImageData, loadImageData } from 'react-vtkjs-viewport';
|
||||
import { getReactVtkjsViewport } from './utils/getReactVtkjsViewport';
|
||||
import ConnectedVTKViewport from './ConnectedVTKViewport';
|
||||
import LoadingIndicator from './LoadingIndicator.js';
|
||||
import OHIF from '@ohif/core';
|
||||
@@ -69,7 +69,7 @@ class OHIFVTKViewport extends Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onScroll: () => {},
|
||||
onScroll: () => { },
|
||||
};
|
||||
|
||||
static id = 'OHIFVTKViewport';
|
||||
@@ -133,7 +133,8 @@ class OHIFVTKViewport extends Component {
|
||||
displaySetInstanceUID,
|
||||
SOPClassUID,
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
frameIndex,
|
||||
reactVtkjsViewport
|
||||
) => {
|
||||
const stack = OHIFVTKViewport.getCornerstoneStack(
|
||||
studies,
|
||||
@@ -144,7 +145,10 @@ class OHIFVTKViewport extends Component {
|
||||
frameIndex
|
||||
);
|
||||
|
||||
const imageDataObject = getImageData(stack.imageIds, displaySetInstanceUID);
|
||||
const imageDataObject = reactVtkjsViewport.getImageData(
|
||||
stack.imageIds,
|
||||
displaySetInstanceUID
|
||||
);
|
||||
let labelmapDataObject;
|
||||
let labelmapColorLUT;
|
||||
|
||||
@@ -257,7 +261,9 @@ class OHIFVTKViewport extends Component {
|
||||
return volumeActor;
|
||||
}
|
||||
|
||||
setStateFromProps() {
|
||||
async setStateFromProps() {
|
||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
||||
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
@@ -295,8 +301,10 @@ class OHIFVTKViewport extends Component {
|
||||
studies,
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
sopClassUIDs[0],
|
||||
SOPInstanceUID,
|
||||
frameIndex
|
||||
frameIndex,
|
||||
reactVtkjsViewport
|
||||
);
|
||||
|
||||
this.imageDataObject = imageDataObject;
|
||||
@@ -317,7 +325,7 @@ class OHIFVTKViewport extends Component {
|
||||
dataDetails,
|
||||
},
|
||||
() => {
|
||||
this.loadProgressively(imageDataObject);
|
||||
this.loadProgressively(imageDataObject, reactVtkjsViewport);
|
||||
|
||||
// TODO: There must be a better way to do this.
|
||||
// We do this so that if all the data is available the react-vtkjs-viewport
|
||||
@@ -335,26 +343,26 @@ class OHIFVTKViewport extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setStateFromProps();
|
||||
async componentDidMount() {
|
||||
await this.setStateFromProps();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
async componentDidUpdate(prevProps) {
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
|
||||
if (
|
||||
displaySet.displaySetInstanceUID !==
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
prevDisplaySet.displaySetInstanceUID ||
|
||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||
) {
|
||||
this.setStateFromProps();
|
||||
await this.setStateFromProps();
|
||||
}
|
||||
}
|
||||
|
||||
loadProgressively(imageDataObject) {
|
||||
loadImageData(imageDataObject);
|
||||
loadProgressively(imageDataObject, reactVtkjsViewport) {
|
||||
reactVtkjsViewport.loadImageData(imageDataObject);
|
||||
|
||||
const { isLoading, insertPixelDataPromises } = imageDataObject;
|
||||
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { View2D } from 'react-vtkjs-viewport';
|
||||
// import { View2D } from 'react-vtkjs-viewport';
|
||||
import PropTypes from 'prop-types';
|
||||
import asyncComponent from './asyncComponent.js';
|
||||
import { getReactVtkjsViewport } from './utils/getReactVtkjsViewport';
|
||||
|
||||
import './VTKViewport.css';
|
||||
|
||||
const View2D = asyncComponent(async () => {
|
||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
||||
return { default: reactVtkjsViewport.View2D };
|
||||
});
|
||||
|
||||
const VTKViewport = props => {
|
||||
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||
|
||||
@@ -52,7 +59,7 @@ VTKViewport.propTypes = {
|
||||
};
|
||||
|
||||
VTKViewport.defaultProps = {
|
||||
onScroll: () => {},
|
||||
onScroll: () => { },
|
||||
};
|
||||
|
||||
export default VTKViewport;
|
||||
@@ -1,10 +1,5 @@
|
||||
import throttle from 'lodash.throttle';
|
||||
import {
|
||||
vtkInteractorStyleMPRCrosshairs,
|
||||
vtkInteractorStyleMPRWindowLevel,
|
||||
vtkInteractorStyleMPRRotate,
|
||||
vtkSVGCrosshairsWidget,
|
||||
} from 'react-vtkjs-viewport';
|
||||
import { getReactVtkjsViewport } from './utils/getReactVtkjsViewport';
|
||||
|
||||
import setMPRLayout from './utils/setMPRLayout.js';
|
||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||
@@ -124,16 +119,18 @@ const commandsModule = ({ commandsManager }) => {
|
||||
|
||||
_setView(api, [0, 1, 0], [0, 0, 1]);
|
||||
},
|
||||
enableRotateTool: () => {
|
||||
enableRotateTool: async () => {
|
||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||
const istyle = reactVtkjsViewport.vtkInteractorStyleMPRRotate.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle });
|
||||
});
|
||||
},
|
||||
enableCrosshairsTool: () => {
|
||||
enableCrosshairsTool: async () => {
|
||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
||||
apis.forEach((api, apiIndex) => {
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
const istyle = reactVtkjsViewport.vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
@@ -141,7 +138,7 @@ const commandsModule = ({ commandsManager }) => {
|
||||
});
|
||||
});
|
||||
},
|
||||
enableLevelTool: () => {
|
||||
enableLevelTool: async () => {
|
||||
function updateVOI(apis, windowWidth, windowCenter) {
|
||||
apis.forEach(api => {
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
@@ -162,8 +159,9 @@ const commandsModule = ({ commandsManager }) => {
|
||||
},
|
||||
};
|
||||
|
||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||
const istyle = reactVtkjsViewport.vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle, callbacks });
|
||||
});
|
||||
@@ -260,14 +258,15 @@ const commandsModule = ({ commandsManager }) => {
|
||||
}
|
||||
|
||||
// Add widgets and set default interactorStyle of each viewport.
|
||||
const reactVtkjsViewport = await getReactVtkjsViewport();
|
||||
apis.forEach((api, apiIndex) => {
|
||||
api.addSVGWidget(
|
||||
vtkSVGCrosshairsWidget.newInstance(),
|
||||
reactVtkjsViewport.vtkSVGCrosshairsWidget.newInstance(),
|
||||
'crosshairsWidget'
|
||||
);
|
||||
|
||||
const uid = api.uid;
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
const istyle = reactVtkjsViewport.vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function getReactVtkjsViewport() {
|
||||
return import(
|
||||
/* webpackChunkName: "ReactVtkjsViewport" */ 'react-vtkjs-viewport'
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.6.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.8...@ohif/core@2.6.9) (2020-04-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.6.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.7...@ohif/core@2.6.8) (2020-04-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.6...@ohif/core@2.6.7) (2020-04-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix multiframe images ([#1595](https://github.com/OHIF/Viewers/issues/1595)) ([9e0bd52](https://github.com/OHIF/Viewers/commit/9e0bd52c6a86648eb6673344a8555ad787043e5c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.5...@ohif/core@2.6.6) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.5](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.4...@ohif/core@2.6.5) (2020-04-01)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "2.6.5",
|
||||
"version": "2.6.9",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -39,7 +39,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"ajv": "^6.10.0",
|
||||
"dcmjs": "^0.8.3",
|
||||
"dcmjs": "^0.12.0",
|
||||
"dicomweb-client": "^0.5.2",
|
||||
"immer": "6.0.2",
|
||||
"isomorphic-base64": "^1.0.2",
|
||||
|
||||
@@ -10,6 +10,13 @@ const findMostRecentStructuredReport = studies => {
|
||||
studies.forEach(study => {
|
||||
const allSeries = study.getSeries ? study.getSeries() : [];
|
||||
allSeries.forEach(series => {
|
||||
// Skip series that may not have instances yet
|
||||
// This can happen if we have retrieved just the initial
|
||||
// details about the series via QIDO-RS, but not the full metadata
|
||||
if (!series.instances.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isStructuredReportSeries(series)) {
|
||||
if (
|
||||
!mostRecentStructuredReport ||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import {
|
||||
clearStudyLoadingProgress,
|
||||
setStudyLoadingProgress,
|
||||
} from '../redux/actions';
|
||||
import StackManager from '../utils/StackManager';
|
||||
import { getCornerstoneWADOImageLoader } from '../utils/cornerstoneWADOImageLoader';
|
||||
|
||||
class BaseLoadingListener {
|
||||
constructor(stack, options = {}) {
|
||||
@@ -103,7 +103,8 @@ class DICOMFileLoadingListener extends BaseLoadingListener {
|
||||
this._checkCachedData();
|
||||
}
|
||||
|
||||
_checkCachedData() {
|
||||
async _checkCachedData() {
|
||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
||||
const dataSet = cornerstoneWADOImageLoader.wadouri.dataSetCacheManager.get(
|
||||
this._dataSetUrl
|
||||
);
|
||||
|
||||
@@ -78,7 +78,7 @@ export class StudyMetadata extends Metadata {
|
||||
Object.defineProperty(this, 'studyInstanceUID', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
get: function () {
|
||||
get: function() {
|
||||
return this.getStudyInstanceUID();
|
||||
},
|
||||
});
|
||||
@@ -100,20 +100,32 @@ export class StudyMetadata extends Metadata {
|
||||
* Split a series metadata object into display sets
|
||||
* @param {Array} sopClassHandlerModules List of SOP Class Modules
|
||||
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||
* @param {Array} [givenDisplaySets] An optional list to which the display sets will be appended
|
||||
* @returns {Array} The list of display sets created for the given series object
|
||||
*/
|
||||
_createDisplaySetsForSeries(
|
||||
sopClassHandlerModules,
|
||||
series,
|
||||
givenDisplaySets
|
||||
) {
|
||||
const study = this;
|
||||
const displaySets = Array.isArray(givenDisplaySets) ? givenDisplaySets : [];
|
||||
const displaySets = [];
|
||||
|
||||
const anyInstances = series.getInstanceCount() > 0;
|
||||
|
||||
if (!anyInstances) {
|
||||
return;
|
||||
const displaySet = new ImageSet([]);
|
||||
const seriesData = series.getData();
|
||||
|
||||
displaySet.setAttributes({
|
||||
displaySetInstanceUID: displaySet.uid,
|
||||
SeriesInstanceUID: seriesData.SeriesInstanceUID,
|
||||
SeriesDescription: seriesData.SeriesDescription,
|
||||
SeriesNumber: seriesData.SeriesNumber,
|
||||
Modality: seriesData.Modality,
|
||||
});
|
||||
|
||||
displaySets.push(displaySet);
|
||||
|
||||
return displaySets;
|
||||
}
|
||||
|
||||
const sopClassUIDs = getSopClassUIDs(series);
|
||||
@@ -282,13 +294,14 @@ export class StudyMetadata extends Metadata {
|
||||
|
||||
// Loop through the series (SeriesMetadata)
|
||||
this.forEachSeries(
|
||||
series =>
|
||||
void this._createDisplaySetsForSeries(
|
||||
sopClassHandlerModules,
|
||||
series,
|
||||
displaySets
|
||||
)
|
||||
);
|
||||
series => {
|
||||
const displaySetsForSeries = this._createDisplaySetsForSeries(
|
||||
sopClassHandlerModules,
|
||||
series,
|
||||
);
|
||||
|
||||
displaySets.push(...displaySetsForSeries);
|
||||
});
|
||||
|
||||
return sortDisplaySetList(displaySets);
|
||||
}
|
||||
@@ -304,13 +317,27 @@ export class StudyMetadata extends Metadata {
|
||||
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
|
||||
*/
|
||||
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
|
||||
if (this.containsSeries(series)) {
|
||||
this.setDisplaySets(
|
||||
this._createDisplaySetsForSeries(sopClassHandlerModules, series)
|
||||
);
|
||||
return true;
|
||||
if (!this.containsSeries(series)) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
const displaySets = this._createDisplaySetsForSeries(sopClassHandlerModules, series)
|
||||
|
||||
// Note: filtering in place because this._displaySets has writable: false
|
||||
for (let i = this._displaySets.length - 1; i >= 0; i--) {
|
||||
const displaySet = this._displaySets[i];
|
||||
if (displaySet.SeriesInstanceUID === series.getSeriesInstanceUID()) {
|
||||
this._displaySets.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
displaySets.forEach(displaySet => {
|
||||
this.addDisplaySet(displaySet);
|
||||
});
|
||||
|
||||
this.sortDisplaySets();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,6 +346,9 @@ export class StudyMetadata extends Metadata {
|
||||
*/
|
||||
setDisplaySets(displaySets) {
|
||||
if (Array.isArray(displaySets) && displaySets.length > 0) {
|
||||
// TODO: This is weird, can we just switch it to writable: true?
|
||||
this._displaySets.splice(0);
|
||||
|
||||
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
||||
this.sortDisplaySets();
|
||||
}
|
||||
@@ -407,6 +437,30 @@ export class StudyMetadata extends Metadata {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a series in the current study by SeriesInstanceUID.
|
||||
* @param {String} SeriesInstanceUID The SeriesInstanceUID to be updated
|
||||
* @param {SeriesMetadata} series The series to be added to the current study.
|
||||
* @returns {boolean} Returns true on success, false otherwise.
|
||||
*/
|
||||
updateSeries(SeriesInstanceUID, series) {
|
||||
const index = this._series.findIndex(series => {
|
||||
return series.getSeriesInstanceUID() === SeriesInstanceUID;
|
||||
});
|
||||
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(series instanceof SeriesMetadata)) {
|
||||
throw new Error('Series must be an instance of SeriesMetadata');
|
||||
}
|
||||
|
||||
this._series[index] = series;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a series by index.
|
||||
* @param {number} index An integer representing a list index.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { api } from 'dicomweb-client';
|
||||
import dcmjs from 'dcmjs';
|
||||
import DICOMWeb from '../../../DICOMWeb/';
|
||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||
import { sortStudySeries, sortingCriteria } from '../../sortStudy';
|
||||
@@ -8,6 +9,8 @@ import {
|
||||
addInstancesToStudy,
|
||||
} from './studyInstanceHelpers';
|
||||
|
||||
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||
|
||||
/**
|
||||
* Map series to an array of SeriesInstanceUID
|
||||
* @param {Arrays} series list of Series Instance UIDs
|
||||
@@ -101,15 +104,21 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
||||
|
||||
async preLoad() {
|
||||
const preLoaders = this.getPreLoaders();
|
||||
const result = await this.runLoaders(preLoaders);
|
||||
|
||||
// seriesData is the result of the QIDO-RS Search For Series request
|
||||
// It's an array of Objects containing DICOM Tag values at the Series level
|
||||
const seriesData = await this.runLoaders(preLoaders);
|
||||
|
||||
const seriesSorted = sortStudySeries(
|
||||
result,
|
||||
seriesData,
|
||||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
|
||||
);
|
||||
const seriesInstanceUIDsMap = mapStudySeries(seriesSorted);
|
||||
|
||||
return seriesInstanceUIDsMap;
|
||||
return {
|
||||
seriesInstanceUIDsMap,
|
||||
seriesData
|
||||
};
|
||||
}
|
||||
|
||||
async load(preLoadData) {
|
||||
@@ -118,7 +127,7 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
||||
const seriesAsyncLoader = makeSeriesAsyncLoader(
|
||||
client,
|
||||
studyInstanceUID,
|
||||
preLoadData
|
||||
preLoadData.seriesInstanceUIDsMap
|
||||
);
|
||||
|
||||
const firstSeries = await seriesAsyncLoader.next();
|
||||
@@ -126,16 +135,38 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
||||
return {
|
||||
sopInstances: firstSeries.sopInstances,
|
||||
asyncLoader: seriesAsyncLoader,
|
||||
seriesData: preLoadData.seriesData,
|
||||
};
|
||||
}
|
||||
|
||||
async posLoad(loadData) {
|
||||
const { server } = this;
|
||||
|
||||
const { sopInstances, asyncLoader } = loadData;
|
||||
const { sopInstances, asyncLoader, seriesData } = loadData;
|
||||
|
||||
const study = await createStudyFromSOPInstanceList(server, sopInstances);
|
||||
|
||||
// TODO: Should this be in a helper
|
||||
const seriesDataNaturalized = seriesData.map(naturalizeDataset);
|
||||
|
||||
seriesDataNaturalized.forEach((series, idx) => {
|
||||
const seriesDataFromQIDO = {
|
||||
SeriesInstanceUID: series.SeriesInstanceUID,
|
||||
SeriesDescription: series.SeriesDescription,
|
||||
SeriesNumber: series.SeriesNumber,
|
||||
Modality: series.Modality,
|
||||
instances: []
|
||||
};
|
||||
|
||||
if (study.series[idx]) {
|
||||
study.series[idx] = Object.assign(seriesDataFromQIDO, study.series[idx]);
|
||||
} else {
|
||||
study.series[idx] = seriesDataFromQIDO;
|
||||
}
|
||||
|
||||
study.seriesMap[series.SeriesInstanceUID] = study.series[idx];
|
||||
});
|
||||
|
||||
if (asyncLoader.hasNext()) {
|
||||
attachSeriesLoader(server, study, asyncLoader);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import DICOMWeb from '../../../DICOMWeb';
|
||||
import metadataProvider from '../../../classes/MetadataProvider';
|
||||
import getWADORSImageId from '../../../utils/getWADORSImageId';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import getReferencedSeriesSequence from './getReferencedSeriesSequence';
|
||||
import { getCornerstoneWADOImageLoader } from '../../../utils/cornerstoneWADOImageLoader';
|
||||
|
||||
/**
|
||||
* Create a plain JS object that describes a study (a study descriptor object)
|
||||
@@ -144,7 +144,7 @@ async function makeSOPInstance(server, study, instance) {
|
||||
wadorsuri,
|
||||
wadoRoot: server.wadoRoot,
|
||||
imageRendering: server.imageRendering,
|
||||
thumbnailRendering: server.thumbnailRendering,
|
||||
thumbnailRendering: server.thumbnailRendering
|
||||
};
|
||||
|
||||
series.instances.push(sopInstance);
|
||||
@@ -160,6 +160,7 @@ async function makeSOPInstance(server, study, instance) {
|
||||
|
||||
const wadoRSMetadata = Object.assign(instance);
|
||||
|
||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
||||
if (sopInstance.NumberOfFrames) {
|
||||
for (let i = 0; i < sopInstance.NumberOfFrames; i++) {
|
||||
const wadorsImageId = getWADORSImageId(sopInstance, i);
|
||||
|
||||
@@ -35,7 +35,8 @@ function createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks) {
|
||||
imageIndex: imageIndex + 1,
|
||||
};
|
||||
|
||||
const NumberOfFrames = image.NumberOfFrames;
|
||||
const naturalizedInstance = instance.getData().metadata;
|
||||
const NumberOfFrames = naturalizedInstance.NumberOfFrames;
|
||||
|
||||
if (NumberOfFrames > 1) {
|
||||
for (let i = 0; i < NumberOfFrames; i++) {
|
||||
@@ -60,8 +61,6 @@ function createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks) {
|
||||
imageId = getImageId(image);
|
||||
imageIds.push(imageId);
|
||||
|
||||
const naturalizedInstance = instance.getData().metadata;
|
||||
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function getCornerstoneWADOImageLoader() {
|
||||
return import(
|
||||
/* webpackChunkName: "CornerstoneWADOImageLoader" */ 'cornerstone-wado-image-loader'
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import { api } from 'dicomweb-client';
|
||||
import DICOMWeb from '../DICOMWeb';
|
||||
import { getCornerstoneWADOImageLoader } from './cornerstoneWADOImageLoader';
|
||||
|
||||
const getImageId = imageObj => {
|
||||
if (!imageObj) {
|
||||
@@ -86,7 +86,7 @@ const getImageLoaderType = imageId => {
|
||||
};
|
||||
|
||||
class DicomLoaderService {
|
||||
getLocalData(dataset, studies) {
|
||||
async getLocalData(dataset, studies) {
|
||||
if (dataset && dataset.localFile) {
|
||||
// Use referenced imageInstance
|
||||
const imageInstance = getImageInstance(dataset);
|
||||
@@ -98,6 +98,7 @@ class DicomLoaderService {
|
||||
}
|
||||
|
||||
if (!someInvalidStrings(imageId)) {
|
||||
const cornerstoneWADOImageLoader = await getCornerstoneWADOImageLoader();
|
||||
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
||||
}
|
||||
}
|
||||
@@ -176,16 +177,16 @@ class DicomLoaderService {
|
||||
}
|
||||
}
|
||||
|
||||
*getLoaderIterator(dataset, studies) {
|
||||
yield this.getLocalData(dataset, studies);
|
||||
async *getLoaderIterator(dataset, studies) {
|
||||
yield await this.getLocalData(dataset, studies);
|
||||
yield this.getDataByImageType(dataset);
|
||||
yield this.getDataByDatasetType(dataset);
|
||||
}
|
||||
|
||||
findDicomDataPromise(dataset, studies) {
|
||||
async findDicomDataPromise(dataset, studies) {
|
||||
const loaderIterator = this.getLoaderIterator(dataset, studies);
|
||||
// it returns first valid retriever method.
|
||||
for (const loader of loaderIterator) {
|
||||
for await (const loader of loaderIterator) {
|
||||
if (loader) {
|
||||
return loader;
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
.locize
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.52.8](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.7...@ohif/i18n@0.52.8) (2020-04-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/i18n
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.7](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.6...@ohif/i18n@0.52.7) (2020-03-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/i18n
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/i18n",
|
||||
"version": "0.52.7",
|
||||
"version": "0.52.8",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'ar': {
|
||||
UserPreferencesModal,
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -13,4 +13,4 @@
|
||||
"Show": "Show",
|
||||
"Stop": "Stop",
|
||||
"StudyDate": "Study Date"
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
'en-US': {
|
||||
AboutModal,
|
||||
Buttons,
|
||||
@@ -21,5 +21,5 @@ export default {
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
ViewportDownloadForm,
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import Buttons from "./Buttons.json";
|
||||
import CineDialog from "./CineDialog.json";
|
||||
import Common from "./Common.json";
|
||||
import Header from "./Header.json";
|
||||
import MeasurementTable from "./MeasurementTable.json";
|
||||
import StudyList from "./StudyList.json";
|
||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import Header from './Header.json';
|
||||
import MeasurementTable from './MeasurementTable.json';
|
||||
import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'es': {
|
||||
@@ -16,4 +16,4 @@ export default {
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Acquired": "Acquis",
|
||||
"Angle": "Angle",
|
||||
"Axial": "Axial",
|
||||
"Bidirectional": "Bi-directionel",
|
||||
"Brush": "Brosse",
|
||||
"CINE": "Ciné",
|
||||
"Cancel": "Annuler",
|
||||
"Circle": "Cercle",
|
||||
"Clear": "Effacer",
|
||||
"Coronal": "Coronal",
|
||||
"Crosshairs": "Repère",
|
||||
"Ellipse": "Ellipse",
|
||||
"Elliptical": "Elliptique",
|
||||
"Flip H": "Flip H",
|
||||
"Flip V": "Flip V",
|
||||
"Freehand": "Main levée",
|
||||
"Invert": "Inverser",
|
||||
"Layout": "$t(Common:Layout)",
|
||||
"Length": "Longueur",
|
||||
"Levels": "Niveaux",
|
||||
"Magnify": "Agrandir",
|
||||
"Manual": "Manuel",
|
||||
"Measurements": "Mesures",
|
||||
"More": "$t(Common:More)",
|
||||
"Next": "$t(Common:Next)",
|
||||
"Pan": "Déplacer",
|
||||
"Play": "$t(Common:Play)",
|
||||
"Previous": "$t(Common:Previous)",
|
||||
"Probe": "Sonde",
|
||||
"ROI Window": "ROI fenêtrage",
|
||||
"Rectangle": "Rectangle",
|
||||
"Reset": "$t(Common:Reset)",
|
||||
"Reset to Defaults": "Valeurs d'usine",
|
||||
"Rotate Right": "Tourner à droite",
|
||||
"Sagittal": "Sagittal",
|
||||
"Save": "Sauvegarder",
|
||||
"Stack Scroll": "Défilement",
|
||||
"Stop": "$t(Common:Stop)",
|
||||
"Themes": "Themes",
|
||||
"Zoom": "Zoom"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Next image": "$t(Common:Play) $t(Common:Image)",
|
||||
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
||||
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||
"Skip to first image": "Retour à la première $t(Common:Image)",
|
||||
"Skip to last image": "Aller à la dernière $t(Common:Image)",
|
||||
"fps": "ips"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Image": "Image",
|
||||
"Layout": "Disposition",
|
||||
"More": "Plus",
|
||||
"Next": "Suivant",
|
||||
"Play": "Play",
|
||||
"Previous": "Précédent",
|
||||
"Reset": "Reset",
|
||||
"Stop": "Stop"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"About": "A Propos",
|
||||
"Back to Viewer": "Retour au viewer",
|
||||
"INVESTIGATIONAL USE ONLY": "Seulement pour utilisation expérimentale",
|
||||
"Options": "Options",
|
||||
"Preferences": "Préférences",
|
||||
"Study list": "Liste d'études"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Cancel": "$t(Buttons:Cancel)",
|
||||
"Reset to Defaults": "$t(Buttons:Reset to Defaults)",
|
||||
"Save": "$t(Buttons:Save)",
|
||||
"User Preferences": "Préférences utilisateur"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import Header from './Header.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'fr': {
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
Header,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import ar from './ar/';
|
||||
import en_US from './en-US/';
|
||||
import es from './es/';
|
||||
import fr from './fr/';
|
||||
import ja_JP from './ja-JP/';
|
||||
import nl from './nl/';
|
||||
import pt_BR from './pt-BR/';
|
||||
@@ -11,9 +12,10 @@ export default {
|
||||
...ar,
|
||||
...en_US,
|
||||
...es,
|
||||
...fr,
|
||||
...ja_JP,
|
||||
...nl,
|
||||
...pt_BR,
|
||||
...vi,
|
||||
...zh,
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import Buttons from "./Buttons.json";
|
||||
import CineDialog from "./CineDialog.json";
|
||||
import Common from "./Common.json";
|
||||
import Header from "./Header.json";
|
||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import Header from './Header.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'ja-JP': {
|
||||
@@ -12,4 +12,4 @@ export default {
|
||||
Header,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import Buttons from "./Buttons.json";
|
||||
import Common from "./Common.json";
|
||||
import Header from "./Header.json";
|
||||
import Buttons from './Buttons.json';
|
||||
import Common from './Common.json';
|
||||
import Header from './Header.json';
|
||||
|
||||
export default {
|
||||
'nl': {
|
||||
@@ -8,4 +8,4 @@ export default {
|
||||
Common,
|
||||
Header,
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -6,7 +6,7 @@ import DatePicker from './DatePicker.json';
|
||||
import Header from './Header.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
export default {
|
||||
'pt-BR': {
|
||||
AboutModal,
|
||||
Buttons,
|
||||
@@ -15,5 +15,5 @@ export default {
|
||||
DatePicker,
|
||||
Header,
|
||||
UserPreferencesModal,
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import Buttons from "./Buttons.json";
|
||||
import CineDialog from "./CineDialog.json";
|
||||
import Common from "./Common.json";
|
||||
import Header from "./Header.json";
|
||||
import StudyList from "./StudyList.json";
|
||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import Header from './Header.json';
|
||||
import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'vi': {
|
||||
@@ -14,4 +14,4 @@ export default {
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import Buttons from "./Buttons.json";
|
||||
import CineDialog from "./CineDialog.json";
|
||||
import Common from "./Common.json";
|
||||
import Header from "./Header.json";
|
||||
import MeasurementTable from "./MeasurementTable.json";
|
||||
import StudyList from "./StudyList.json";
|
||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import Header from './Header.json';
|
||||
import MeasurementTable from './MeasurementTable.json';
|
||||
import StudyList from './StudyList.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
export default {
|
||||
'zh': {
|
||||
@@ -16,4 +16,4 @@ export default {
|
||||
StudyList,
|
||||
UserPreferencesModal,
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -43,7 +43,7 @@ directories.forEach(directory => {
|
||||
const filename = path.basename(file);
|
||||
const namespace = path.basename(file, '.json');
|
||||
|
||||
content += `import ${namespace} from "./${filename}";\n`;
|
||||
content += `import ${namespace} from './${filename}';\n`;
|
||||
});
|
||||
|
||||
content += '\n';
|
||||
@@ -51,7 +51,7 @@ directories.forEach(directory => {
|
||||
namespaces.forEach(namespace => {
|
||||
exportLines += ` ${namespace},\n`;
|
||||
});
|
||||
exportLines += ' }\n};';
|
||||
exportLines += ' }\n};\n';
|
||||
|
||||
content += exportLines;
|
||||
|
||||
@@ -80,7 +80,7 @@ languageVariables.forEach(language => {
|
||||
fileContent += ` ...${language},\n`;
|
||||
});
|
||||
|
||||
fileContent += '};';
|
||||
fileContent += '};\n';
|
||||
|
||||
// Create one file (index.js) inside the locales folder
|
||||
// which exports each of the languages
|
||||
|
||||
@@ -3,6 +3,27 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.2...@ohif/ui@1.4.3) (2020-04-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.0...@ohif/ui@1.4.1) (2020-03-17)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "1.4.1",
|
||||
"version": "1.4.3",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -9,14 +9,21 @@ export class TableList extends Component {
|
||||
defaultItems: PropTypes.object,
|
||||
children: PropTypes.node.isRequired,
|
||||
headerTitle: PropTypes.string,
|
||||
headless: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
headless: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="tableList">
|
||||
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
||||
{this.getHeader()}
|
||||
</div>
|
||||
{!this.props.headless && (
|
||||
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
||||
{this.getHeader()}
|
||||
</div>
|
||||
)}
|
||||
<div className="tableListContent">{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -85,8 +85,12 @@ import trash from './icons/trash.svg';
|
||||
import unlink from './icons/unlink.svg';
|
||||
import user from './icons/user.svg';
|
||||
import youtube from './icons/youtube.svg';
|
||||
import eye from './icons/eye.svg';
|
||||
import eyeClosed from './icons/eye-closed.svg';
|
||||
|
||||
const ICONS = {
|
||||
eye,
|
||||
'eye-closed': eyeClosed,
|
||||
brush,
|
||||
scissors,
|
||||
user,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>
|
||||
|
After Width: | Height: | Size: 974 B |
@@ -0,0 +1 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>
|
||||
|
After Width: | Height: | Size: 557 B |
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
export function ViewportLoadingIndicator(props) {
|
||||
return (
|
||||
<div
|
||||
className="loadingIndicator"
|
||||
className="loading-indicator"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
|
||||
@@ -3,6 +3,51 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.8.19](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.18...@ohif/viewer@3.8.19) (2020-04-09)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file. See
|
||||
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.8.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.17...@ohif/viewer@3.8.18) (2020-04-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.8.17](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.16...@ohif/viewer@3.8.17) (2020-04-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.8.16](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.15...@ohif/viewer@3.8.16) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.8.15](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.14...@ohif/viewer@3.8.15) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.8.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.13...@ohif/viewer@3.8.14) (2020-04-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('OHIF Save Measurements', function() {
|
||||
cy.get('@measurementsBtn').click();
|
||||
});
|
||||
|
||||
it('checks if measurements of unsupported tools were not saved', function() {
|
||||
/*it('checks if measurements of unsupported tools were not saved', function() {
|
||||
// Add measurement for supported tool in the viewport
|
||||
cy.addLengthMeasurement();
|
||||
// Add measurement for unsupported tool in the viewport
|
||||
@@ -146,5 +146,5 @@ describe('OHIF Save Measurements', function() {
|
||||
|
||||
// Close Measurements panel
|
||||
cy.get('@measurementsBtn').click();
|
||||
});
|
||||
});*/
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/viewer",
|
||||
"version": "3.8.14",
|
||||
"version": "3.8.19",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -48,16 +48,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^2.6.5",
|
||||
"@ohif/extension-cornerstone": "^2.5.1",
|
||||
"@ohif/core": "^2.6.9",
|
||||
"@ohif/extension-cornerstone": "^2.5.2",
|
||||
"@ohif/extension-dicom-html": "^1.1.2",
|
||||
"@ohif/extension-dicom-microscopy": "^0.50.9",
|
||||
"@ohif/extension-dicom-pdf": "^1.0.3",
|
||||
"@ohif/extension-dicom-segmentation": "^0.1.1",
|
||||
"@ohif/extension-dicom-rt": "^0.0.2",
|
||||
"@ohif/extension-dicom-segmentation": "^0.1.2",
|
||||
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||
"@ohif/extension-vtk": "^1.3.6",
|
||||
"@ohif/i18n": "^0.52.7",
|
||||
"@ohif/ui": "^1.4.1",
|
||||
"@ohif/extension-vtk": "^1.3.10",
|
||||
"@ohif/i18n": "^0.52.8",
|
||||
"@ohif/ui": "^1.4.3",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
@@ -65,7 +66,7 @@
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "4.12.5",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.10.1",
|
||||
"dcmjs": "^0.12.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
"dicomweb-client": "^0.4.4",
|
||||
"hammerjs": "^2.0.8",
|
||||
|
||||
@@ -117,4 +117,12 @@ window.config = {
|
||||
},
|
||||
],
|
||||
cornerstoneExtensionConfig: {},
|
||||
// Following property limits number of simultaneous series metadata requests.
|
||||
// For http/1.x-only servers, set this to 5 or less to improve
|
||||
// on first meaningful display in viewer
|
||||
// If the server is particularly slow to respond to series metadata
|
||||
// requests as it extracts the metadata from raw files everytime,
|
||||
// try setting this to even lower value
|
||||
// Leave it undefined for no limit, sutiable for HTTP/2 enabled servers
|
||||
// maxConcurrentMetadataRequests: 5,
|
||||
};
|
||||
@@ -34,10 +34,7 @@ import i18n from '@ohif/i18n';
|
||||
import './config';
|
||||
|
||||
/** Utils */
|
||||
import {
|
||||
getUserManagerForOpenIdConnectClient,
|
||||
initWebWorkers,
|
||||
} from './utils/index.js';
|
||||
import { getUserManagerForOpenIdConnectClient } from './utils/index.js';
|
||||
|
||||
/** Extensions */
|
||||
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions';
|
||||
@@ -140,7 +137,6 @@ class App extends Component {
|
||||
*/
|
||||
_initHotkeys(appConfigHotkeys);
|
||||
_initServers(servers);
|
||||
initWebWorkers();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import OHIF from '@ohif/core';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import dicomParser from 'dicom-parser';
|
||||
import version from './version.js';
|
||||
|
||||
let homepage;
|
||||
@@ -15,31 +12,4 @@ window.info = {
|
||||
homepage,
|
||||
};
|
||||
|
||||
// For debugging
|
||||
//if (process.env.node_env === 'development') {
|
||||
window.cornerstone = cornerstone;
|
||||
window.cornerstoneWADOImageLoader = cornerstoneWADOImageLoader;
|
||||
//}
|
||||
|
||||
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
|
||||
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
|
||||
|
||||
OHIF.user.getAccessToken = () => {
|
||||
// TODO: Get the Redux store from somewhere else
|
||||
const state = window.store.getState();
|
||||
if (!state.oidc || !state.oidc.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
return state.oidc.user.access_token;
|
||||
};
|
||||
|
||||
cornerstoneWADOImageLoader.configure({
|
||||
beforeSend: function(xhr) {
|
||||
const headers = OHIF.DICOMWeb.getAuthorizationHeader();
|
||||
|
||||
if (headers.Authorization) {
|
||||
xhr.setRequestHeader('Authorization', headers.Authorization);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -116,11 +116,16 @@ const _addSeriesToStudy = (studyMetadata, series) => {
|
||||
extensionManager.modules['sopClassHandlerModule'];
|
||||
const study = studyMetadata.getData();
|
||||
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
||||
studyMetadata.addSeries(seriesMetadata);
|
||||
const existingSeries = studyMetadata.getSeriesByUID(series.SeriesInstanceUID);
|
||||
if (existingSeries) {
|
||||
studyMetadata.updateSeries(series.SeriesInstanceUID, seriesMetadata);
|
||||
} else {
|
||||
studyMetadata.addSeries(seriesMetadata);
|
||||
}
|
||||
|
||||
studyMetadata.createAndAddDisplaySetsForSeries(
|
||||
sopClassHandlerModules,
|
||||
seriesMetadata,
|
||||
false
|
||||
);
|
||||
study.displaySets = studyMetadata.getDisplaySets();
|
||||
_updateStudyMetadataManager(study, studyMetadata);
|
||||
@@ -148,25 +153,6 @@ const _updateStudyDisplaySets = (study, studyMetadata) => {
|
||||
const _sortStudyDisplaySet = (study, studyMetadata) => {
|
||||
studyMetadata.sortDisplaySets(study.displaySets);
|
||||
};
|
||||
const _loadRemainingSeries = studyMetadata => {
|
||||
const { seriesLoader } = studyMetadata.getData();
|
||||
if (!seriesLoader) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const promisesLoaders = [];
|
||||
while (seriesLoader.hasNext()) {
|
||||
promisesLoaders.push(
|
||||
seriesLoader
|
||||
.next()
|
||||
.then(
|
||||
series => void _addSeriesToStudy(studyMetadata, series),
|
||||
error => void log.error(error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promisesLoaders);
|
||||
};
|
||||
|
||||
function ViewerRetrieveStudyData({
|
||||
server,
|
||||
@@ -180,7 +166,10 @@ function ViewerRetrieveStudyData({
|
||||
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
|
||||
const snackbarContext = useSnackbarContext();
|
||||
const { appConfig = {} } = useContext(AppContext);
|
||||
const { filterQueryParam: isFilterStrategy = false } = appConfig;
|
||||
const {
|
||||
filterQueryParam: isFilterStrategy = false,
|
||||
maxConcurrentMetadataRequests,
|
||||
} = appConfig;
|
||||
|
||||
let cancelableSeriesPromises;
|
||||
let cancelableStudiesPromises;
|
||||
@@ -241,7 +230,7 @@ function ViewerRetrieveStudyData({
|
||||
|
||||
// Attempt to load remaning series if any
|
||||
cancelableSeriesPromises[study.StudyInstanceUID] = makeCancelable(
|
||||
_loadRemainingSeries(studyMetadata)
|
||||
loadRemainingSeries(studyMetadata)
|
||||
)
|
||||
.then(result => {
|
||||
if (result && !result.isCanceled) {
|
||||
@@ -262,6 +251,27 @@ function ViewerRetrieveStudyData({
|
||||
}
|
||||
};
|
||||
|
||||
const forceRerender = () => setStudies(studies => [...studies]);
|
||||
|
||||
const loadRemainingSeries = async studyMetadata => {
|
||||
const { seriesLoader } = studyMetadata.getData();
|
||||
if (!seriesLoader) return;
|
||||
|
||||
const loadNextSeries = async () => {
|
||||
if (!seriesLoader.hasNext()) return;
|
||||
const series = await seriesLoader.next();
|
||||
_addSeriesToStudy(studyMetadata, series);
|
||||
forceRerender();
|
||||
return loadNextSeries();
|
||||
};
|
||||
|
||||
const concurrentRequestsAllowed = maxConcurrentMetadataRequests || studyMetadata.getSeriesCount();
|
||||
const promises = Array(concurrentRequestsAllowed)
|
||||
.fill(null)
|
||||
.map(loadNextSeries);
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
const loadStudies = async () => {
|
||||
try {
|
||||
const filters = {};
|
||||
|
||||
@@ -26,6 +26,7 @@ import ReactDOM from 'react-dom';
|
||||
import OHIFVTKExtension from '@ohif/extension-vtk';
|
||||
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
||||
import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
|
||||
import OHIFDicomRtExtension from '@ohif/extension-dicom-rt';
|
||||
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
||||
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
||||
|
||||
@@ -46,6 +47,7 @@ const appProps = {
|
||||
OHIFDicomMicroscopyExtension,
|
||||
OHIFDicomPDFExtension,
|
||||
OHIFDicomSegmentationExtension,
|
||||
OHIFDicomRtExtension,
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import FileLoaderService from './localFileLoaders/fileLoaderService';
|
||||
const processFile = async file => {
|
||||
try {
|
||||
const fileLoaderService = new FileLoaderService(file);
|
||||
const imageId = fileLoaderService.addFile(file);
|
||||
const imageId = await fileLoaderService.addFile(file);
|
||||
const image = await fileLoaderService.loadFile(file, imageId);
|
||||
const dataset = await fileLoaderService.getDataset(image, imageId);
|
||||
const studies = await fileLoaderService.getStudies(dataset, imageId);
|
||||
|
||||
Loaded 100 of 110 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user